# #region Test.MigrationPlugin [C:3] [TYPE Module] [SEMANTICS test,migration,plugin]
# @BRIEF Unit tests for MigrationPlugin — properties, get_schema, execute, and edge cases.
# @RELATION BINDS_TO -> [MigrationPlugin]
# @TEST_EDGE: missing_env_field -> Raises ValueError
# @TEST_EDGE: invalid_regex_pattern -> Regex compilation succeeds filter
# @TEST_EDGE: target_api_timeout -> Partial success with failed_dashboards
# @TEST_EDGE: no_selection_criteria -> Returns NO_SELECTION
# @TEST_EDGE: zero_dashboards_match -> Returns NO_MATCHES
# @TEST_EDGE: uninitialized_client -> Raises ValueError on falsy client
# @TEST_EDGE: non_dict_db_mapping -> Falls back to empty dict
# @TEST_EDGE: transform_retry_after_resolution -> Retries after wait_for_resolution
# @TEST_EDGE: password_error_yaml_pattern -> Extracts db_name from yaml path
# @TEST_EDGE: id_mapping_sync_failure -> Non-fatal, continues

import pytest
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

from src.plugins.migration import MigrationPlugin


# ── Helpers ──


def _make_env(id_val="env-1", name="Source Env"):
    env = MagicMock()
    env.id = id_val
    env.name = name
    env.url = "https://superset.example.com"
    env.username = "admin"
    env.password = "secret"
    return env


def _make_task_context():
    ctx = MagicMock()
    ctx.logger = MagicMock()
    ctx.logger.with_source.return_value = MagicMock()
    return ctx


def _make_dashboard(dash_id=1, title="Test Dash"):
    return {"id": dash_id, "slug": f"slug-{dash_id}", "dashboard_title": title}


class TestMigrationPluginProperties:
    """Verify static property values."""

    def test_id(self):
        plugin = MigrationPlugin()
        assert plugin.id == "superset-migration"

    def test_name(self):
        plugin = MigrationPlugin()
        assert plugin.name == "Superset Dashboard Migration"

    def test_description(self):
        plugin = MigrationPlugin()
        assert plugin.description == "Migrates dashboards between Superset environments."

    def test_version(self):
        plugin = MigrationPlugin()
        assert plugin.version == "1.0.0"

    def test_ui_route(self):
        plugin = MigrationPlugin()
        assert plugin.ui_route == "/migration"


class TestMigrationPluginGetSchema:
    """Verify get_schema — dynamic schema based on environments."""

    def test_get_schema_with_envs(self):
        plugin = MigrationPlugin()
        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [_make_env("e1", "Dev"), _make_env("e2", "Prod")]

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm):
            schema = plugin.get_schema()

        assert schema["type"] == "object"
        assert schema["properties"]["from_env"]["enum"] == ["Dev", "Prod"]
        assert schema["properties"]["to_env"]["enum"] == ["Dev", "Prod"]
        assert "dashboard_regex" in schema["properties"]
        assert "from_env" in schema["required"]
        assert "to_env" in schema["required"]

    def test_get_schema_no_envs(self):
        """Fallback to dev/prod when no environments configured."""
        plugin = MigrationPlugin()
        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = []

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm):
            schema = plugin.get_schema()

        assert schema["properties"]["from_env"]["enum"] == ["dev", "prod"]


class TestMigrationPluginExecute:
    """Verify MigrationPlugin.execute with various scenarios."""

    # ── Happy path: migrate by name + regex ──

    @pytest.mark.asyncio
    async def test_execute_happy_regex(self):
        """Happy: regex-based dashboard selection succeeds."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [
            _make_dashboard(1, "Revenue Dashboard"),
            _make_dashboard(2, "Sales Dashboard"),
        ]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip_content", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "from_env": "Source",
                "to_env": "Target",
                "dashboard_regex": "Revenue",
                "replace_db_config": False,
                "fix_cross_filters": True,
            })

            assert result["status"] == "SUCCESS"
            assert result["selected_dashboards"] == 1
            assert len(result["migrated_dashboards"]) == 1
            assert result["migrated_dashboards"][0]["title"] == "Revenue Dashboard"

    # ── Happy path: migrate by selected_ids ──

    @pytest.mark.asyncio
    async def test_execute_happy_selected_ids(self):
        """Happy: selected_ids-based dashboard selection."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [
            _make_dashboard(1, "Dash A"),
            _make_dashboard(2, "Dash B"),
            _make_dashboard(3, "Dash C"),
        ]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "from_env": "Source",
                "to_env": "Target",
                "dashboard_regex": "Revenue",
                "replace_db_config": False,
                "fix_cross_filters": True,
            })

            assert result["status"] == "SUCCESS"
            assert result["selected_dashboards"] == 1
            assert len(result["migrated_dashboards"]) == 1
            assert result["migrated_dashboards"][0]["title"] == "Revenue Dashboard"

    # ── Happy path: migrate by selected_ids ──

    @pytest.mark.asyncio
    async def test_execute_happy_selected_ids(self):
        """Happy: selected_ids-based dashboard selection."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [
            _make_dashboard(1, "Dash A"),
            _make_dashboard(2, "Dash B"),
            _make_dashboard(3, "Dash C"),
        ]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1, 3],
                "replace_db_config": False,
            })

            assert result["status"] == "SUCCESS"
            assert result["selected_dashboards"] == 2

    # ── Edge: environment not found ──

    @pytest.mark.asyncio
    async def test_execute_env_not_found(self):
        """Negative: missing environment raises ValueError."""
        plugin = MigrationPlugin()
        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = []

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm):
            with pytest.raises(ValueError, match="Could not resolve source or target"):
                await plugin.execute({
                    "from_env": "Missing",
                    "to_env": "Target",
                    "dashboard_regex": ".*",
                })

    # ── Edge: no selection criteria ──

    @pytest.mark.asyncio
    async def test_execute_no_selection_criteria(self):
        """Edge: no dashboard_regex or selected_ids returns NO_SELECTION."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, []))

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient', return_value=mock_src_client):

            result = await plugin.execute({
                "from_env": "Source",
                "to_env": "Target",
            })

            assert result["status"] == "NO_SELECTION"

    # ── Edge: zero dashboards match ──

    @pytest.mark.asyncio
    async def test_execute_zero_matches(self):
        """Edge: regex matches zero dashboards returns NO_MATCHES."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Other")]))

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient', return_value=mock_src_client):

            result = await plugin.execute({
                "from_env": "Source",
                "to_env": "Target",
                "dashboard_regex": "NonExistent",
            })

            assert result["status"] == "NO_MATCHES"

    # ── Edge: partial success ──

    @pytest.mark.asyncio
    async def test_execute_partial_success(self):
        """Edge: one dashboard fails, others succeed → PARTIAL_SUCCESS."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [
            _make_dashboard(1, "Working"),
            _make_dashboard(2, "Failing"),
        ]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(side_effect=[
            (b"zip_ok", "meta"), RuntimeError("Export failed for dash 2")
        ])
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1, 2],
                "replace_db_config": False,
            })

            assert result["status"] == "PARTIAL_SUCCESS"
            assert len(result["migrated_dashboards"]) == 1
            assert len(result["failed_dashboards"]) == 1

    # ── Edge: transform_zip fails, no replace_db_config ──

    @pytest.mark.asyncio
    async def test_execute_transform_fails_no_replace(self):
        """Edge: transform_zip fails and replace_db_config is False → dashboard in failed list."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = False

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "replace_db_config": False,
            })

            assert len(result["failed_dashboards"]) == 1
            assert "transform" in result["failed_dashboards"][0]["error"].lower()

    # ── Edge: password injection flow ──

    @pytest.mark.asyncio
    async def test_execute_password_injection(self):
        """Edge: missing DB password triggers await_input, retries with password."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_task_manager = MagicMock()
        mock_task_manager.get_task.return_value = MagicMock(
            params={"passwords": {"PostgreSQL": "secret123"}}
        )
        mock_task_manager.await_input = MagicMock()
        mock_task_manager.wait_for_input = AsyncMock()

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        # First export succeeds, import fails with password error
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        # First import fails with password error, retry succeeds
        mock_tgt_client.import_dashboard = AsyncMock(side_effect=[
            RuntimeError("Must provide a password for the database 'PostgreSQL'"),
            None,
        ])

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.dependencies.get_task_manager', return_value=mock_task_manager), \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "replace_db_config": False,
                "_task_id": "task-1",
            })

            assert result["status"] == "SUCCESS"
            assert len(result["migrated_dashboards"]) == 1
            # Password params should be deleted from task after retry
            assert "passwords" not in mock_task_manager.get_task("task-1").params

    # ── Edge: with TaskContext logger ──

    @pytest.mark.asyncio
    async def test_execute_with_task_context(self):
        """Edge: execute uses TaskContext for logging when provided."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        ctx = _make_task_context()

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
            }, context=ctx)

            assert result["status"] == "SUCCESS"

    # ── Edge: replace_db_config loads mappings from DB ──

    @pytest.mark.asyncio
    async def test_execute_replace_db_config(self):
        """Edge: replace_db_config loads DB mappings from catalog."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        # Mock DB session with Environment and DatabaseMapping
        mock_db = MagicMock()
        mock_db_env = MagicMock()
        mock_db_env.id = 1
        mock_db_env.name = "Source"
        mock_db_env2 = MagicMock()
        mock_db_env2.id = 2
        mock_db_env2.name = "Target"

        mock_mapping = MagicMock()
        mock_mapping.source_db_uuid = "src-uuid"
        mock_mapping.target_db_uuid = "tgt-uuid"

        # DB query chain: Environment filter → first returns mock_db_env
        # DatabaseMapping filter → all returns [mock_mapping]
        mock_db.query.return_value.filter.return_value.first.side_effect = [mock_db_env, mock_db_env2]
        mock_db.query.return_value.filter.return_value.all.return_value = [mock_mapping]

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal', return_value=mock_db):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "replace_db_config": True,
            })

            assert result["status"] == "SUCCESS"
            assert result["mapping_count"] > 0

    # ── Edge: uninitialized client ──

    @pytest.mark.asyncio
    async def test_execute_uninitialized_client_raises(self):
        """Edge: SupersetClient returns falsy -> ValueError."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC:

            # Return None (falsy) for the source client
            MockSC.side_effect = [None, MagicMock()]

            with pytest.raises(ValueError, match="Clients not initialized"):
                await plugin.execute({
                    "from_env": "Source",
                    "to_env": "Target",
                    "dashboard_regex": ".*",
                })

    # ── Edge: non-dict db_mappings ──

    @pytest.mark.asyncio
    async def test_execute_non_dict_db_mapping(self):
        """Edge: db_mappings param is not a dict, falls back to empty dict."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [_make_dashboard(1, "Test Dash")]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "db_mappings": "not-a-dict",  # non-dict value
            })

            assert result["status"] == "SUCCESS"

    # ── Edge: transform fails with replace_db_config, retries ──

    @pytest.mark.asyncio
    async def test_execute_transform_retry_after_resolution(self):
        """Edge: transform fails -> wait_for_resolution -> retry succeeds."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_task_manager = MagicMock()
        mock_task_manager.wait_for_resolution = AsyncMock()

        # Use a single client mock that handles both get_dashboards and import_dashboard
        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        # First call returns False (fails), second call returns True (retry succeeds)
        mock_engine.transform_zip.side_effect = [False, True]

        # Mock DB session for Environments and DatabaseMapping
        mock_db = MagicMock()
        mock_db_env = MagicMock()
        mock_db_env.id = 1
        mock_db_env.name = "Source"
        mock_db_env2 = MagicMock()
        mock_db_env2.id = 2
        mock_db_env2.name = "Target"
        # Extend side_effect to cover initial (2) + retry (2) calls to first()
        mock_db.query.return_value.filter.return_value.first.side_effect = [
            mock_db_env, mock_db_env2, mock_db_env, mock_db_env2
        ]
        mock_db.query.return_value.filter.return_value.all.return_value = []

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient', return_value=mock_client), \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.dependencies.get_task_manager', return_value=mock_task_manager), \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal', return_value=mock_db):

            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "replace_db_config": True,
                "_task_id": "task-retry-1",
            })

            assert result["status"] == "SUCCESS"
            assert mock_task_manager.wait_for_resolution.called
            # transform_zip should have been called twice
            assert mock_engine.transform_zip.call_count == 2
            # import_dashboard should have been called once (after retry)
            assert mock_client.import_dashboard.called

    # ── Edge: password error with yaml path pattern ──

    @pytest.mark.asyncio
    async def test_execute_password_error_yaml_pattern(self):
        """Edge: password error with 'databases/db_name.yaml' pattern."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_task_manager = MagicMock()
        mock_task_manager.get_task.return_value = MagicMock(
            params={"passwords": {"PostgreSQL": "secret123"}}
        )
        mock_task_manager.await_input = MagicMock()
        mock_task_manager.wait_for_input = AsyncMock()

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, [_make_dashboard(1, "Dash")]))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock(side_effect=[
            RuntimeError("Must provide a password for the database databases/PostgreSQL.yaml"),
            None,
        ])

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.dependencies.get_task_manager', return_value=mock_task_manager), \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal'):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
                "replace_db_config": False,
                "_task_id": "task-pw-1",
            })

            assert result["status"] == "SUCCESS"
            assert len(result["migrated_dashboards"]) == 1

    # ── Edge: ID mapping sync failure is non-fatal ──

    @pytest.mark.asyncio
    async def test_execute_id_mapping_sync_failure(self):
        """Edge: IdMappingService.sync_environment raises, execution continues."""
        plugin = MigrationPlugin()
        src_env = _make_env("env-1", "Source")
        tgt_env = _make_env("env-2", "Target")
        dashboards = [_make_dashboard(1, "Dash")]

        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [src_env, tgt_env]

        mock_src_client = MagicMock()
        mock_src_client.get_dashboards = AsyncMock(return_value=(True, dashboards))
        mock_src_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta"))
        mock_tgt_client = MagicMock()
        mock_tgt_client.import_dashboard = AsyncMock()

        mock_engine = MagicMock()
        mock_engine.transform_zip.return_value = True

        mock_db_session = MagicMock()
        mock_sync = MagicMock()
        mock_sync.sync_environment = MagicMock(side_effect=RuntimeError("Sync failed"))
        from src.core.mapping_service import IdMappingService
        original_sync = IdMappingService.sync_environment

        with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.migration.SupersetClient') as MockSC, \
             patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
             patch('src.plugins.migration.create_temp_file') as mock_ctf, \
             patch('src.plugins.migration.IdMappingService'), \
             patch('src.plugins.migration.get_async_job_runner'), \
             patch('src.plugins.migration.SessionLocal', return_value=mock_db_session):

            MockSC.side_effect = [mock_src_client, mock_tgt_client]
            mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")

            # Should not raise — sync failure is caught and logged
            result = await plugin.execute({
                "source_env_id": "env-1",
                "target_env_id": "env-2",
                "selected_ids": [1],
            })

            assert result["status"] == "SUCCESS"

# #endregion Test.MigrationPlugin
