# #region TestDryRunOrchestrator [C:2] [TYPE Module]
#
# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.
# @LAYER Domain
# @RELATION  DEPENDS_ON -> [MigrationDryRunOrchestratorModule]
#
import json
from pathlib import Path
import sys
from unittest.mock import MagicMock, patch

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

backend_dir = str(Path(__file__).parent.parent.parent.parent.resolve())
if backend_dir not in sys.path:
    sys.path.insert(0, backend_dir)

from src.core.migration.dry_run_orchestrator import MigrationDryRunService
from src.models.dashboard import DashboardSelection
from src.models.mapping import Base


# #region _load_fixture [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> [TestDryRunOrchestrator]
# @PURPOSE: Load canonical migration dry-run fixture payload used by deterministic orchestration assertions.
def _load_fixture() -> dict:
    fixture_path = (
        Path(__file__).parents[2] / "fixtures" / "migration_dry_run_fixture.json"
    )
    return json.loads(fixture_path.read_text())


# #endregion _load_fixture


# #region _make_session [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> [TestDryRunOrchestrator]
# @PURPOSE: Build isolated in-memory SQLAlchemy session for dry-run service tests.
def _make_session():
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    return Session()


# #endregion _make_session


# #region test_migration_dry_run_service_builds_diff_and_risk [C:2] [TYPE Function]
# @RELATION  BINDS_TO -> [TestDryRunOrchestrator]
# @PURPOSE: Verify dry-run orchestration returns stable diff summary and required risk codes.
# @TEST_SCENARIO: dry_run_builds_diff_and_risk -> Stable diff summary and required risk codes are returned.
# @TEST_EDGE: missing_field -> Missing target datasource remains visible in risk items.
# @TEST_EDGE: invalid_type -> Broken dataset reference remains visible in risk items.
# @TEST_EDGE: external_fail -> Engine transform stub failure would stop result production.
# @TEST_INVARIANT: dry_run_result_contract_matches_fixture -> VERIFIED_BY: [dry_run_builds_diff_and_risk]
def test_migration_dry_run_service_builds_diff_and_risk():
    # @TEST_CONTRACT: dry_run_result_contract -> {
    #   required_fields: {diff: object, summary: object, risk: object},
    #   invariants: ["risk.score >= 0", "summary.selected_dashboards == len(selection.selected_ids)"]
    # }
    # @TEST_FIXTURE: migration_dry_run_fixture -> backend/tests/fixtures/migration_dry_run_fixture.json
    # @TEST_EDGE: missing_target_datasource -> fixture.transformed_zip_objects.datasets[0].database_uuid
    # @TEST_EDGE: breaking_reference -> fixture.transformed_zip_objects.charts[0].dataset_uuid
    fixture = _load_fixture()
    db = _make_session()
    selection = DashboardSelection(
        selected_ids=[42],
        source_env_id="src",
        target_env_id="tgt",
        replace_db_config=False,
        fix_cross_filters=True,
    )

    source_client = MagicMock()
    source_client.get_dashboards_summary.return_value = fixture[
        "source_dashboard_summary"
    ]
    source_client.export_dashboard.return_value = (b"PK\x03\x04", "source.zip")

    target_client = MagicMock()
    target_client.get_dashboards.return_value = (
        len(fixture["target"]["dashboards"]),
        fixture["target"]["dashboards"],
    )
    target_client.get_datasets.return_value = (
        len(fixture["target"]["datasets"]),
        fixture["target"]["datasets"],
    )
    target_client.get_charts.return_value = (
        len(fixture["target"]["charts"]),
        fixture["target"]["charts"],
    )
    target_client.get_databases.return_value = (
        len(fixture["target"]["databases"]),
        fixture["target"]["databases"],
    )

    parser = MagicMock()
    parser.extract_objects_from_zip.return_value = fixture["transformed_zip_objects"]
    service = MigrationDryRunService(parser=parser)

    with patch("src.core.migration.dry_run_orchestrator.MigrationEngine") as EngineMock:
        engine = MagicMock()
        engine.transform_zip.return_value = True
        EngineMock.return_value = engine
        result = service.run(selection, source_client, target_client, db)

    if "summary" not in result:
        raise AssertionError("summary is missing in dry-run payload")
    if result["summary"]["selected_dashboards"] != 1:
        raise AssertionError("selected_dashboards summary mismatch")
    if result["summary"]["dashboards"]["update"] != 1:
        raise AssertionError("dashboard update count mismatch")
    if result["summary"]["charts"]["create"] != 1:
        raise AssertionError("chart create count mismatch")
    if result["summary"]["datasets"]["create"] != 1:
        raise AssertionError("dataset create count mismatch")

    risk_codes = {item["code"] for item in result["risk"]["items"]}
    if "missing_datasource" not in risk_codes:
        raise AssertionError("missing_datasource risk is not detected")
    if "breaking_reference" not in risk_codes:
        raise AssertionError("breaking_reference risk is not detected")


# #endregion test_migration_dry_run_service_builds_diff_and_risk
# #endregion TestDryRunOrchestrator
