# [DEF:backend.tests.core.test_migration_engine:Module]
#
# @TIER:      STANDARD
# @PURPOSE:   Unit tests for MigrationEngine's cross-filter patching algorithms.
# @LAYER:     Domain
# @RELATION:  VERIFIES -> backend.src.core.migration_engine
#
import pytest
import tempfile
import json
import yaml
import zipfile
import sys
import os
from pathlib import Path
from unittest.mock import MagicMock

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

from src.core.migration_engine import MigrationEngine
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType


# --- Fixtures ---

class MockMappingService:
    """Mock that simulates IdMappingService.get_remote_ids_batch."""
    def __init__(self, mappings: dict):
        self.mappings = mappings

    def get_remote_ids_batch(self, env_id, resource_type, uuids):
        result = {}
        for uuid in uuids:
            if uuid in self.mappings:
                result[uuid] = self.mappings[uuid]
        return result


def _write_dashboard_yaml(dir_path: Path, metadata: dict) -> Path:
    """Helper: writes a dashboard YAML file with json_metadata."""
    file_path = dir_path / "dash.yaml"
    with open(file_path, 'w') as f:
        yaml.dump({"json_metadata": json.dumps(metadata)}, f)
    return file_path


# --- _patch_dashboard_metadata tests ---

def test_patch_dashboard_metadata_replaces_chart_ids():
    """Verifies that chartId values are replaced using the mapping service."""
    mock_service = MockMappingService({"uuid-chart-A": 999})
    engine = MigrationEngine(mock_service)

    metadata = {
        "native_filter_configuration": [
            {"targets": [{"chartId": 42}]}
        ]
    }

    with tempfile.TemporaryDirectory() as td:
        fp = _write_dashboard_yaml(Path(td), metadata)
        source_map = {42: "uuid-chart-A"}

        engine._patch_dashboard_metadata(fp, "target-env", source_map)

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            result = json.loads(data["json_metadata"])
            assert result["native_filter_configuration"][0]["targets"][0]["chartId"] == 999


def test_patch_dashboard_metadata_replaces_dataset_ids():
    """Verifies that datasetId values are replaced using the mapping service."""
    mock_service = MockMappingService({"uuid-ds-B": 500})
    engine = MigrationEngine(mock_service)

    metadata = {
        "native_filter_configuration": [
            {"targets": [{"datasetId": 10}]}
        ]
    }

    with tempfile.TemporaryDirectory() as td:
        fp = _write_dashboard_yaml(Path(td), metadata)
        source_map = {10: "uuid-ds-B"}

        engine._patch_dashboard_metadata(fp, "target-env", source_map)

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            result = json.loads(data["json_metadata"])
            assert result["native_filter_configuration"][0]["targets"][0]["datasetId"] == 500


def test_patch_dashboard_metadata_skips_when_no_metadata():
    """Verifies early return when json_metadata key is absent."""
    mock_service = MockMappingService({})
    engine = MigrationEngine(mock_service)

    with tempfile.TemporaryDirectory() as td:
        fp = Path(td) / "dash.yaml"
        with open(fp, 'w') as f:
            yaml.dump({"title": "No metadata here"}, f)

        engine._patch_dashboard_metadata(fp, "target-env", {})

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            assert "json_metadata" not in data


def test_patch_dashboard_metadata_handles_missing_targets():
    """When some source IDs have no target mapping, patches what it can and leaves the rest."""
    mock_service = MockMappingService({"uuid-A": 100})  # Only uuid-A maps
    engine = MigrationEngine(mock_service)

    metadata = {
        "native_filter_configuration": [
            {"targets": [{"datasetId": 1}, {"datasetId": 2}]}
        ]
    }

    with tempfile.TemporaryDirectory() as td:
        fp = _write_dashboard_yaml(Path(td), metadata)
        source_map = {1: "uuid-A", 2: "uuid-MISSING"}  # uuid-MISSING won't resolve

        engine._patch_dashboard_metadata(fp, "target-env", source_map)

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            result = json.loads(data["json_metadata"])
            targets = result["native_filter_configuration"][0]["targets"]
            # ID 1 should be replaced to 100; ID 2 should remain 2
            assert targets[0]["datasetId"] == 100
            assert targets[1]["datasetId"] == 2


# --- _extract_chart_uuids_from_archive tests ---

def test_extract_chart_uuids_from_archive():
    """Verifies that chart YAML files are parsed for id->uuid mappings."""
    engine = MigrationEngine()

    with tempfile.TemporaryDirectory() as td:
        charts_dir = Path(td) / "charts"
        charts_dir.mkdir()

        chart1 = {"id": 42, "uuid": "uuid-42", "slice_name": "Chart One"}
        chart2 = {"id": 99, "uuid": "uuid-99", "slice_name": "Chart Two"}

        with open(charts_dir / "chart1.yaml", 'w') as f:
            yaml.dump(chart1, f)
        with open(charts_dir / "chart2.yaml", 'w') as f:
            yaml.dump(chart2, f)

        result = engine._extract_chart_uuids_from_archive(Path(td))

        assert result == {42: "uuid-42", 99: "uuid-99"}


# --- _transform_yaml tests ---

def test_transform_yaml_replaces_database_uuid():
    """Verifies that database_uuid in a dataset YAML is replaced."""
    engine = MigrationEngine()

    with tempfile.TemporaryDirectory() as td:
        fp = Path(td) / "dataset.yaml"
        with open(fp, 'w') as f:
            yaml.dump({"database_uuid": "source-uuid-abc", "table_name": "my_table"}, f)

        engine._transform_yaml(fp, {"source-uuid-abc": "target-uuid-xyz"})

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            assert data["database_uuid"] == "target-uuid-xyz"
            assert data["table_name"] == "my_table"


def test_transform_yaml_ignores_unmapped_uuid():
    """Verifies no changes when UUID is not in the mapping."""
    engine = MigrationEngine()

    with tempfile.TemporaryDirectory() as td:
        fp = Path(td) / "dataset.yaml"
        original = {"database_uuid": "unknown-uuid", "table_name": "test"}
        with open(fp, 'w') as f:
            yaml.dump(original, f)

        engine._transform_yaml(fp, {"other-uuid": "replacement"})

        with open(fp, 'r') as f:
            data = yaml.safe_load(f)
            assert data["database_uuid"] == "unknown-uuid"


# --- [NEW] transform_zip E2E tests ---

def test_transform_zip_end_to_end():
    """Verifies full orchestration: extraction, transformation, patching, and re-packaging."""
    mock_service = MockMappingService({"char-uuid": 101, "ds-uuid": 202})
    engine = MigrationEngine(mock_service)

    with tempfile.TemporaryDirectory() as td:
        td_path = Path(td)
        zip_path = td_path / "source.zip"
        output_path = td_path / "target.zip"

        # Create source ZIP structure
        with tempfile.TemporaryDirectory() as src_dir:
            src_path = Path(src_dir)
            
            # 1. Dataset
            ds_dir = src_path / "datasets"
            ds_dir.mkdir()
            with open(ds_dir / "ds.yaml", 'w') as f:
                yaml.dump({"database_uuid": "source-db-uuid", "table_name": "users"}, f)
            
            # 2. Chart
            ch_dir = src_path / "charts"
            ch_dir.mkdir()
            with open(ch_dir / "ch.yaml", 'w') as f:
                yaml.dump({"id": 10, "uuid": "char-uuid"}, f)
            
            # 3. Dashboard
            db_dir = src_path / "dashboards"
            db_dir.mkdir()
            metadata = {"native_filter_configuration": [{"targets": [{"chartId": 10}]}]}
            with open(db_dir / "db.yaml", 'w') as f:
                yaml.dump({"json_metadata": json.dumps(metadata)}, f)

            with zipfile.ZipFile(zip_path, 'w') as zf:
                for root, _, files in os.walk(src_path):
                    for file in files:
                        p = Path(root) / file
                        zf.write(p, p.relative_to(src_path))

        db_mapping = {"source-db-uuid": "target-db-uuid"}
        
        # Execute transform
        success = engine.transform_zip(
            str(zip_path), 
            str(output_path), 
            db_mapping, 
            target_env_id="test-target", 
            fix_cross_filters=True
        )

        assert success is True
        assert output_path.exists()

        # Verify contents
        with tempfile.TemporaryDirectory() as out_dir:
            with zipfile.ZipFile(output_path, 'r') as zf:
                zf.extractall(out_dir)
            
            out_path = Path(out_dir)
            
            # Verify dataset transformation
            with open(out_path / "datasets" / "ds.yaml", 'r') as f:
                ds_data = yaml.safe_load(f)
                assert ds_data["database_uuid"] == "target-db-uuid"
            
            # Verify dashboard patching
            with open(out_path / "dashboards" / "db.yaml", 'r') as f:
                db_data = yaml.safe_load(f)
                meta = json.loads(db_data["json_metadata"])
                assert meta["native_filter_configuration"][0]["targets"][0]["chartId"] == 101


def test_transform_zip_invalid_path():
    """@PRE: Verify behavior (False) on invalid ZIP path."""
    engine = MigrationEngine()
    success = engine.transform_zip("non_existent.zip", "output.zip", {})
    assert success is False


def test_transform_yaml_nonexistent_file():
    """@PRE: Verify behavior on non-existent YAML file."""
    engine = MigrationEngine()
    # Should log error and not crash (implemented via try-except if wrapped, 
    # but _transform_yaml itself might raise FileNotFoundError if not guarded)
    with pytest.raises(FileNotFoundError):
        engine._transform_yaml(Path("non_existent.yaml"), {})


# [/DEF:backend.tests.core.test_migration_engine:Module]
