# #region TestMigrationRoutes [TYPE Module] [C:3] [SEMANTICS test, migration, api, route, handler]
#
# @BRIEF Unit tests for migration API route handlers.
# @LAYER     API
# @RELATION BINDS_TO -> [MigrationApi]
#
from datetime import UTC, datetime
from pathlib import Path
import pytest
import sys
from unittest.mock import AsyncMock, MagicMock, patch

# Add backend directory to sys.path
backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
if backend_dir not in sys.path:
    sys.path.insert(0, backend_dir)
import os

# Force SQLite in-memory for all database connections BEFORE importing any application code
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["DATABASE_URL"] = "sqlite:///:memory:"
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["TASKS_DATABASE_URL"] = "sqlite:///:memory:"
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["AUTH_DATABASE_URL"] = "sqlite:///:memory:"
# @SIDE_EFFECT_WARNING: os.environ mutation at module import time — no teardown. This bleeds into all subsequently collected tests. Migrate to pytest.fixture(autouse=True) with monkeypatch.setenv.
os.environ["ENVIRONMENT"] = "testing"
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from src.models.mapping import Base, ResourceMapping, ResourceType

# Patch the get_db dependency if `src.api.routes.migration` imports it
patch("src.core.database.get_db").start()
# --- Fixtures ---
@pytest.fixture
def db_session():
    """In-memory SQLite session for testing."""
    from sqlalchemy.pool import StaticPool
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()
# #region _make_config_manager [TYPE Function]
# @RELATION BINDS_TO -> TestMigrationRoutes
def _make_config_manager(cron="0 2 * * *"):
    """Creates a mock config manager with a realistic AppConfig-like object."""
    settings = MagicMock()
    settings.migration_sync_cron = cron
    config = MagicMock()
    config.settings = settings
    cm = MagicMock()
    cm.get_config.return_value = config
    cm.save_config = MagicMock()
    return cm
# --- get_migration_settings tests ---
# #endregion _make_config_manager
@pytest.mark.asyncio
async def test_get_migration_settings_returns_default_cron():
    """Verify the settings endpoint returns the stored cron string."""
    from src.api.routes.migration import get_migration_settings
    cm = _make_config_manager(cron="0 3 * * *")
    # Call the handler directly, bypassing Depends
    result = await get_migration_settings(config_manager=cm, _=None)
    assert result == {"cron": "0 3 * * *"}
    cm.get_config.assert_called_once()
@pytest.mark.asyncio
async def test_get_migration_settings_returns_fallback_when_no_cron():
    """When migration_sync_cron uses the default, should return '0 2 * * *'."""
    from src.api.routes.migration import get_migration_settings
    # Use the default cron value (simulating a fresh config)
    cm = _make_config_manager()
    result = await get_migration_settings(config_manager=cm, _=None)
    assert result == {"cron": "0 2 * * *"}
# --- update_migration_settings tests ---
@pytest.mark.asyncio
async def test_update_migration_settings_saves_cron():
    """Verify that a valid cron update saves to config."""
    from src.api.routes.migration import update_migration_settings
    cm = _make_config_manager()
    result = await update_migration_settings(
        payload={"cron": "0 4 * * *"}, config_manager=cm, _=None
    )
    assert result["cron"] == "0 4 * * *"
    assert result["status"] == "updated"
    cm.save_config.assert_called_once()
@pytest.mark.asyncio
async def test_update_migration_settings_rejects_missing_cron():
    """Verify 400 error when 'cron' key is missing from payload."""
    from src.api.routes.migration import update_migration_settings
    cm = _make_config_manager()
    with pytest.raises(HTTPException) as exc_info:
        await update_migration_settings(
            payload={"interval": "daily"}, config_manager=cm, _=None
        )
    assert exc_info.value.status_code == 400
    assert "cron" in exc_info.value.detail.lower()
# --- get_resource_mappings tests ---
@pytest.mark.asyncio
async def test_get_resource_mappings_returns_formatted_list(db_session):
    """Verify mappings are returned as formatted dicts with correct keys."""
    from src.api.routes.migration import get_resource_mappings
    # Populate test data
    m1 = ResourceMapping(
        environment_id="prod",
        resource_type=ResourceType.CHART,
        uuid="uuid-1",
        remote_integer_id="42",
        resource_name="Sales Chart",
        last_synced_at=datetime(2026, 1, 15, 12, 0, 0, tzinfo=UTC),
    )
    db_session.add(m1)
    db_session.commit()
    result = await get_resource_mappings(
        skip=0,
        limit=50,
        search=None,
        env_id=None,
        resource_type=None,
        db=db_session,
        _=None,
    )
    assert result["total"] == 1
    assert len(result["items"]) == 1
    assert result["items"][0]["environment_id"] == "prod"
    assert result["items"][0]["resource_type"] == "chart"
    assert result["items"][0]["uuid"] == "uuid-1"
    assert result["items"][0]["remote_id"] == "42"
    assert result["items"][0]["resource_name"] == "Sales Chart"
    assert result["items"][0]["last_synced_at"] is not None
@pytest.mark.asyncio
async def test_get_resource_mappings_respects_pagination(db_session):
    """Verify skip and limit parameters work correctly."""
    from src.api.routes.migration import get_resource_mappings
    for i in range(5):
        db_session.add(
            ResourceMapping(
                environment_id="prod",
                resource_type=ResourceType.DATASET,
                uuid=f"uuid-{i}",
                remote_integer_id=str(i),
            )
        )
    db_session.commit()
    result = await get_resource_mappings(
        skip=2,
        limit=2,
        search=None,
        env_id=None,
        resource_type=None,
        db=db_session,
        _=None,
    )
    assert result["total"] == 5
    assert len(result["items"]) == 2
@pytest.mark.asyncio
async def test_get_resource_mappings_search_by_name(db_session):
    """Verify search filters by resource_name."""
    from src.api.routes.migration import get_resource_mappings
    db_session.add(
        ResourceMapping(
            environment_id="prod",
            resource_type=ResourceType.CHART,
            uuid="u1",
            remote_integer_id="1",
            resource_name="Sales Chart",
        )
    )
    db_session.add(
        ResourceMapping(
            environment_id="prod",
            resource_type=ResourceType.CHART,
            uuid="u2",
            remote_integer_id="2",
            resource_name="Revenue Dashboard",
        )
    )
    db_session.commit()
    result = await get_resource_mappings(
        skip=0,
        limit=50,
        search="sales",
        env_id=None,
        resource_type=None,
        db=db_session,
        _=None,
    )
    assert result["total"] == 1
    assert result["items"][0]["resource_name"] == "Sales Chart"
@pytest.mark.asyncio
async def test_get_resource_mappings_filter_by_env(db_session):
    """Verify env_id filter returns only matching environment."""
    from src.api.routes.migration import get_resource_mappings
    db_session.add(
        ResourceMapping(
            environment_id="ss1",
            resource_type=ResourceType.CHART,
            uuid="u1",
            remote_integer_id="1",
            resource_name="Chart A",
        )
    )
    db_session.add(
        ResourceMapping(
            environment_id="ss2",
            resource_type=ResourceType.CHART,
            uuid="u2",
            remote_integer_id="2",
            resource_name="Chart B",
        )
    )
    db_session.commit()
    result = await get_resource_mappings(
        skip=0,
        limit=50,
        search=None,
        env_id="ss2",
        resource_type=None,
        db=db_session,
        _=None,
    )
    assert result["total"] == 1
    assert result["items"][0]["environment_id"] == "ss2"
@pytest.mark.asyncio
async def test_get_resource_mappings_filter_by_type(db_session):
    """Verify resource_type filter returns only matching type."""
    from src.api.routes.migration import get_resource_mappings
    db_session.add(
        ResourceMapping(
            environment_id="prod",
            resource_type=ResourceType.CHART,
            uuid="u1",
            remote_integer_id="1",
            resource_name="My Chart",
        )
    )
    db_session.add(
        ResourceMapping(
            environment_id="prod",
            resource_type=ResourceType.DATASET,
            uuid="u2",
            remote_integer_id="2",
            resource_name="My Dataset",
        )
    )
    db_session.commit()
    result = await get_resource_mappings(
        skip=0,
        limit=50,
        search=None,
        env_id=None,
        resource_type="dataset",
        db=db_session,
        _=None,
    )
    assert result["total"] == 1
    assert result["items"][0]["resource_type"] == "dataset"
# --- trigger_sync_now tests ---
@pytest.fixture
# #region _mock_env [TYPE Function]
# @RELATION BINDS_TO -> TestMigrationRoutes
def _mock_env():
    """Creates a mock config environment object."""
    env = MagicMock()
    env.id = "test-env-1"
    env.name = "Test Env"
    env.url = "http://superset.test"
    env.username = "admin"
    env.password = "admin"
    env.verify_ssl = False
    env.timeout = 30
    return env
# #endregion _mock_env
# #region _make_sync_config_manager [TYPE Function]
# @RELATION BINDS_TO -> TestMigrationRoutes
def _make_sync_config_manager(environments):
    """Creates a mock config manager with environments list."""
    settings = MagicMock()
    settings.migration_sync_cron = "0 2 * * *"
    config = MagicMock()
    config.settings = settings
    config.environments = environments
    cm = MagicMock()
    cm.get_config.return_value = config
    cm.get_environments.return_value = environments
    return cm
# #endregion _make_sync_config_manager
@pytest.mark.asyncio
async def test_trigger_sync_now_creates_env_row_and_syncs(db_session, _mock_env):
    """Verify that trigger_sync_now creates an Environment row in DB before syncing,
    preventing FK constraint violations on resource_mappings inserts."""
    from src.api.routes.migration import trigger_sync_now
    from src.models.mapping import Environment as EnvironmentModel
    cm = _make_sync_config_manager([_mock_env])
    with (
        patch("src.api.routes.migration.SupersetClient") as MockClient,
        patch("src.api.routes.migration.IdMappingService") as MockService,
    ):
        mock_client_instance = MagicMock()
        MockClient.return_value = mock_client_instance
        mock_service_instance = MagicMock()
        MockService.return_value = mock_service_instance
        result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)
    # Environment row must exist in DB
    env_row = db_session.query(EnvironmentModel).filter_by(id="test-env-1").first()
    assert env_row is not None
    assert env_row.name == "Test Env"
    assert env_row.url == "http://superset.test"
    # Sync must have been called
    mock_service_instance.sync_environment.assert_called_once_with(
        "test-env-1", mock_client_instance
    )
    assert result["synced_count"] == 1
    assert result["failed_count"] == 0
@pytest.mark.asyncio
async def test_trigger_sync_now_rejects_empty_environments(db_session):
    """Verify 400 error when no environments are configured."""
    from src.api.routes.migration import trigger_sync_now
    cm = _make_sync_config_manager([])
    with pytest.raises(HTTPException) as exc_info:
        await trigger_sync_now(config_manager=cm, db=db_session, _=None)
    assert exc_info.value.status_code == 400
    assert "No environments" in exc_info.value.detail
@pytest.mark.asyncio
async def test_trigger_sync_now_handles_partial_failure(db_session, _mock_env):
    """Verify that if sync_environment raises for one env, it's captured in failed list."""
    from src.api.routes.migration import trigger_sync_now
    env2 = MagicMock()
    env2.id = "test-env-2"
    env2.name = "Failing Env"
    env2.url = "http://fail.test"
    env2.username = "admin"
    env2.password = "admin"
    env2.verify_ssl = False
    env2.timeout = 30
    cm = _make_sync_config_manager([_mock_env, env2])
    with (
        patch("src.api.routes.migration.SupersetClient") as MockClient,
        patch("src.api.routes.migration.IdMappingService") as MockService,
    ):
        mock_service_instance = MagicMock()
        mock_service_instance.sync_environment.side_effect = [
            None,
            RuntimeError("Connection refused"),
        ]
        MockService.return_value = mock_service_instance
        MockClient.return_value = MagicMock()
        result = await trigger_sync_now(config_manager=cm, db=db_session, _=None)
    assert result["synced_count"] == 1
    assert result["failed_count"] == 1
    assert result["details"]["failed"][0]["env_id"] == "test-env-2"
@pytest.mark.asyncio
async def test_trigger_sync_now_idempotent_env_upsert(db_session, _mock_env):
    """Verify that calling sync twice doesn't duplicate the Environment row."""
    from src.api.routes.migration import trigger_sync_now
    from src.models.mapping import Environment as EnvironmentModel
    cm = _make_sync_config_manager([_mock_env])
    with (
        patch("src.api.routes.migration.SupersetClient"),
        patch("src.api.routes.migration.IdMappingService"),
    ):
        await trigger_sync_now(config_manager=cm, db=db_session, _=None)
        await trigger_sync_now(config_manager=cm, db=db_session, _=None)
    env_count = db_session.query(EnvironmentModel).filter_by(id="test-env-1").count()
    assert env_count == 1
# --- get_dashboards tests ---
@pytest.mark.asyncio
async def test_get_dashboards_success(_mock_env):
    from src.api.routes.migration import get_dashboards
    cm = _make_sync_config_manager([_mock_env])
    with patch("src.api.routes.migration.SupersetClient") as MockClient:
        mock_client = MagicMock()
        mock_client.get_dashboards_summary.return_value = [{"id": 1, "title": "Test"}]
        MockClient.return_value = mock_client
        result = await get_dashboards(env_id="test-env-1", config_manager=cm, _=None)
        assert len(result) == 1
        assert result[0]["id"] == 1
@pytest.mark.asyncio
async def test_get_dashboards_invalid_env_raises_404(_mock_env):
    from src.api.routes.migration import get_dashboards
    cm = _make_sync_config_manager([_mock_env])
    with pytest.raises(HTTPException) as exc:
        await get_dashboards(env_id="wrong-env", config_manager=cm, _=None)
    assert exc.value.status_code == 404
# --- execute_migration tests ---
@pytest.mark.asyncio
async def test_execute_migration_success(_mock_env):
    from src.api.routes.migration import execute_migration
    from src.models.dashboard import DashboardSelection
    cm = _make_sync_config_manager([_mock_env, _mock_env])  # Need both source/target
    tm = MagicMock()
    tm.create_task = AsyncMock(return_value=MagicMock(id="task-123"))
    selection = DashboardSelection(
        source_env_id="test-env-1", target_env_id="test-env-1", selected_ids=[1, 2]
    )
    result = await execute_migration(
        selection=selection, config_manager=cm, task_manager=tm, _=None
    )
    assert result["task_id"] == "task-123"
    tm.create_task.assert_called_once()
@pytest.mark.asyncio
async def test_execute_migration_invalid_env_raises_400(_mock_env):
    from src.api.routes.migration import execute_migration
    from src.models.dashboard import DashboardSelection
    cm = _make_sync_config_manager([_mock_env])
    selection = DashboardSelection(
        source_env_id="test-env-1", target_env_id="non-existent", selected_ids=[1]
    )
    with pytest.raises(HTTPException) as exc:
        await execute_migration(
            selection=selection, config_manager=cm, task_manager=MagicMock(), _=None
        )
    assert exc.value.status_code == 400
@pytest.mark.asyncio
async def test_dry_run_migration_returns_diff_and_risk(db_session):
    # @TEST_EDGE  missing_target_datasource -> validates high risk item generation
    # @TEST_EDGE  breaking_reference -> validates high risk on missing dataset link
    from src.api.routes.migration import dry_run_migration
    from src.models.dashboard import DashboardSelection
    env_source = MagicMock()
    env_source.id = "src"
    env_source.name = "Source"
    env_source.url = "http://source"
    env_source.username = "admin"
    env_source.password = "admin"
    env_source.verify_ssl = False
    env_source.timeout = 30
    env_target = MagicMock()
    env_target.id = "tgt"
    env_target.name = "Target"
    env_target.url = "http://target"
    env_target.username = "admin"
    env_target.password = "admin"
    env_target.verify_ssl = False
    env_target.timeout = 30
    cm = _make_sync_config_manager([env_source, env_target])
    selection = DashboardSelection(
        selected_ids=[42],
        source_env_id="src",
        target_env_id="tgt",
        replace_db_config=False,
        fix_cross_filters=True,
    )
    with (
        patch("src.api.routes.migration.SupersetClient") as MockClient,
        patch("src.api.routes.migration.MigrationDryRunService") as MockService,
    ):
        source_client = MagicMock()
        target_client = MagicMock()
        MockClient.side_effect = [source_client, target_client]
        service_instance = MagicMock()
        service_payload = {
            "generated_at": "2026-02-27T00:00:00+00:00",
            "selection": selection.model_dump(),
            "selected_dashboard_titles": ["Sales"],
            "diff": {
                "dashboards": {
                    "create": [],
                    "update": [{"uuid": "dash-1"}],
                    "delete": [],
                },
                "charts": {"create": [{"uuid": "chart-1"}], "update": [], "delete": []},
                "datasets": {
                    "create": [{"uuid": "dataset-1"}],
                    "update": [],
                    "delete": [],
                },
            },
            "summary": {
                "dashboards": {"create": 0, "update": 1, "delete": 0},
                "charts": {"create": 1, "update": 0, "delete": 0},
                "datasets": {"create": 1, "update": 0, "delete": 0},
                "selected_dashboards": 1,
            },
            "risk": {
                "score": 75,
                "level": "high",
                "items": [
                    {"code": "missing_datasource"},
                    {"code": "breaking_reference"},
                ],
            },
        }
        service_instance.run.return_value = service_payload
        MockService.return_value = service_instance
        result = await dry_run_migration(
            selection=selection, config_manager=cm, db=db_session, _=None
        )
    assert result["summary"]["dashboards"]["update"] == 1
    assert result["summary"]["charts"]["create"] == 1
    assert result["summary"]["datasets"]["create"] == 1
    assert result["risk"]["score"] > 0
    assert any(item["code"] == "missing_datasource" for item in result["risk"]["items"])
    assert any(item["code"] == "breaking_reference" for item in result["risk"]["items"])
@pytest.mark.asyncio
async def test_dry_run_migration_rejects_same_environment(db_session):
    from src.api.routes.migration import dry_run_migration
    from src.models.dashboard import DashboardSelection
    env = MagicMock()
    env.id = "same"
    env.name = "Same"
    env.url = "http://same"
    env.username = "admin"
    env.password = "admin"
    env.verify_ssl = False
    env.timeout = 30
    cm = _make_sync_config_manager([env])
    selection = DashboardSelection(
        selected_ids=[1], source_env_id="same", target_env_id="same"
    )
    with pytest.raises(HTTPException) as exc:
        await dry_run_migration(
            selection=selection, config_manager=cm, db=db_session, _=None
        )
    assert exc.value.status_code == 400
# #endregion TestMigrationRoutes
