"""Shared helpers for semantic validation fixture suites.

Provides fixture loading, lookup, and rule-map construction used by
Behave steps, Robot Framework helpers, and ASV benchmarks.  Centralises
the boilerplate so that adding a new rule or fixture file requires a
single update instead of three.
"""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from cleveragents.application.services.semantic_validation_rules import (
    APIMisuseRule,
    BrokenReferenceRule,
    DuplicateImportRule,
    MissingImportRule,
    MissingSymbolRule,
    SemanticCheckResult,
    SyntaxCheckRule,
)
from cleveragents.application.services.semantic_validation_service import (
    SemanticValidationRule,
)

# ---------------------------------------------------------------------------
# Fixture directory
# ---------------------------------------------------------------------------

FIXTURE_DIR: Path = Path(__file__).resolve().parent

# ---------------------------------------------------------------------------
# Canonical fixture filenames
# ---------------------------------------------------------------------------

PORTING_FILENAME = "language_porting_mismatches.json"
DEPENDENCY_FILENAME = "dependency_graph_violations.json"
API_FILENAME = "api_surface_changes.json"
CROSS_FILE_FILENAME = "cross_file_symbols.json"
CIRCULAR_FILENAME = "circular_import_detection.json"

ALL_FIXTURE_FILENAMES: list[str] = [
    PORTING_FILENAME,
    DEPENDENCY_FILENAME,
    API_FILENAME,
    CROSS_FILE_FILENAME,
    CIRCULAR_FILENAME,
]

# ---------------------------------------------------------------------------
# Rule map (Protocol-typed)
# ---------------------------------------------------------------------------

RULE_MAP: dict[str, SemanticValidationRule] = {
    "syntax_error": SyntaxCheckRule(),
    "missing_import": MissingImportRule(),
    "broken_reference": BrokenReferenceRule(),
    "duplicate_import": DuplicateImportRule(),
    "api_misuse": APIMisuseRule(),
    "missing_symbol": MissingSymbolRule(),
}

# ---------------------------------------------------------------------------
# Fixture loading & lookup
# ---------------------------------------------------------------------------


def load_fixtures(filename: str) -> list[dict[str, Any]]:
    """Load fixtures from a JSON file in the validation fixtures directory."""
    path = FIXTURE_DIR / filename
    with open(path, encoding="utf-8") as fh:
        data: dict[str, Any] = json.load(fh)
    result: list[dict[str, Any]] = data["fixtures"]
    return result


def find_fixture(fixtures: list[dict[str, Any]], name: str) -> dict[str, Any]:
    """Find a fixture by name in a fixture list.

    Raises ``ValueError`` if not found.
    """
    for f in fixtures:
        if f["name"] == name:
            return f
    msg = f"Fixture '{name}' not found"
    raise ValueError(msg)


# ---------------------------------------------------------------------------
# Fixture runner
# ---------------------------------------------------------------------------


def run_fixture(fixture: dict[str, Any]) -> SemanticCheckResult:
    """Run a single fixture through its expected rule and return the result."""
    rule_name: str = fixture["expected_rule"]
    rule = RULE_MAP[rule_name]
    source: str = fixture["source"]
    filename: str = fixture["filename"]
    return rule.check(source, filename)


def run_all_fixtures(fixtures: list[dict[str, Any]]) -> list[SemanticCheckResult]:
    """Run all fixtures through their expected rules."""
    return [run_fixture(f) for f in fixtures]


def verify_fixture_result(
    fixture: dict[str, Any],
    result: SemanticCheckResult,
) -> None:
    """Verify a fixture result against expectations.

    Checks ``passed``, optional ``expected_message_contains``, and
    optional ``expected_data_contains``.  Raises ``AssertionError``
    on mismatch.
    """
    expected_passed: bool = fixture["expected_passed"]
    assert result.passed is expected_passed, (
        f"Fixture '{fixture['name']}': expected passed={expected_passed}, "
        f"got passed={result.passed}, message='{result.message}'"
    )

    expected_msg: str | None = fixture.get("expected_message_contains")
    if expected_msg is not None:
        assert expected_msg in result.message, (
            f"Fixture '{fixture['name']}': expected message containing "
            f"'{expected_msg}', got '{result.message}'"
        )

    expected_data: dict[str, object] | None = fixture.get("expected_data_contains")
    if expected_data is not None:
        assert result.data is not None, (
            f"Fixture '{fixture['name']}': expected data containing "
            f"{expected_data}, but result.data is None"
        )
        for key, value in expected_data.items():
            assert key in result.data, (
                f"Fixture '{fixture['name']}': expected key '{key}' in "
                f"result.data, got keys {list(result.data.keys())}"
            )
            if isinstance(value, str):
                assert value in str(result.data[key]), (
                    f"Fixture '{fixture['name']}': expected data['{key}'] "
                    f"to contain '{value}', got '{result.data[key]}'"
                )


def validate_fixture_schema(fixture: dict[str, Any]) -> None:
    """Validate that a fixture dict has the required keys.

    Raises ``AssertionError`` if any required key is missing.
    """
    required_keys = {
        "name",
        "description",
        "source",
        "filename",
        "expected_rule",
        "expected_passed",
    }
    actual_keys = set(fixture.keys())
    missing = required_keys - actual_keys
    assert not missing, (
        f"Fixture '{fixture.get('name', '<unknown>')}' is missing "
        f"required keys: {missing}"
    )
    assert fixture["expected_rule"] in RULE_MAP, (
        f"Fixture '{fixture['name']}' references unknown rule "
        f"'{fixture['expected_rule']}'"
    )
