"""Contract tests for proof_types.py — ensures TypedDicts match authoritative sources.

These tests prevent proof_types.py from drifting out of sync with the code
that produces and consumes proof artifacts. Constants are imported from
the actual source files (not duplicated) so drift in either direction
causes a test failure.
"""

import importlib.util
import json
import typing
from pathlib import Path

from scripts.proof_types import (
    SearchRegistryEntry, CitationEntry, CoiFlag, CrossCheck,
    LoadedProof, ProofData, ProofDataV3, Generator, NormalizedVerdict,
)


def _import_validate_site_proof():
    """Import tools/validate-site-proof.py despite the hyphenated filename."""
    spec = importlib.util.spec_from_file_location(
        "validate_site_proof",
        Path(__file__).parent.parent / "tools" / "validate-site-proof.py",
    )
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


_vsp = _import_validate_site_proof()
REQUIRED_JSON_KEYS = _vsp.REQUIRED_JSON_KEYS
REQUIRED_GENERATOR_KEYS = _vsp.REQUIRED_GENERATOR_KEYS
SEARCH_REGISTRY_AUTHORED_KEYS = _vsp.SEARCH_REGISTRY_AUTHORED_KEYS


def test_search_registry_entry_covers_authored_keys():
    """SearchRegistryEntry must include all SEARCH_REGISTRY_AUTHORED_KEYS."""
    sr_fields = set(typing.get_type_hints(SearchRegistryEntry).keys())
    expected = set(SEARCH_REGISTRY_AUTHORED_KEYS)
    missing = expected - sr_fields
    assert not missing, f"SearchRegistryEntry missing fields: {missing}"


def test_proof_data_covers_required_json_keys():
    """ProofData must include all REQUIRED_JSON_KEYS."""
    pd_fields = set(typing.get_type_hints(ProofData).keys())
    expected = set(REQUIRED_JSON_KEYS)
    missing = expected - pd_fields
    assert not missing, f"ProofData missing required keys: {missing}"


def test_generator_covers_required_keys():
    """Generator must include all REQUIRED_GENERATOR_KEYS."""
    gen_fields = set(typing.get_type_hints(Generator).keys())
    expected = set(REQUIRED_GENERATOR_KEYS)
    missing = expected - gen_fields
    assert not missing, f"Generator missing required keys: {missing}"


def test_citation_entry_matches_build_citation_detail():
    """CitationEntry fields must match what build_citation_detail() produces.

    Authoritative source: verify_citations.py build_citation_detail() lines 610-619.
    """
    ce_fields = set(typing.get_type_hints(CitationEntry).keys())
    expected = {"source_key", "source_name", "url", "quote", "status",
                "method", "coverage_pct", "fetch_mode", "credibility"}
    missing = expected - ce_fields
    assert not missing, f"CitationEntry missing fields: {missing}"
    # CitationEntry should NOT have runtime-only fields
    assert "fetch_error" not in ce_fields, "fetch_error is runtime-only, not persisted"
    assert "message" not in ce_fields, "message is runtime-only, not persisted"


def test_loaded_proof_matches_loader_return():
    """LoadedProof fields must match what load_proof() actually returns.

    Authoritative source: tools/lib/proof_loader.py load_proof() lines 111-123.
    """
    lp_fields = set(typing.get_type_hints(LoadedProof).keys())
    expected = {"slug", "proof_data", "sections_md", "sections_audit",
                "verdict", "tags", "featured", "citation_count",
                "search_count", "date", "proof_engine_version",
                "verdict_summary", "source_names", "source_names_extra",
                "sections_narrative", "verdict_declaration", "verdict_hook",
                "format_version"}
    missing = expected - lp_fields
    assert not missing, f"LoadedProof missing fields: {missing}"


def test_types_match_published_proofs():
    """Spot-check: ProofData fields cover all keys in published proof.json files."""
    pd_fields = set(typing.get_type_hints(ProofData).keys())
    pd_v3_fields = set(typing.get_type_hints(ProofDataV3).keys())
    all_fields = pd_fields | pd_v3_fields
    proofs_dir = Path(__file__).parent.parent / "site" / "proofs"
    if not proofs_dir.exists():
        return  # Skip if no published proofs
    for proof_dir in sorted(proofs_dir.iterdir()):
        proof_json = proof_dir / "proof.json"
        if proof_json.exists():
            data = json.loads(proof_json.read_text())
            for key in data.keys():
                assert key in all_fields, (
                    f"proof.json key '{key}' in {proof_dir.name} "
                    f"not in ProofData or ProofDataV3 TypedDict"
                )


def test_coi_flag_has_required_fields():
    """CoiFlag must have source_key, coi_type, relationship, direction, severity."""
    cf_fields = set(typing.get_type_hints(CoiFlag).keys())
    expected = {"source_key", "coi_type", "relationship", "direction", "severity"}
    missing = expected - cf_fields
    assert not missing, f"CoiFlag missing fields: {missing}"


def test_cross_check_has_coi_flags():
    """CrossCheck must include coi_flags field."""
    cc_fields = set(typing.get_type_hints(CrossCheck).keys())
    assert "coi_flags" in cc_fields, "CrossCheck missing coi_flags field"


def test_cross_check_has_source_counting_fields():
    """CrossCheck must include fields used by source-counting templates."""
    cc_fields = set(typing.get_type_hints(CrossCheck).keys())
    expected = {"n_sources_consulted", "n_sources_verified", "sources",
                "independence_note", "tolerance"}
    missing = expected - cc_fields
    assert not missing, f"CrossCheck missing source-counting fields: {missing}"


def test_cross_check_has_absence_fields():
    """CrossCheck must include fields used by absence templates."""
    cc_fields = set(typing.get_type_hints(CrossCheck).keys())
    expected = {"n_databases_searched", "n_null_verified", "n_reviewed", "databases"}
    missing = expected - cc_fields
    assert not missing, f"CrossCheck missing absence fields: {missing}"


def test_closest_passage_not_in_citation_entry():
    """closest_passage and closest_similarity must NOT leak into CitationEntry."""
    from scripts.proof_types import CitationEntry
    # CitationEntry is a TypedDict — its __annotations__ define allowed keys.
    assert "closest_passage" not in CitationEntry.__annotations__
    assert "closest_similarity" not in CitationEntry.__annotations__


def test_build_citation_detail_excludes_closest_passage():
    """build_citation_detail must not propagate closest_passage to output."""
    from scripts.verify_citations import build_citation_detail

    result = {
        "status": "not_found",
        "fetch_mode": "live",
        "message": "Quote NOT found",
        "closest_passage": "some diagnostic text",
        "closest_similarity": 0.45,
    }
    detail = build_citation_detail(
        {"B1": {"type": "B", "key": "src_a"}},
        {"src_a": result},
        {"src_a": {"source_name": "Test", "url": "https://example.com", "quote": "test quote"}},
    )
    assert "closest_passage" not in detail["B1"]
    assert "closest_similarity" not in detail["B1"]


def test_proof_types_has_not_required():
    """NotRequired should be importable from proof_types regardless of Python version."""
    from scripts.proof_types import ProofData, CitationEntry, VerificationResult
    # These TypedDicts use NotRequired — if the import fallback is broken,
    # the module-level import fails and this test never reaches these asserts
    assert ProofData is not None
    assert CitationEntry is not None
    assert VerificationResult is not None


def test_structured_verdict_has_required_keys():
    """StructuredVerdict TypedDict defines value, qualified, qualifier, reason."""
    from scripts.proof_types import StructuredVerdict
    import typing
    hints = typing.get_type_hints(StructuredVerdict)
    assert "value" in hints
    assert "qualified" in hints
    assert "qualifier" in hints
    assert "reason" in hints


def test_evidence_entry_has_type_and_label():
    """EvidenceEntry TypedDict defines type and label."""
    from scripts.proof_types import EvidenceEntry
    import typing
    hints = typing.get_type_hints(EvidenceEntry)
    assert "type" in hints
    assert "label" in hints


def test_proof_data_v3_has_evidence_key():
    """ProofDataV3 TypedDict includes the evidence dict."""
    from scripts.proof_types import ProofDataV3
    import typing
    hints = typing.get_type_hints(ProofDataV3)
    assert "evidence" in hints
    assert "format_version" in hints


def test_proof_types_fallback_import(monkeypatch):
    """The typing_extensions fallback path must work when typing.NotRequired is absent.

    Simulates Python 3.10 by temporarily removing NotRequired from typing,
    then re-importing proof_types to exercise the except ImportError branch.

    On Python 3.11+, typing_extensions.NotRequired is the same object as
    typing.NotRequired, and typing.TypedDict's metaclass internally references
    NotRequired by name within its own module — so removing it from typing
    would crash the metaclass itself. We skip the simulation on 3.11+ and
    instead verify the fallback guard is present in the source."""
    import sys
    import importlib

    if sys.version_info >= (3, 11):
        # On 3.11+, the fallback is unreachable (typing has NotRequired).
        # Verify the try/except guard is present in the source instead.
        import inspect
        import scripts.proof_types as pt
        src = inspect.getsource(pt)
        assert "except ImportError" in src, (
            "proof_types.py must contain an 'except ImportError' fallback for NotRequired"
        )
        assert "typing_extensions" in src, (
            "proof_types.py must fall back to typing_extensions for NotRequired"
        )
        return

    # Python 3.10 simulation: remove NotRequired from typing and re-import.
    import typing
    original = getattr(typing, "NotRequired", None)
    if original is not None:
        monkeypatch.delattr(typing, "NotRequired")

    # Remove cached module so re-import hits the try/except
    mod_name = "scripts.proof_types"
    if mod_name in sys.modules:
        monkeypatch.delitem(sys.modules, mod_name)

    # Re-import — should succeed via typing_extensions fallback
    mod = importlib.import_module(mod_name)
    assert hasattr(mod, "ProofData")
    assert hasattr(mod, "CitationEntry")
