from __future__ import annotations

from datetime import datetime as real_datetime

from recoil.pipeline._lib import breakdown_coverage_validator as coverage
from recoil.pipeline._lib.breakdown_coverage_validator import (
    CoverageFinding,
    mention_key,
    verify_coverage,
    write_coverage_report,
)
from recoil.pipeline._lib.prose_validator import Severity


def test_r1_character_trips_bad_fixture_and_passes_good_fixture():
    bad = _ledger([_mention("character", character_id="ghost")])
    good = _ledger([_mention("character", character_id="jade")])

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r1"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r2_location_trips_bad_fixture_and_alias_passes_good_fixture():
    bad = _ledger([_mention("location", location_id="missing")])
    good = _ledger([_mention("location", location_id="service corridor")])

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r2"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r3_sublocation_trips_bad_fixture_and_alias_location_passes_good_fixture():
    bad = _ledger(
        [_mention("sublocation", location_id="lower_decks", sublocation="missing_node")]
    )
    good = _ledger(
        [_mention("sublocation", location_id="service corridor", sublocation="junction_node")]
    )

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r3"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r4_prop_trips_bad_fixture_and_passes_good_fixture():
    bad = _ledger([_mention("prop", prop_id="missing_pod")])
    good = _ledger([_mention("prop", prop_id="cryo_pod")])

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r4"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r5_prop_state_trips_bad_fixture_and_passes_good_fixture():
    bad = _ledger(
        [_mention("prop_state", scene_id="EP001_SC003", prop_id="cryo_pod", state_id="opened")]
    )
    good = _ledger(
        [_mention("prop_state", scene_id="EP001_SC002", prop_id="cryo_pod", state_id="cracked")]
    )

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r5"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r5_reachability_obeys_episode_and_scene_ordering():
    bible = _bible()
    bible["props"]["cryo_pod"]["transitions"].append(
        {
            "from": "opened",
            "to": "venting",
            "reversible": False,
            "trigger_scene": "EP002_SC001",
        }
    )
    bible["props"]["cryo_pod"]["states"]["venting"] = {
        "description": "Venting.",
        "visual_delta": "White vapor.",
    }

    early = _ledger(
        [_mention("prop_state", scene_id="EP001_SC999", prop_id="cryo_pod", state_id="venting")]
    )
    late = _ledger(
        [_mention("prop_state", scene_id="EP002_SC001", prop_id="cryo_pod", state_id="venting")]
    )

    assert _checks(verify_coverage(early, bible)) == {"coverage_r5"}
    assert not _blocks(verify_coverage(late, bible))


def test_r6_wardrobe_change_trips_bad_fixture_and_passes_good_fixture():
    bible = _bible(with_wardrobe=True)
    bad = _ledger(
        [
            _mention(
                "wardrobe_change",
                scene_id="EP001_SC001",
                character_id="jade",
                piece="jacket",
                change="removed",
            )
        ]
    )
    good = _ledger(
        [
            _mention(
                "wardrobe_change",
                scene_id="EP001_SC002",
                character_id="jade",
                piece="jacket",
                change="removed",
            )
        ]
    )

    assert "coverage_r6" in _checks(verify_coverage(bad, bible))
    assert not _blocks(verify_coverage(good, bible))


def test_r6_allows_prior_trigger_for_wardrobe_change():
    bible = _bible(with_wardrobe=True)
    later_mention = _ledger(
        [
            _mention(
                "wardrobe_change",
                scene_id="EP001_SC003",
                character_id="jade",
                piece="jacket",
                change="removed",
            )
        ]
    )

    assert "coverage_r6" not in _checks(verify_coverage(later_mention, bible))


def test_r7_transient_state_warns_bad_fixture_and_passes_good_fixture():
    bad = _ledger([_mention("transient_state", character_id="jade", state_desc="soaked")])
    good = _ledger([_mention("transient_state", character_id="jade", state_desc="injured")])

    results = verify_coverage(bad, _bible())
    assert _checks(results, Severity.WARN) == {"coverage_r7"}
    assert not verify_coverage(good, _bible())


def test_r8_identity_contradiction_trips_bad_fixture_and_passes_good_fixture():
    bad = _ledger(
        [
            _mention(
                "identity_observation",
                character_id="jade",
                attribute="eye_color",
                observed_value="blue",
            )
        ]
    )
    good = _ledger(
        [
            _mention(
                "identity_observation",
                character_id="jade",
                attribute="eye_color",
                observed_value="brown",
            )
        ]
    )

    assert _checks(verify_coverage(bad, _bible())) == {"coverage_r8"}
    assert not _blocks(verify_coverage(good, _bible()))


def test_r9_phantom_trigger_and_unhandled_change_both_block():
    phantom = verify_coverage(_ledger([]), _bible(with_wardrobe=True))
    unhandled = verify_coverage(
        _ledger(
            [
                _mention(
                    "wardrobe_change",
                    scene_id="EP001_SC002",
                    character_id="jade",
                    piece="jacket",
                    change="removed",
                )
            ]
        ),
        _bible(),
    )

    assert "coverage_r9" in _checks(phantom)
    assert "coverage_r9" in _checks(unhandled)


def test_tombstone_suppresses_block_and_emits_info():
    mention = _mention("character", character_id="ghost")
    tombstone = {
        "mention_key": mention_key(mention),
        "scene_id": "*",
        "reason": "Known dream apparition.",
        "approved_by": "JT",
    }

    results = verify_coverage(_ledger([mention]), _bible(), tombstones=[tombstone])

    assert not _blocks(results)
    assert len(results) == 1
    assert results[0].severity is Severity.INFO
    assert results[0].check == "coverage_tombstoned"
    assert "coverage_r1 suppressed" in results[0].message


def test_degraded_grace_no_states_warns_no_invariants_is_silent():
    no_states_bible = _bible()
    no_states_bible["props"]["legacy_prop"] = {"description": "Legacy prose only."}
    state_results = verify_coverage(
        _ledger([_mention("prop_state", prop_id="legacy_prop", state_id="glowing")]),
        no_states_bible,
    )

    no_invariants_bible = _bible()
    no_invariants_bible["characters"]["legacy"] = {"phases": []}
    invariant_results = verify_coverage(
        _ledger(
            [
                _mention(
                    "identity_observation",
                    character_id="legacy",
                    attribute="eye_color",
                    observed_value="silver",
                )
            ]
        ),
        no_invariants_bible,
    )

    assert _checks(state_results, Severity.WARN) == {"coverage_r5"}
    assert not invariant_results


def test_mention_key_uses_fixed_kind_field_order():
    assert (
        mention_key(_mention("prop_state", prop_id="cryo_pod", state_id="cracked"))
        == "prop_state|cryo_pod|cracked"
    )
    assert (
        mention_key(
            _mention(
                "sublocation",
                location_id="int_lower_decks_corridor",
                sublocation="junction_node",
            )
        )
        == "sublocation|int_lower_decks_corridor|junction_node"
    )


def test_write_coverage_report_is_frozen_and_never_overwrites(monkeypatch, tmp_path):
    class FrozenDatetime:
        @classmethod
        def now(cls):
            return real_datetime(2026, 6, 11, 1, 2, 3, 456)

    monkeypatch.setattr(coverage, "datetime", FrozenDatetime)
    finding = CoverageFinding(
        Severity.BLOCK,
        "coverage_r9",
        "phantom wardrobe trigger",
        _mention(
            "wardrobe_change",
            character_id="jade",
            piece="jacket",
            change="removed",
        ),
        "EP001_SC002",
        "Jade removes the jacket.",
    )
    ledger = _ledger([])

    first = write_coverage_report([finding], ledger, tmp_path)
    second = write_coverage_report([finding], ledger, tmp_path)

    assert first != second
    assert first.name == "coverage_report_20260611-010203-000456.md"
    assert second.name == "coverage_report_20260611-010203-000456-2.md"
    report_text = first.read_text(encoding="utf-8")
    assert "coverage_r9" in report_text
    assert "BLOCK: 1" in report_text


def _bible(*, with_wardrobe: bool = False) -> dict:
    bible = {
        "characters": {
            "jade": {
                "identity_invariants": {
                    "hair_color": "black",
                    "eye_color": "brown",
                    "skin_tone": "warm brown",
                    "build": "lean",
                    "distinguishing": ["amber debt counter"],
                },
                "transients": ["injured"],
                "phases": [],
            }
        },
        "locations": {
            "lower_decks": {
                "aliases": ["service corridor"],
                "sublocations": {
                    "junction_node": {"description": "A corridor junction."},
                },
            }
        },
        "props": {
            "cryo_pod": {
                "states": {
                    "sealed": {"description": "Sealed.", "visual_delta": "Frosted glass."},
                    "cracked": {"description": "Cracked.", "visual_delta": "A split viewport."},
                    "opened": {"description": "Opened.", "visual_delta": "Hatch lifted."},
                },
                "initial_state": "sealed",
                "transitions": [
                    {
                        "from": "sealed",
                        "to": "cracked",
                        "reversible": False,
                        "trigger_scene": "EP001_SC002",
                    },
                    {
                        "from": "cracked",
                        "to": "opened",
                        "reversible": False,
                        "trigger_scene": "EP001_SC004",
                    },
                ],
                "carriable": False,
            }
        },
    }
    if with_wardrobe:
        bible["characters"]["jade"]["phases"] = [
            {
                "trigger": {
                    "type": "script_event",
                    "scene_ref": "EP001_SC002",
                    "description": "Jade removes the jacket.",
                    "evidence_hash": "a" * 64,
                },
                "appearance": {
                    "wardrobe": [
                        {
                            "piece": "jacket",
                            "descriptor": "oil-dark utility jacket",
                            "state": "removed",
                            "salient": True,
                        }
                    ],
                    "hair_state": "loose",
                    "visible_gear": [],
                    "notable_marks": [],
                },
            }
        ]
    return bible


def _ledger(mentions: list[dict]) -> dict:
    return {
        "schema_version": 1,
        "project": "tartarus",
        "episode": 1,
        "scenes": [
            {
                "scene_id": "EP001_SC002",
                "scene_hash": "h",
                "slugline": "INT. LOWER DECKS - NIGHT",
                "mentions": mentions,
                "carried_forward": False,
            }
        ],
    }


def _mention(kind: str, scene_id: str = "EP001_SC002", **kwargs) -> dict:
    mention = {
        "kind": kind,
        "surface_text": kind,
        "scene_id": scene_id,
        "scene_hash": "h",
        "span_quote": f"{kind} evidence.",
    }
    mention.update(kwargs)
    return mention


def _blocks(results: list[CoverageFinding]) -> list[CoverageFinding]:
    return [result for result in results if result.severity is Severity.BLOCK]


def _checks(
    results: list[CoverageFinding],
    severity: Severity = Severity.BLOCK,
) -> set[str]:
    return {result.check for result in results if result.severity is severity}


def test_r9_carry_forward_wardrobe_does_not_phantom_block():
    """appearance.wardrobe is full appearance STATE: a phase listing three
    carry-forward 'worn' items plus one real change must emit exactly ONE
    trigger-level event — matched by one ledger mention — not four."""
    from recoil.pipeline._lib.breakdown_coverage_validator import verify_coverage
    from recoil.pipeline._lib.prose_validator import Severity

    bible = {
        "characters": {
            "JADE": {
                "phases": [{
                    "phase_id": "jade_phase_2",
                    "trigger": {"type": "script_event", "scene_ref": "EP001_SC002",
                                "description": "ties jacket around waist"},
                    "appearance": {"wardrobe": [
                        {"piece": "tanktop", "state": "worn", "salient": True},
                        {"piece": "cargo pants", "state": "worn", "salient": False},
                        {"piece": "work boots", "state": "worn", "salient": False},
                        {"piece": "canvas jacket", "state": "removed", "salient": True},
                    ]},
                }],
            }
        },
        "locations": {}, "props": {},
    }
    ledger = {"scenes": [{"scene_id": "EP001_SC002", "scene_hash": "h", "mentions": [{
        "kind": "wardrobe_change", "character_id": "JADE", "piece": "canvas jacket",
        "change": "removed", "scene_id": "EP001_SC002", "scene_hash": "h",
        "surface_text": "jacket off", "span_quote": "ties the jacket around her waist",
    }]}]}
    results = verify_coverage(ledger, bible)
    r9_blocks = [r for r in results if r.check == "coverage_r9" and r.severity == Severity.BLOCK]
    assert r9_blocks == []
