import json

import pytest
from copy import deepcopy
from pathlib import Path

from recoil.pipeline._lib.render_schema import (
    validate_appearance,
    validate_identity_invariants,
    validate_phase_trigger,
    validate_prop_state_machine,
)


SHA = "a" * 64
TARTARUS_BIBLE = Path(
    "/Users/joeturnerlin/Dropbox/CLAUDE_DATA/recoil/projects/tartarus/_pipeline/state/visual/global_bible.json"
)
SHAFT_SUBLOCATIONS = {
    "shaft_lip",
    "pod_platform",
    "anchor_cables",
    "upper_catwalk",
    "the_drop",
    "ceiling_strut",
}


def test_validate_identity_invariants_accepts_valid_block_and_unknown_keys():
    char = {
        "char_id": "JADE",
        "real_name": "Jade",
        "_manual_override": True,
        "sheet_profile": "operator-edit",
        "identity_invariants": {
            "hair_color": "black",
            "eye_color": "brown",
            "skin_tone": "warm brown",
            "build": "lean wiry",
            "distinguishing": ["amber debt counter"],
        },
        "transients": ["soaked", "injured"],
    }

    assert validate_identity_invariants(char) == []


def test_validate_identity_invariants_rejects_malformed_present_block():
    errors = validate_identity_invariants(
        {
            "identity_invariants": {
                "hair_color": "black",
                "eye_color": "brown",
                "skin_tone": "warm brown",
                "build": 42,
                "distinguishing": "scarred",
            },
            "transients": ["injured", 10],
        }
    )

    assert "identity_invariants.build must be a string" in errors
    assert "identity_invariants.distinguishing must be a list of strings" in errors
    assert "transients must be a list of strings" in errors


def test_validate_phase_trigger_accepts_valid_trigger():
    phase = {
        "phase_id": "jade_ep001",
        "trigger": {
            "type": "script_event",
            "scene_ref": "EP002_SC014",
            "description": "Jade tears her jacket escaping the shaft.",
            "evidence_hash": SHA,
        },
    }

    assert validate_phase_trigger(phase) == []


def test_validate_phase_trigger_rejects_bad_enum_and_hash():
    errors = validate_phase_trigger(
        {
            "trigger": {
                "type": "director_note",
                "scene_ref": "EP002_SC014",
                "description": "Bad trigger",
                "evidence_hash": "not-a-sha",
            }
        }
    )

    assert "trigger.type must be one of: ep_range, script_event" in errors
    assert "trigger.evidence_hash must be a lowercase sha256 hex digest" in errors


def test_validate_appearance_accepts_valid_appearance():
    phase = {
        "appearance": {
            "wardrobe": [
                {
                    "piece": "jacket",
                    "descriptor": "oil-dark utility jacket",
                    "state": "damaged",
                    "salient": True,
                }
            ],
            "hair_state": "tucked",
            "visible_gear": ["rebreather"],
            "notable_marks": ["rust-orange cuticles"],
        }
    }

    assert validate_appearance(phase) == []


def test_validate_appearance_rejects_bad_wardrobe_state():
    errors = validate_appearance(
        {
            "appearance": {
                "wardrobe": [
                    {
                        "piece": "jacket",
                        "descriptor": "oil-dark utility jacket",
                        "state": "clean",
                        "salient": "yes",
                    }
                ],
                "hair_state": "braided",
                "visible_gear": ["rebreather"],
                "notable_marks": [],
            }
        }
    )

    assert "appearance.wardrobe[0].state must be one of: damaged, removed, torn, worn" in errors
    assert "appearance.wardrobe[0].salient must be a boolean" in errors
    assert "appearance.hair_state must be one of: covered, loose, tucked, tied" in errors


def test_validate_prop_state_machine_accepts_reachable_states():
    prop = {
        "states": {
            "sealed": {"description": "Frosted shut", "visual_delta": "viewport iced over"},
            "opened": {"description": "Hatch open", "visual_delta": "fog spills from seam"},
            "damaged": {"description": "Hull cracked", "visual_delta": "sparks at release panel"},
        },
        "initial_state": "sealed",
        "transitions": [
            {"from": "sealed", "to": "opened", "reversible": False, "trigger_scene": "EP001_SC002"},
            {"from": "opened", "to": "damaged", "reversible": False},
        ],
        "carriable": False,
        "state_notes": "Annotation only; not operative.",
    }

    assert validate_prop_state_machine(prop) == []


def test_validate_prop_state_machine_rejects_bad_endpoint_and_initial_state():
    errors = validate_prop_state_machine(
        {
            "states": {
                "sealed": {"description": "Frosted shut", "visual_delta": "viewport iced over"},
            },
            "initial_state": "missing",
            "transitions": [
                {"from": "sealed", "to": "opened", "reversible": False},
            ],
            "carriable": "no",
        }
    )

    assert "initial_state 'missing' is not declared in states" in errors
    assert "transitions[0].to 'opened' is not declared in states" in errors
    assert "carriable must be a boolean" in errors


def test_validate_prop_state_machine_catches_unreachable_states():
    errors = validate_prop_state_machine(
        {
            "states": {
                "sealed": {"description": "Frosted shut", "visual_delta": "viewport iced over"},
                "opened": {"description": "Hatch open", "visual_delta": "fog spills from seam"},
                "lost": {"description": "Dropped into abyss", "visual_delta": "no longer visible"},
            },
            "initial_state": "sealed",
            "transitions": [
                {"from": "sealed", "to": "opened", "reversible": False},
            ],
            "carriable": False,
        }
    )

    assert "states unreachable from initial_state 'sealed': lost" in errors


def test_legacy_bible_without_new_blocks_passes_validators():
    legacy_char = {"char_id": "WREN", "sheet_front": "kept", "phases": [{"phase_id": "base"}]}
    legacy_phase = {"phase_id": "base", "start_ep": 1, "end_ep": 1}
    legacy_prop = {"prop_id": "salvage_hook", "description": "Hook"}

    assert validate_identity_invariants(legacy_char) == []
    assert validate_phase_trigger(legacy_phase) == []
    assert validate_appearance(legacy_phase) == []
    assert validate_prop_state_machine(legacy_prop) == []


def test_merge_survival_preserves_new_nested_blocks():
    """HONEST LIMITATION MARKER (merge-gate r6): the live Stage-1 merge
    (ingest_pipeline.run_breakdown_pass(merge=True)) is LLM-mediated — it
    feeds the existing bible into the prompt and saves the model's output, so
    deterministic survival of the additive L9 blocks CANNOT be guaranteed or
    meaningfully simulated here (the prior version of this test simulated a
    deterministic section-replacement merge that the live path does not
    perform, which was false confidence).

    Operational guard until C5 owns staleness: re-running Stage-1 ingest on a
    bible carrying L9 blocks may drop them — re-apply via the Gate A flow
    (bible_proposal replay) or restore from git history of the data root.
    What we CAN assert deterministically: a bible carrying every L9 block
    round-trips the full GlobalBible schema, so carrying the blocks is never
    itself a schema violation.
    """
    from recoil.pipeline._lib.breakdown_propose import validate_l9_bible
    from recoil.pipeline._lib.render_schema import GlobalBible

    bible = {
        "project": "fixture",
        "total_episodes": 1,
        "characters": {
            "jade": {
                "char_id": "jade",
                "display_name": "Jade",
                "visual_description": "Lean salvager.",
                "identity_invariants": {
                    "hair_color": "red", "eye_color": "brown",
                    "skin_tone": "pale", "build": "wiry", "distinguishing": [],
                },
                "phases": [{
                    "phase_id": "p1", "start_ep": 1, "end_ep": 1,
                    "wardrobe_description": "Tanktop and cargo pants.",
                    "trigger": {"type": "script_event", "scene_ref": "EP001_SC001",
                                "description": "intro", "evidence_hash": "a" * 64},
                    "appearance": {"wardrobe": [{"piece": "tanktop", "descriptor": "grey",
                                                  "state": "worn", "salient": True}],
                                    "hair_state": "loose", "visible_gear": [],
                                    "notable_marks": []},
                }],
            }
        },
        "locations": {
            "lower_decks": {
                "location_id": "lower_decks",
                "description": "metal grating",
                "sublocations": {"junction_node": {"description": "a junction"}},
            }
        },
        "props": {
            "cryo_pod": {
                "prop_id": "cryo_pod",
                "description": "brushed steel pod",
                "states": {"sealed": {"description": "sealed", "visual_delta": "frost"},
                            "cracked": {"description": "cracked", "visual_delta": "amber"}},
                "initial_state": "sealed",
                "transitions": [{"from": "sealed", "to": "cracked", "reversible": False}],
                "carriable": False,
            }
        },
    }
    GlobalBible.model_validate(bible)
    assert validate_l9_bible(bible) == []


def test_sublocation_seed_insert_preserves_other_structure_in_fixture():
    before = {
        "locations": {
            "int_lower_decks_maintenance_shaft": {
                "location_id": "int_lower_decks_maintenance_shaft",
                "description": "bottomless abyss, anchor cables",
                "atmosphere": "Vertigo made physical.",
                "sublocations": None,
            }
        },
        "characters": {"JADE": {"char_id": "JADE"}},
    }
    after = deepcopy(before)
    after["locations"]["int_lower_decks_maintenance_shaft"]["sublocations"] = {
        key: {"description": f"{key} description"} for key in SHAFT_SUBLOCATIONS
    }

    stripped_before = deepcopy(before)
    stripped_after = deepcopy(after)
    stripped_before["locations"]["int_lower_decks_maintenance_shaft"].pop("sublocations")
    stripped_after["locations"]["int_lower_decks_maintenance_shaft"].pop("sublocations")

    assert stripped_after == stripped_before


def test_tartarus_shaft_sublocation_seed_is_shape_valid():
    # Live-data check is machine-local by nature (the seed is project DATA on
    # Dropbox, not repo content) — skip cleanly where the data root is absent
    # so the merge gate never depends on unstaged local state.
    if not TARTARUS_BIBLE.exists():
        pytest.skip("tartarus data root not present on this machine")
    bible = json.loads(TARTARUS_BIBLE.read_text())
    sublocations = bible["locations"]["int_lower_decks_maintenance_shaft"]["sublocations"]

    assert set(sublocations) == SHAFT_SUBLOCATIONS
    for sublocation in sublocations.values():
        assert set(sublocation) == {"description"}
        assert isinstance(sublocation["description"], str)
        assert sublocation["description"].strip()


