"""Tests for derivation_manifest (provenance/freshness records).

Hermetic: `ProjectPaths.for_project` is monkeypatched to a tmp-rooted ProjectPaths
(the breakdown_propose test pattern), so no real data root is touched.
"""
from __future__ import annotations

import asyncio
import hashlib
import json
import os
from types import SimpleNamespace

import pytest

from recoil.core.paths import ProjectPaths
from recoil.pipeline._lib import derivation_manifest, episode_script, plan_loader
from recoil.pipeline._lib.derivation_sha import (
    board_content_freshness_sha,
    content_sha,
    plan_structural_sha,
    shotset_hash,
)
from recoil.pipeline._lib.grouping import Group, GroupingIdentity
from recoil.pipeline.cli import generate
from recoil.pipeline.core.persistence import load_scene, save_scene, scene_path
from recoil.pipeline.core.take import Scene
from recoil.pipeline.orchestrator import episode_runner
from recoil.pipeline.orchestrator.episode_runner import EpisodeRunner
from recoil.pipeline.orchestrator.ingest_pipeline import IngestPipeline


@pytest.fixture
def project_paths(tmp_path, monkeypatch):
    """A tmp-rooted project; `for_project` resolves to it for any slug."""
    paths = ProjectPaths.from_root(tmp_path / "tartarus")
    monkeypatch.setattr(
        derivation_manifest.ProjectPaths,
        "for_project",
        classmethod(lambda cls, project: paths),
    )
    return paths


def _script_md5(text: str) -> str:
    return hashlib.md5(text.encode("utf-8")).hexdigest()


def _camera_tested_stage(project: str, episode: int, *, script_text: str | None = "old") -> str:
    content = "sha256:CT0"
    source = {} if script_text is None else {"script_sha": _script_md5(script_text)}
    derivation_manifest.stamp_stage(
        project,
        episode,
        "camera_tested",
        kind="derived",
        content_sha=content,
        source=source,
        builder="stage0.camera_test",
    )
    return content


def _stamp_bible(project: str, content: str = "sha256:BIBLE0") -> str:
    derivation_manifest.stamp_bible(
        project,
        content_sha=content,
        builder="test",
        built_at="2026-06-20T00:00:00+00:00",
    )
    return content


def _stamp_plan(
    project: str,
    episode: int,
    *,
    structural_sha: str,
    camera_content_sha: str = "sha256:CT0",
    bible_content_sha: str = "sha256:BIBLE0",
) -> None:
    derivation_manifest.stamp_stage(
        project,
        episode,
        "plan",
        kind="derived",
        structural_sha=structural_sha,
        content_sha="sha256:PLAN-CONTENT",
        source={
            "camera_tested_content_sha": camera_content_sha,
            "bible_content_sha": bible_content_sha,
        },
        builder="stage2.plan",
    )


def _raw_shot(shot_id: str, span_hash: str | None, *, scene_index: int = 1) -> dict:
    return {
        "shot_id": shot_id,
        "scene_index": scene_index,
        "source_text_hash": span_hash,
        "routing_data": {
            "is_env_only": False,
            "target_editorial_duration_s": 5,
            "has_dialogue": False,
        },
        "asset_data": {"location_id": "LOC_A", "characters": []},
        "prompt_data": {"shot_type": "MS"},
        "spatial_data": {},
    }


def _write_plan(paths: ProjectPaths, episode: int, raw: dict) -> plan_loader.CanonicalPlan:
    paths.plans_dir.mkdir(parents=True, exist_ok=True)
    path = paths.plans_dir / f"ep_{episode:03d}_plan.json"
    path.write_text(json.dumps(raw), encoding="utf-8")
    return plan_loader.load_plan(path)


def _plan_raw(spans: list[tuple[str, str | None]]) -> dict:
    return {
        "episode_id": "EP001",
        "project": "tartarus",
        "total_shots": len(spans),
        "shots": [
            _raw_shot(shot_id, span_hash, scene_index=i)
            for i, (shot_id, span_hash) in enumerate(spans, start=1)
        ],
    }


def _patch_script_sha(monkeypatch, text: str) -> None:
    monkeypatch.setattr(
        episode_script,
        "episode_script_sha",
        lambda project, episode: episode_script.episode_script_text_sha(text),
    )


def _fake_grouping():
    def assemble(shots, ctx):
        groups = []
        for index, shot in enumerate(shots, start=1):
            scene_id = f"SC_{index:03d}"
            groups.append(
                Group(
                    identity=GroupingIdentity(
                        strategy="continuity",
                        ordinal=index,
                        shot_ids=[shot.shot_id],
                    ),
                    shots=[shot],
                    scene_id=scene_id,
                    modality="video_i2v",
                )
            )
        return groups

    return SimpleNamespace(assemble=assemble)


def test_stamp_and_load_roundtrip(project_paths):
    derivation_manifest.stamp_stage(
        "tartarus", 1, "plan",
        kind="derived",
        structural_sha="sha256:STRUCT0",
        content_sha="sha256:CONTENT0",
        source={
            "camera_tested_content_sha": "sha256:CT0",
            "bible_content_sha": "sha256:B0",
        },
        builder="stage2.plan",
        model="storyboard-model",
        via="oauth",
        built_at="2026-06-14T00:00:00+00:00",
        extra={"shot_ids": ["EP001_SH01", "EP001_SH02"]},
    )

    manifest = derivation_manifest.load("tartarus", 1)
    plan = manifest["stages"]["plan"]
    assert plan["kind"] == "derived"
    assert plan["structural_sha"] == "sha256:STRUCT0"
    assert plan["content_sha"] == "sha256:CONTENT0"
    assert plan["source"]["camera_tested_content_sha"] == "sha256:CT0"
    assert plan["source"]["bible_content_sha"] == "sha256:B0"
    assert plan["builder"] == "stage2.plan"
    assert plan["shot_ids"] == ["EP001_SH01", "EP001_SH02"]

    # The file on disk is valid JSON.
    path = derivation_manifest.manifest_path("tartarus", 1)
    on_disk = json.loads(path.read_text(encoding="utf-8"))
    assert on_disk["stages"]["plan"]["content_sha"] == "sha256:CONTENT0"
    assert on_disk["episode"] == "ep_001"
    assert on_disk["schema_version"] == 1


def test_save_is_atomic(project_paths, monkeypatch):
    target = derivation_manifest.manifest_path("tartarus", 7)
    assert not target.exists()

    def _boom(*_a, **_k):
        raise RuntimeError("simulated os.replace failure")

    monkeypatch.setattr(os, "replace", _boom)

    with pytest.raises(RuntimeError):
        derivation_manifest.save("tartarus", 7, {"episode": "ep_007", "stages": {}})

    assert not target.exists()                  # no partial manifest at the target
    assert list(target.parent.iterdir()) == []  # tmp cleaned up — nothing left behind


def test_freshness_detects_stale(project_paths, monkeypatch):
    S0 = "sha256:PLAN_S0"
    S1 = "sha256:PLAN_S1"
    _patch_script_sha(monkeypatch, "old")
    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 3, script_text="old")

    derivation_manifest.stamp_stage(
        "tartarus", 3, "plan",
        kind="derived", structural_sha=S0, content_sha="sha256:PLANC0",
        source={
            "camera_tested_content_sha": camera_content,
            "bible_content_sha": bible_content,
        },
        builder="stage2.plan",
    )
    derivation_manifest.stamp_stage(
        "tartarus", 3, "coverage_passes",
        kind="derived", content_sha="sha256:PASSC0",
        source={"plan_structural_sha": S0}, builder="build_coverage_passes --lock",
    )
    assert derivation_manifest.freshness("tartarus", 3, "coverage_passes") == (True, None)

    # Re-stamp the plan with a NEW structural_sha → coverage_passes provenance is now stale.
    derivation_manifest.stamp_stage(
        "tartarus", 3, "plan",
        kind="derived", structural_sha=S1, content_sha="sha256:PLANC1",
        source={
            "camera_tested_content_sha": camera_content,
            "bible_content_sha": bible_content,
        },
        builder="stage2.plan",
    )
    assert derivation_manifest.freshness("tartarus", 3, "coverage_passes") == (False, "coverage_passes")


def test_recompute_health_flags_gap(project_paths):
    derivation_manifest.stamp_stage(
        "tartarus", 2, "scenes",
        kind="derived", content_sha="sha256:SCENES0",
        source={"plan_structural_sha": "sha256:PLAN0"},
        builder="scene_writer",
        extra={"missing_shots": ["SH14", "SH15"]},
    )

    health = derivation_manifest.recompute_health("tartarus", 2)
    assert health["complete_chain"] is False
    assert health["flags"]["scenes.missing_shots"] == ["SH14", "SH15"]

    # Persisted into the manifest.
    assert derivation_manifest.load("tartarus", 2)["health"] == health


def test_freshness_detects_camera_tested_script_drift(project_paths, monkeypatch):
    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 5, script_text="old")
    _stamp_plan(
        "tartarus",
        5,
        structural_sha="sha256:PLAN0",
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )

    _patch_script_sha(monkeypatch, "new")
    assert derivation_manifest.freshness("tartarus", 5, "plan") == (False, "camera_tested")

    _patch_script_sha(monkeypatch, "old")
    assert derivation_manifest.freshness("tartarus", 5, "plan") == (True, None)

    _camera_tested_stage("tartarus", 5, script_text=None)
    assert derivation_manifest.freshness("tartarus", 5, "plan") == (False, "camera_tested")

    _camera_tested_stage("tartarus", 5, script_text="old")

    def _missing_script(project, episode):
        raise FileNotFoundError("missing")

    monkeypatch.setattr(episode_script, "episode_script_sha", _missing_script)
    assert derivation_manifest.freshness("tartarus", 5, "plan") == (False, "camera_tested")


def test_freshness_scenes_detects_span_drift(project_paths, monkeypatch):
    _patch_script_sha(monkeypatch, "old")
    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 6, script_text="old")
    raw = _plan_raw([("SH01", "span-a"), ("SH02", "span-b")])
    _write_plan(project_paths, 6, raw)
    plan_sha = plan_structural_sha(raw)
    _stamp_plan(
        "tartarus",
        6,
        structural_sha=plan_sha,
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )
    spans = {"SC_001": {"SH01": "span-a"}, "SC_002": {"SH02": "span-b"}}
    derivation_manifest.stamp_stage(
        "tartarus",
        6,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES0",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={"shot_script_spans": spans, "scene_ids": ["SC_001", "SC_002"]},
    )
    assert derivation_manifest.freshness("tartarus", 6, "scenes") == (True, None)

    drifted = _plan_raw([("SH01", "span-a"), ("SH02", "span-drifted")])
    _write_plan(project_paths, 6, drifted)
    assert plan_structural_sha(drifted) == plan_sha
    assert derivation_manifest.freshness("tartarus", 6, "scenes") == (False, "scenes")

    _write_plan(project_paths, 6, raw)
    derivation_manifest.stamp_stage(
        "tartarus",
        6,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES0",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
    )
    assert derivation_manifest.freshness("tartarus", 6, "scenes") == (False, "scenes")

    for bad_spans, live_raw in [
        ({"SC_001": {"SH01": None}}, raw),
        ({"SC_001": {"SH01": "span-a"}}, _plan_raw([("SH01", None), ("SH02", "span-b")])),
        ({"SC_001": {"SH99": "span-z"}}, raw),
    ]:
        _write_plan(project_paths, 6, live_raw)
        derivation_manifest.stamp_stage(
            "tartarus",
            6,
            "scenes",
            kind="derived",
            content_sha="sha256:SCENES0",
            source={"plan_structural_sha": plan_sha},
            builder="episode_runner.scenes",
            extra={"shot_script_spans": bad_spans},
        )
        assert derivation_manifest.freshness("tartarus", 6, "scenes") == (False, "scenes")


def test_recompute_health_flags_script_and_scene_span_drift(project_paths, monkeypatch):
    monkeypatch.setattr(
        episode_script,
        "episode_script_sha",
        lambda project, episode: episode_script.episode_script_text_sha("new"),
    )
    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 7, script_text="old")
    live_raw = _plan_raw([("SH01", "live-span")])
    _write_plan(project_paths, 7, live_raw)
    plan_sha = plan_structural_sha(live_raw)
    _stamp_plan(
        "tartarus",
        7,
        structural_sha=plan_sha,
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )
    derivation_manifest.stamp_stage(
        "tartarus",
        7,
        "coverage_passes",
        kind="derived",
        content_sha="sha256:PASS0",
        source={"plan_structural_sha": plan_sha},
        builder="test",
    )
    derivation_manifest.stamp_stage(
        "tartarus",
        7,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES0",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={"shot_script_spans": {"SC_001": {"SH01": "old-span"}}},
    )

    health = derivation_manifest.recompute_health("tartarus", 7)
    assert health["flags"]["camera_tested.stale_vs_script"] is True
    assert health["flags"]["scenes.stale_vs_script_spans"] is True


def test_camera_tested_stamp_uses_text_sha(project_paths, monkeypatch):
    script = "# Episode 1\n\n**Word Count:** 100\n\n---\n\nINT. LAB - NIGHT\nJade waits."
    project_paths.episodes_dir.mkdir(parents=True, exist_ok=True)
    (project_paths.episodes_dir / "ep_001.md").write_text(script, encoding="utf-8")

    response = {
        "episode_id": "EP001",
        "project": "tartarus",
        "total_shots": 1,
        "shots": [
            {
                "shot_index": 1,
                "scene_index": 1,
                "source_text": "INT. LAB - NIGHT\nJade waits.",
                "has_dialogue": False,
                "characters_mentioned": ["Jade"],
                "location_hint": "INT. LAB - NIGHT",
            }
        ],
    }
    pipeline = IngestPipeline(
        project="tartarus",
        project_root=project_paths.project_root,
        min_shots=1,
        max_shots=1,
        extraction_model="gemini-test",
    )
    monkeypatch.setattr(pipeline, "_call_gemini", lambda **kwargs: json.dumps(response))

    pipeline._run_camera_test_single(1)

    stripped = episode_script.strip_metadata(script)
    manifest = derivation_manifest.load("tartarus", 1)
    assert (
        manifest["stages"]["camera_tested"]["source"]["script_sha"]
        == episode_script.episode_script_text_sha(stripped)
    )


def test_scenes_stage_stamped_with_backlink(project_paths, monkeypatch):
    monkeypatch.setattr(episode_runner, "init_scenes_from_plan", lambda *a, **k: [])
    monkeypatch.setattr(episode_runner, "_preflight_board_gate", lambda **kwargs: None)
    monkeypatch.setattr(episode_runner, "get_grouping", lambda name: _fake_grouping())
    _patch_script_sha(monkeypatch, "old")

    raw = _plan_raw([("SH01", "span-a"), ("SH02", "span-b")])
    canonical_plan = _write_plan(project_paths, 1, raw)
    runner = EpisodeRunner(project="tartarus", episode="ep_001", plan={"sequences": {}})

    result = asyncio.run(
        runner.run_episode_batches(
            canonical_plan,
            derive_only=True,
            dry_run=False,
            selected_coverage_passes=[],
        )
    )
    assert result["written"] == ["SC_001", "SC_002"]
    manifest = derivation_manifest.load("tartarus", 1)
    scenes_stage = manifest["stages"]["scenes"]
    assert scenes_stage["source"]["plan_structural_sha"] == plan_structural_sha(canonical_plan.raw)
    assert scenes_stage["shot_script_spans"] == {
        "SC_001": {"SH01": "span-a"},
        "SC_002": {"SH02": "span-b"},
    }
    persisted = {
        sid: load_scene(scene_path("tartarus", "ep_001", sid)).to_dict()
        for sid in result["written"]
    }
    assert scenes_stage["content_sha"] == content_sha(persisted)

    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 1, script_text="old")
    _stamp_plan(
        "tartarus",
        1,
        structural_sha=plan_structural_sha(canonical_plan.raw),
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )
    assert derivation_manifest.freshness("tartarus", 1, "scenes") == (True, None)
    changed = _plan_raw([("SH01", "span-a"), ("SH02", "span-b"), ("SH03", "span-c")])
    _write_plan(project_paths, 1, changed)
    _stamp_plan(
        "tartarus",
        1,
        structural_sha=plan_structural_sha(changed),
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )
    assert derivation_manifest.freshness("tartarus", 1, "scenes") == (False, "scenes")

    raw_non_derive = _plan_raw([("SH10", "span-x")])
    canonical_non_derive = _write_plan(project_paths, 8, raw_non_derive)
    runner_non_derive = EpisodeRunner(project="tartarus", episode="ep_008", plan={"sequences": {}})

    async def _mutating_run_scene(self, scene, **kwargs):
        scene.beats[0].beat_metadata["mutated_by_test"] = True

    monkeypatch.setattr(EpisodeRunner, "run_scene", _mutating_run_scene)
    scenes = asyncio.run(
        runner_non_derive.run_episode_batches(
            canonical_non_derive,
            derive_only=False,
            dry_run=False,
            selected_coverage_passes=[],
        )
    )
    assert [scene.scene_id for scene in scenes] == ["SC_001"]
    manifest = derivation_manifest.load("tartarus", 8)
    final_payload = {"SC_001": load_scene(scene_path("tartarus", "ep_008", "SC_001")).to_dict()}
    assert final_payload["SC_001"]["beats"][0]["beat_metadata"]["mutated_by_test"] is True
    assert manifest["stages"]["scenes"]["content_sha"] == content_sha(final_payload)

    locked_plan = _write_plan(project_paths, 9, _plan_raw([("SH20", "span-lock")]))
    locked_scene = Scene(scene_id="SC_001", locked=True, lock_reason="approved")
    save_scene(locked_scene, scene_path("tartarus", "ep_009", "SC_001"))
    locked_runner = EpisodeRunner(project="tartarus", episode="ep_009", plan={"sequences": {}})
    asyncio.run(
        locked_runner.run_episode_batches(
            locked_plan,
            derive_only=True,
            dry_run=False,
            selected_coverage_passes=[],
        )
    )
    assert "scenes" not in derivation_manifest.load("tartarus", 9)["stages"]

    full_raw = _plan_raw([("SH30", "span-1"), ("SH31", "span-2")])
    full_plan = _write_plan(project_paths, 10, full_raw)
    sliced_plan = plan_loader.CanonicalPlan(
        episode_id=full_plan.episode_id,
        project=full_plan.project,
        shots=full_plan.shots[:1],
        source_path=full_plan.source_path,
        raw=full_plan.raw,
    )
    sliced_runner = EpisodeRunner(project="tartarus", episode="ep_010", plan={"sequences": {}})
    asyncio.run(
        sliced_runner.run_episode_batches(
            sliced_plan,
            derive_only=True,
            dry_run=False,
            selected_coverage_passes=[],
        )
    )
    assert "scenes" not in derivation_manifest.load("tartarus", 10)["stages"]

    full_runner = EpisodeRunner(project="tartarus", episode="ep_011", plan={"sequences": {}})
    full_plan_ep11 = _write_plan(project_paths, 11, full_raw)
    asyncio.run(
        full_runner.run_episode_batches(
            full_plan_ep11,
            derive_only=True,
            dry_run=False,
            selected_coverage_passes=[],
        )
    )
    assert "scenes" in derivation_manifest.load("tartarus", 11)["stages"]


def test_stamp_flag_persists_through_recompute(project_paths):
    derivation_manifest.stamp_flag(
        "tartarus",
        4,
        "location.unresolved",
        {"error": "boom", "shot_index": 3, "hint": "INT. NOWHERE"},
    )

    manifest = derivation_manifest.load("tartarus", 4)
    assert manifest["health"]["flags"]["location.unresolved"]["shot_index"] == 3

    derivation_manifest.recompute_health("tartarus", 4)
    manifest = derivation_manifest.load("tartarus", 4)
    assert manifest["health"]["flags"]["location.unresolved"]["hint"] == "INT. NOWHERE"

    derivation_manifest.clear_flag("tartarus", 4, "location.unresolved")
    manifest = derivation_manifest.load("tartarus", 4)
    assert "location.unresolved" not in (manifest.get("health") or {}).get("flags") or {}


def test_stamp_board_get_board_roundtrip_and_absent(project_paths):
    record = {
        "status": "approved",
        "artifact": "artifacts/boards/board-1.png",
        "source_sha256": "sha256:BOARD0",
        "covered_shot_ids": ["EP001_SH01", "EP001_SH02"],
        "approved_by": "director",
    }

    derivation_manifest.stamp_board("tartarus", 1, "shotset:abc", record)

    assert derivation_manifest.get_board("tartarus", 1, "shotset:abc") == record
    assert derivation_manifest.get_board("tartarus", 1, "shotset:missing") is None


def test_board_freshness_detects_content_drift(project_paths, monkeypatch):
    _patch_script_sha(monkeypatch, "old")
    bible_content = _stamp_bible("tartarus")
    camera_content = _camera_tested_stage("tartarus", 12, script_text="old")
    raw = _plan_raw([("SH01", "span-a"), ("SH02", "span-b")])
    _write_plan(project_paths, 12, raw)
    plan_sha = plan_structural_sha(raw)
    _stamp_plan(
        "tartarus",
        12,
        structural_sha=plan_sha,
        camera_content_sha=camera_content,
        bible_content_sha=bible_content,
    )
    derivation_manifest.stamp_stage(
        "tartarus",
        12,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES0",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={
            "shot_script_spans": {
                "SC_001": {"SH01": "span-a", "SH02": "span-b"},
            },
        },
    )
    spans = {"SH01": "span-a", "SH02": "span-b"}
    record = {
        "status": "approved",
        "artifact": "artifacts/boards/board-1.png",
        "source_sha256": "intentionally-ignored-cache-identity",
        "covered_shot_ids": ["SH01", "SH02"],
        "shot_script_spans": spans,
        "content_freshness_sha": board_content_freshness_sha(spans),
    }
    derivation_manifest.stamp_board("tartarus", 12, "shotset:board", dict(record))

    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", True, None)
    ]
    assert (
        derivation_manifest.get_board("tartarus", 12, "shotset:board")["source_sha256"]
        == "intentionally-ignored-cache-identity"
    )

    drifted = _plan_raw([("SH01", "span-a"), ("SH02", "span-drifted")])
    _write_plan(project_paths, 12, drifted)
    derivation_manifest.stamp_stage(
        "tartarus",
        12,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES1",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={
            "shot_script_spans": {
                "SC_001": {"SH01": "span-a", "SH02": "span-drifted"},
            },
        },
    )
    assert plan_structural_sha(drifted) == plan_sha
    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", False, "board")
    ]

    _write_plan(project_paths, 12, raw)
    derivation_manifest.stamp_stage(
        "tartarus",
        12,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES2",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={
            "shot_script_spans": {
                "SC_001": {"SH01": "span-a", "SH02": "span-b"},
            },
        },
    )
    legacy = dict(record)
    legacy.pop("content_freshness_sha")
    derivation_manifest.stamp_board("tartarus", 12, "shotset:board", legacy)
    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", False, "board")
    ]

    derivation_manifest.stamp_board("tartarus", 12, "shotset:board", dict(record))
    _patch_script_sha(monkeypatch, "new")
    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", False, "camera_tested")
    ]
    _patch_script_sha(monkeypatch, "old")

    missing_shot_record = {
        **record,
        "covered_shot_ids": ["SH01", "SH99"],
        "shot_script_spans": {"SH01": "span-a", "SH99": None},
        "content_freshness_sha": board_content_freshness_sha(
            {"SH01": "span-a", "SH99": None}
        ),
    }
    derivation_manifest.stamp_board(
        "tartarus",
        12,
        "shotset:board",
        missing_shot_record,
    )
    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", False, "board")
    ]

    none_live_raw = _plan_raw([("SH01", "span-a"), ("SH02", None)])
    _write_plan(project_paths, 12, none_live_raw)
    derivation_manifest.stamp_stage(
        "tartarus",
        12,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES3",
        source={"plan_structural_sha": plan_sha},
        builder="episode_runner.scenes",
        extra={
            "shot_script_spans": {
                "SC_001": {"SH01": "span-a"},
            },
        },
    )
    none_record_spans = {"SH01": "span-a", "SH02": None}
    none_record = {
        **record,
        "covered_shot_ids": ["SH01", "SH02"],
        "shot_script_spans": none_record_spans,
        "content_freshness_sha": board_content_freshness_sha(none_record_spans),
    }
    derivation_manifest.stamp_board("tartarus", 12, "shotset:board", none_record)
    assert derivation_manifest.board_freshness("tartarus", 12) == [
        ("shotset:board", False, "board")
    ]


def test_stamp_board_manifest_kwarg_roundtrip_and_load_count(project_paths, monkeypatch):
    record = {
        "status": "approved",
        "artifact": "artifacts/boards/board-1.png",
        "source_sha256": "sha256:BOARD0",
    }
    loaded = derivation_manifest.load("tartarus", 1)
    real_load = derivation_manifest.load
    load_calls = []

    def load_spy(project, episode):
        load_calls.append((project, episode))
        return real_load(project, episode)

    monkeypatch.setattr(derivation_manifest, "load", load_spy)

    derivation_manifest.stamp_board(
        "tartarus",
        1,
        "shotset:with-manifest",
        record,
        manifest=loaded,
    )
    assert load_calls == []
    assert derivation_manifest.get_board("tartarus", 1, "shotset:with-manifest") == record
    assert load_calls == [("tartarus", 1)]

    load_calls.clear()
    derivation_manifest.stamp_board("tartarus", 1, "shotset:fresh-load", record)
    assert load_calls == [("tartarus", 1)]
    assert derivation_manifest.get_board("tartarus", 1, "shotset:fresh-load") == record
    assert load_calls == [("tartarus", 1), ("tartarus", 1)]


def test_stamp_board_ssot_loads_manifest_once(project_paths, monkeypatch):
    derivation_manifest.stamp_stage(
        "tartarus",
        1,
        "plan",
        kind="derived",
        structural_sha="sha256:PLAN0",
        content_sha="sha256:PLANC0",
        source={},
        builder="stage2.plan",
    )
    # REC-231 (41eaa812): _stamp_board_ssot now enforces the L2 invariant
    # stamped shotset_hash == shotset_hash(shot_ids). Use a consistent hash so the
    # fixture exercises the manifest-load-once behavior under the real contract.
    shotset = shotset_hash(["EP001_SH01", "EP001_SH02"])
    beat = type(
        "Beat",
        (),
        {
            "beat_id": "BATCH_001",
            "beat_metadata": {
                "grouping": {
                    "shot_ids": ["EP001_SH01", "EP001_SH02"],
                    "shotset_hash": shotset,
                }
            },
            "board": {
                "status": "approved",
                "artifact": "artifacts/boards/board-1.png",
                "source_sha256": "sha256:BOARD0",
                "approved_by": "director",
                "updated_at": "2026-06-14T00:00:00Z",
            },
        },
    )()
    real_load = derivation_manifest.load
    load_calls = []

    def load_spy(project, episode):
        load_calls.append((project, episode))
        return real_load(project, episode)

    monkeypatch.setattr(derivation_manifest, "load", load_spy)

    generate._stamp_board_ssot("tartarus", 1, beat)

    assert load_calls == [("tartarus", 1)]
    record = derivation_manifest.get_board("tartarus", 1, shotset)
    assert load_calls == [("tartarus", 1), ("tartarus", 1)]
    assert record == {
        "status": "approved",
        "artifact": "artifacts/boards/board-1.png",
        "photoreal_artifact": None,
        "source_sha256": "sha256:BOARD0",
        "fingerprint_version": 1,
        "model": None,
        "provider": None,
        "fallback_from": None,
        "plan_structural_sha_at_approval": "sha256:PLAN0",
        "covered_shot_ids": ["EP001_SH01", "EP001_SH02"],
        "shot_script_spans": {"EP001_SH01": None, "EP001_SH02": None},
        "content_freshness_sha": None,
        "approved_by": "director",
        "updated_at": "2026-06-14T00:00:00Z",
        # REC-231: the active version a board was approved against (None here — this
        # direct _stamp_board_ssot call threads no scene_version; live callers pass it).
        "scene_version": None,
    }


def test_stamp_board_does_not_clobber_other_shotsets(project_paths):
    first = {"status": "approved", "artifact": "board-a.png"}
    second = {"status": "rejected", "artifact": "board-b.png"}

    derivation_manifest.stamp_board("tartarus", 1, "shotset:a", first)
    derivation_manifest.stamp_board("tartarus", 1, "shotset:b", second)

    assert derivation_manifest.get_board("tartarus", 1, "shotset:a") == first
    assert derivation_manifest.get_board("tartarus", 1, "shotset:b") == second


def test_stamp_board_overwrites_same_shotset_hash(project_paths):
    original = {"status": "approved", "artifact": "board-v1.png"}
    updated = {
        "status": "approved",
        "artifact": "board-v1.png",
        "photoreal_artifact": "board-v2.png",
    }

    derivation_manifest.stamp_board("tartarus", 1, "shotset:abc", original)
    derivation_manifest.stamp_board("tartarus", 1, "shotset:abc", updated)

    assert derivation_manifest.get_board("tartarus", 1, "shotset:abc") == updated


def test_stamp_board_survives_recompute_health(project_paths):
    record = {"status": "approved", "artifact": "board.png"}
    derivation_manifest.stamp_board("tartarus", 1, "shotset:abc", record)

    derivation_manifest.recompute_health("tartarus", 1)

    assert derivation_manifest.get_board("tartarus", 1, "shotset:abc") == record
