"""Tests for failure distiller functionality."""
from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from abi_evals.failure_distiller import FailureDistiller, FailureRecord, RegressionEvalCase
from abi_evals.regression_store import RegressionStore


@pytest.fixture
def sample_failures_file(tmp_path: Path) -> Path:
    """Create a sample failures.json file for testing."""
    failures = {
        "failures": [
            {
                "failure_id": "fail_001",
                "failure_class": "import_chain_break",
                "task_id": "task_123",
                "repo": "test-repo",
                "task_category": "refactoring",
                "evidence": {
                    "file": "src/module_a.py",
                    "error_message": "ImportError: cannot import name 'FooBar'",
                    "module": "src.module_b"
                }
            },
            {
                "failure_id": "fail_002",
                "failure_class": "schema_drift",
                "task_id": "task_124",
                "repo": "test-repo",
                "task_category": "api_change",
                "evidence": {
                    "file": "api/schemas/user.json",
                    "error_message": "Schema validation failed",
                    "expected_schema": {
                        "type": "object",
                        "required": ["id", "name"]
                    }
                }
            }
        ]
    }

    file_path = tmp_path / "failures.json"
    with open(file_path, "w", encoding="utf-8") as f:
        json.dump(failures, f)

    return file_path


@pytest.fixture
def distiller() -> FailureDistiller:
    """Create a FailureDistiller instance."""
    return FailureDistiller()


class TestFailureDistiller:
    """Tests for FailureDistiller class."""

    def test_distill_import_chain(self, distiller: FailureDistiller) -> None:
        """Test distilling import_chain_break failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_001",
            failure_class="import_chain_break",
            task_id="task_123",
            repo="test-repo",
            evidence={
                "file": "src/module_a.py",
                "module": "src.module_b",
                "error_message": "ImportError: cannot import"
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_001"
        assert eval_case.check_type == "static_analysis"
        assert "src/module_a.py" in eval_case.target_files or "src.module_b" in eval_case.target_files
        assert eval_case.config["check"] == "import_resolution"
        assert "import_chain" in eval_case.tags
        assert eval_case.expected_outcome == "pass"

    def test_distill_schema_drift(self, distiller: FailureDistiller) -> None:
        """Test distilling schema_drift failure into eval case."""
        schema = {"type": "object", "required": ["id"]}
        failure = FailureRecord(
            failure_id="fail_002",
            failure_class="schema_drift",
            task_id="task_124",
            evidence={
                "file": "api/user.json",
                "expected_schema": schema
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_002"
        assert eval_case.check_type == "schema_validation"
        assert "api/user.json" in eval_case.target_files
        assert eval_case.config["schema"] == schema
        assert "schema" in eval_case.tags

    def test_distill_hallucinated_path(self, distiller: FailureDistiller) -> None:
        """Test distilling hallucinated_path failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_003",
            failure_class="hallucinated_path",
            task_id="task_125",
            evidence={
                "files": ["src/nonexistent.py", "config/missing.json"]
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_003"
        assert eval_case.check_type == "file_existence"
        assert "src/nonexistent.py" in eval_case.target_files
        assert "config/missing.json" in eval_case.target_files
        assert eval_case.config["must_exist"] is True
        assert "path" in eval_case.tags

    def test_distill_stub_not_wired(self, distiller: FailureDistiller) -> None:
        """Test distilling stub_not_wired failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_004",
            failure_class="stub_not_wired",
            task_id="task_126",
            evidence={
                "file": "src/payment.py",
                "error_message": "NotImplementedError"
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_004"
        assert eval_case.check_type == "pattern_search"
        assert "src/payment.py" in eval_case.target_files
        assert "Not implemented" in eval_case.config["patterns"]
        assert "stub" in eval_case.tags

    def test_distill_config_naming_drift(self, distiller: FailureDistiller) -> None:
        """Test distilling config_naming_drift failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_005",
            failure_class="config_naming_drift",
            task_id="task_127",
            evidence={
                "file": "settings.cfg",
                "expected_pattern": r"^[a-z_]+\.config\.(json|yaml)$"
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_005"
        assert eval_case.check_type == "pattern_search"
        assert "config" in eval_case.tags

    def test_distill_event_payload_mismatch(self, distiller: FailureDistiller) -> None:
        """Test distilling event_payload_mismatch failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_006",
            failure_class="event_payload_mismatch",
            task_id="task_128",
            evidence={
                "files": ["emitter.py", "handler.py"],
                "pairs": [{"emitter": "emit_x", "handler": "handle_x"}]
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_006"
        assert eval_case.check_type == "static_analysis"
        assert "event" in eval_case.tags

    def test_distill_invariant_violation(self, distiller: FailureDistiller) -> None:
        """Test distilling invariant_violation failure into eval case."""
        failure = FailureRecord(
            failure_id="fail_007",
            failure_class="invariant_violation",
            task_id="task_129",
            evidence={
                "file": "balance.py",
                "invariant": "balance_non_negative"
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_007"
        assert eval_case.check_type == "static_analysis"
        assert eval_case.config["invariant"] == "balance_non_negative"
        assert "invariant" in eval_case.tags

    def test_distill_other(self, distiller: FailureDistiller) -> None:
        """Test distilling unclassified failure into generic eval case."""
        failure = FailureRecord(
            failure_id="fail_008",
            failure_class="other",
            task_id="task_130",
            evidence={
                "file": "mystery.py"
            }
        )

        eval_case = distiller.distill_failure(failure)

        assert eval_case.source_failure_id == "fail_008"
        assert eval_case.check_type == "file_existence"
        assert "generic" in eval_case.tags

    def test_distill_from_file(self, distiller: FailureDistiller, sample_failures_file: Path) -> None:
        """Test distilling failures from a JSON file."""
        eval_cases = distiller.distill(sample_failures_file)

        assert len(eval_cases) == 2
        assert eval_cases[0].source_failure_id == "fail_001"
        assert eval_cases[1].source_failure_id == "fail_002"

    def test_distill_nonexistent_file(self, distiller: FailureDistiller) -> None:
        """Test that distilling nonexistent file raises FileNotFoundError."""
        with pytest.raises(FileNotFoundError):
            distiller.distill("nonexistent.json")

    def test_eval_id_generation_deterministic(self, distiller: FailureDistiller) -> None:
        """Test that eval IDs are generated deterministically."""
        failure = FailureRecord(
            failure_id="fail_001",
            failure_class="import_chain_break",
            task_id="task_123",
            evidence={"file": "test.py"}
        )

        eval_case_1 = distiller.distill_failure(failure)
        eval_case_2 = distiller.distill_failure(failure)

        # Same input should produce same eval_id
        assert eval_case_1.eval_id == eval_case_2.eval_id

    def test_target_files_extraction(self, distiller: FailureDistiller) -> None:
        """Test extraction of target files from various evidence formats."""
        # Test with single file
        files = distiller._extract_target_files({"file": "test.py"})
        assert "test.py" in files

        # Test with files list
        files = distiller._extract_target_files({"files": ["a.py", "b.py"]})
        assert "a.py" in files and "b.py" in files

        # Test with path
        files = distiller._extract_target_files({"path": "src/main.py"})
        assert "src/main.py" in files

        # Test deduplication
        files = distiller._extract_target_files({
            "file": "test.py",
            "files": ["test.py", "other.py"]
        })
        # Should have only unique files
        assert files.count("test.py") == 1


class TestRegressionStore:
    """Tests for RegressionStore class."""

    def test_add_and_get_case(self, tmp_path: Path) -> None:
        """Test adding and retrieving an eval case."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        case = RegressionEvalCase(
            eval_id="test_001",
            name="test_case",
            source_failure_id="fail_001",
            check_type="static_analysis",
            target_files=["test.py"]
        )

        # Add case
        added = store.add(case)
        assert added is True

        # Retrieve case
        retrieved = store.get("test_001")
        assert retrieved is not None
        assert retrieved.eval_id == "test_001"
        assert retrieved.name == "test_case"

    def test_deduplicate(self, tmp_path: Path) -> None:
        """Test that duplicate cases are not added."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        case1 = RegressionEvalCase(
            eval_id="test_001",
            name="test_case",
            source_failure_id="fail_001",
            check_type="static_analysis",
            target_files=["test.py"]
        )

        case2 = RegressionEvalCase(
            eval_id="test_002",
            name="test_case_duplicate",
            source_failure_id="fail_001",  # Different ID but same signature
            check_type="static_analysis",
            target_files=["test.py"]  # Same target files
        )

        # Add first case
        added1 = store.add(case1)
        assert added1 is True

        # Try to add duplicate (different ID but same signature)
        added2 = store.add(case2)
        assert added2 is False  # Should be rejected as duplicate

        # Verify only one case exists
        assert len(store) == 1

    def test_add_many(self, tmp_path: Path) -> None:
        """Test adding multiple cases with deduplication."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        cases = [
            RegressionEvalCase(
                eval_id=f"test_{i:03d}",
                name=f"test_case_{i}",
                source_failure_id=f"fail_{i:03d}",
                check_type="static_analysis",
                target_files=[f"test_{i}.py"]
            )
            for i in range(5)
        ]

        result = store.add_many(cases)
        assert result["added"] == 5
        assert result["skipped"] == 0

    def test_index_persistence(self, tmp_path: Path) -> None:
        """Test that index is persisted to disk."""
        store_dir = tmp_path / "regression"
        store = RegressionStore(store_dir=store_dir)

        case = RegressionEvalCase(
            eval_id="test_001",
            name="test_case",
            source_failure_id="fail_001",
            check_type="static_analysis",
            target_files=["test.py"]
        )

        store.add(case)

        # Verify index file exists
        assert (store_dir / "index.json").exists()

        # Create new store instance and verify case is loaded
        store2 = RegressionStore(store_dir=store_dir)
        assert len(store2) == 1
        assert "test_001" in store2.index

    def test_list_cases(self, tmp_path: Path) -> None:
        """Test listing all cases."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        cases = [
            RegressionEvalCase(
                eval_id=f"test_{i:03d}",
                name=f"test_case_{i}",
                source_failure_id=f"fail_{i:03d}",
                check_type="static_analysis",
                target_files=[f"test_{i}.py"]
            )
            for i in range(3)
        ]

        store.add_many(cases)
        case_list = store.list_cases()

        assert len(case_list) == 3

    def test_get_by_check_type(self, tmp_path: Path) -> None:
        """Test filtering cases by check type."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        cases = [
            RegressionEvalCase(
                eval_id="test_001",
                name="test_case_1",
                source_failure_id="fail_001",
                check_type="static_analysis",
                target_files=["test.py"]
            ),
            RegressionEvalCase(
                eval_id="test_002",
                name="test_case_2",
                source_failure_id="fail_002",
                check_type="schema_validation",
                target_files=["schema.json"]
            )
        ]

        store.add_many(cases)

        static_cases = store.get_by_check_type("static_analysis")
        assert len(static_cases) == 1
        assert "test_001" in static_cases

    def test_remove_case(self, tmp_path: Path) -> None:
        """Test removing a case from the store."""
        store = RegressionStore(store_dir=tmp_path / "regression")

        case = RegressionEvalCase(
            eval_id="test_001",
            name="test_case",
            source_failure_id="fail_001",
            check_type="static_analysis",
            target_files=["test.py"]
        )

        store.add(case)
        assert len(store) == 1

        removed = store.remove("test_001")
        assert removed is True
        assert len(store) == 0


class TestCLIDistill:
    """Tests for CLI distill command."""

    def test_cli_distill(self, tmp_path: Path) -> None:
        """Test CLI distill command end-to-end."""
        # Use fixture file
        fixture_path = Path(__file__).parent / "fixtures" / "sample_failures.json"

        # If fixture doesn't exist, create it
        if not fixture_path.exists():
            fixture_path.parent.mkdir(parents=True, exist_ok=True)
            failures = {
                "failures": [
                    {
                        "failure_id": "fail_001",
                        "failure_class": "import_chain_break",
                        "task_id": "task_123",
                        "repo": "test-repo",
                        "evidence": {"file": "test.py"}
                    }
                ]
            }
            with open(fixture_path, "w", encoding="utf-8") as f:
                json.dump(failures, f)

        store_dir = tmp_path / "regression"

        # Create distiller and process
        distiller = FailureDistiller()
        store = RegressionStore(store_dir=store_dir)

        eval_cases = distiller.distill(fixture_path)
        result = store.add_many(eval_cases)

        # Verify results
        assert result["added"] > 0
        assert (store_dir / "index.json").exists()
