"""REC-213 C3 — board LOCATION ref = composite SHEET (sheet-for-locked), gated by the
SAME use_composite_sheets flag as video. Hermetic; no spend.

Covers: the _append_sublocation_ref seam (locked+flag+sheet / flag-off / no-sheet warn /
not-locked / corrupt / gate-order zero-behavior-change), the board→finish sidecar handoff,
and the full live build_and_dispatch_board payload path (spend-mocked)."""
from __future__ import annotations

import json
import os
import sys
from pathlib import Path
from types import SimpleNamespace

import pytest
from PIL import Image

_REPO_ROOT = Path(__file__).resolve().parents[4]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.core.paths import ProjectPaths  # noqa: E402
from recoil.core.ref_errors import SheetIntegrityError  # noqa: E402
from recoil.pipeline._lib import board_builder as bb  # noqa: E402
from recoil.pipeline._lib import dispatch_payload as dp  # noqa: E402

_LOC = "int_lower_decks_corridor"
_SUB = "corridor_main"


def _paths(tmp_path) -> ProjectPaths:
    p = ProjectPaths(project_root=tmp_path / "proj")
    p.project_root.mkdir(parents=True, exist_ok=True)
    return p


def _flag_on(monkeypatch):
    monkeypatch.setenv("RECOIL_USE_COMPOSITE_SHEETS", "1")


def _flag_off(monkeypatch):
    monkeypatch.delenv("RECOIL_USE_COMPOSITE_SHEETS", raising=False)
    monkeypatch.setitem(dp._project_config_cache, "proj", {})


def _real_png(p: Path, size=(512, 512)) -> None:
    p.parent.mkdir(parents=True, exist_ok=True)
    Image.frombytes("RGB", size, os.urandom(size[0] * size[1] * 3)).save(p, "PNG")


def _stage_valid_sheet(paths) -> Path:
    sp = paths.sheet_path("loc", _LOC)
    _real_png(sp)
    assert sp.stat().st_size >= 100_000
    return sp


def _stage_corrupt_sheet(paths) -> Path:
    sp = paths.sheet_path("loc", _LOC)
    sp.parent.mkdir(parents=True, exist_ok=True)
    sp.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)  # truncated clobber
    return sp


def _stage_registry_and_plate(paths) -> Path:
    """A location.json registry + individual sublocation plates (the fallback path).

    Registers BOTH `_SUB` (locked cases) and `other_area` (the mixed/not-locked cases)
    so `sublocation_ref` resolves for either. Returns the `_SUB` plate."""
    base = bb.location_base_dir(paths, _LOC)
    base.mkdir(parents=True, exist_ok=True)
    subs = {}
    for sub in (_SUB, "other_area"):
        plate = base / "sublocations" / f"{sub}_v01.png"
        _real_png(plate, size=(64, 64))  # >1KB, decodable -> validate_ref_file passes
        subs[sub] = {"ref": f"sublocations/{sub}_v01.png"}
    (base / "location.json").write_text(json.dumps({"sublocations": subs}), encoding="utf-8")
    return base / "sublocations" / f"{_SUB}_v01.png"


def _prim():
    return SimpleNamespace(location_id=_LOC)


def _segments(locked=True):
    return ([{"sublocation": _SUB}, {"sublocation": _SUB}] if locked
            else [{"sublocation": _SUB}, {"sublocation": "other_area"}])


def _call(paths, locked=True):
    refs: list[str] = []
    ref_layout: dict = {}
    sidecar = bb._append_sublocation_ref(paths, _prim(), _segments(locked), refs, ref_layout)
    return refs, ref_layout, sidecar


# ── 1: locked + flag on + valid sheet -> SHEET used ──
def test_locked_flag_on_valid_sheet_uses_sheet(tmp_path, monkeypatch):
    _flag_on(monkeypatch)
    paths = _paths(tmp_path)
    _stage_registry_and_plate(paths)
    sheet = _stage_valid_sheet(paths)
    refs, ref_layout, sidecar = _call(paths, locked=True)
    assert refs == [str(sheet)], refs
    assert ref_layout["sublocation_ref"] == 1
    assert ref_layout["sublocation_panels"] == [1, 2]
    assert sidecar["shared_ref"] == "sheets/sheet_v1.png"
    assert sidecar["sublocation_refs"][0]["ref"] == f"assets/loc/{_LOC}/sheets/sheet_v1.png"
    assert sidecar["shared_sublocation"] == _SUB


# ── 2: flag OFF -> never sheet, even locked + valid sheet present ──
def test_flag_off_never_uses_sheet(tmp_path, monkeypatch):
    _flag_off(monkeypatch)
    paths = _paths(tmp_path)
    plate = _stage_registry_and_plate(paths)
    _stage_valid_sheet(paths)
    refs, _ref_layout, _sidecar = _call(paths, locked=True)
    assert refs == [str(plate)], refs
    assert not any("sheet_v1.png" in r for r in refs)


# ── 3: flag on + locked + NO sheet -> fall back to plate + WARN ──
def test_locked_no_sheet_falls_back_with_warning(tmp_path, monkeypatch, caplog):
    _flag_on(monkeypatch)
    paths = _paths(tmp_path)
    plate = _stage_registry_and_plate(paths)  # no sheet staged
    import logging
    with caplog.at_level(logging.WARNING):
        refs, _ref_layout, _sidecar = _call(paths, locked=True)
    assert refs == [str(plate)]
    sheet_path = str(paths.sheet_path("loc", _LOC))
    assert any("composite sheet" in r.getMessage() and sheet_path in r.getMessage()
               for r in caplog.records), caplog.text


# ── 4: not-locked + flag on + valid sheet -> individual refs only, no warn ──
def test_not_locked_never_uses_sheet(tmp_path, monkeypatch, caplog):
    _flag_on(monkeypatch)
    paths = _paths(tmp_path)
    plate = _stage_registry_and_plate(paths)
    _stage_valid_sheet(paths)
    import logging
    with caplog.at_level(logging.WARNING):
        refs, _ref_layout, _sidecar = _call(paths, locked=False)
    assert any(str(plate) == r for r in refs)
    assert not any("sheet_v1.png" in r for r in refs)
    assert not any("composite sheet" in r.getMessage() for r in caplog.records)


# ── 5: flag on + locked + corrupt sheet -> SheetIntegrityError ──
def test_corrupt_sheet_raises(tmp_path, monkeypatch):
    _flag_on(monkeypatch)
    paths = _paths(tmp_path)
    _stage_registry_and_plate(paths)
    _stage_corrupt_sheet(paths)
    with pytest.raises(SheetIntegrityError):
        _call(paths, locked=True)


# ── 6: zero-behavior-change — corrupt sheet present but inactive path -> NO raise ──
def test_flag_off_corrupt_sheet_no_raise(tmp_path, monkeypatch):
    _flag_off(monkeypatch)
    paths = _paths(tmp_path)
    plate = _stage_registry_and_plate(paths)
    _stage_corrupt_sheet(paths)
    # sentinel: resolver must NOT be called when the flag is off
    monkeypatch.setattr(bb, "resolve_sheet_asset",
                        lambda *a, **k: pytest.fail("resolve_sheet_asset called when flag off"))
    refs, _ref_layout, _sidecar = _call(paths, locked=True)
    assert refs == [str(plate)]


def test_not_locked_corrupt_sheet_no_raise(tmp_path, monkeypatch):
    _flag_on(monkeypatch)
    paths = _paths(tmp_path)
    _stage_registry_and_plate(paths)
    _stage_corrupt_sheet(paths)
    monkeypatch.setattr(bb, "resolve_sheet_asset",
                        lambda *a, **k: pytest.fail("resolve_sheet_asset called when not locked"))
    refs, _ref_layout, _sidecar = _call(paths, locked=False)  # mixed sublocations
    assert not any("sheet_v1.png" in r for r in refs)


# ── 8: LIVE-PATH (hermetic, no-spend) — build_and_dispatch_board payload uses the sheet ──
def test_build_and_dispatch_board_uses_sheet(tmp_path, monkeypatch):
    from recoil.core.ref_types import RefAsset, ReferenceBundle
    from recoil.pipeline.core.registry import MODALITY_STORYBOARD, RunResult
    from recoil.pipeline.core.take import Beat, Scene
    from recoil.pipeline.core.persistence import scene_path, save_scene
    from recoil.pipeline.core.receipts import GenerationReceipt

    loc = "int_lab"
    sub = "pod_platform"
    _flag_on(monkeypatch)
    paths = ProjectPaths(project_root=tmp_path / "fixture_project")
    paths.project_root.mkdir(parents=True)
    monkeypatch.setattr(ProjectPaths, "for_project",
                        classmethod(lambda cls, project=None: paths))

    # a VALID composite sheet for the location (the C3 path)
    sheet = paths.sheet_path("loc", loc)
    _real_png(sheet)

    shots = [{"shot_id": "EP001_SH10", "scene_index": 1, "duration_s": 1.0,
              "intent": "Beat.", "asset_data": {"characters": [{"char_id": "JADE",
              "wardrobe_phase_id": "p1"}], "location_id": loc},
              "spatial_data": {"sublocation": sub}}]
    beat = Beat(beat_id="BATCH_004", beat_metadata={"scene_id": "BATCH_004",
                "modality": "r2v_multi", "shot": shots[0], "batch_shots": shots,
                "batch_summary": {"shared_characters": [], "shared_location_id": loc}}, board=None)
    scene = Scene(scene_id="BATCH_004", beats=[beat],
                  scene_metadata={"episode": "ep_001", "project": "fixture_project"})
    save_scene(scene, scene_path("fixture_project", "ep_001", "BATCH_004"))
    paths.global_bible_path.parent.mkdir(parents=True)
    paths.global_bible_path.write_text(
        '{"characters":{"JADE":{"visual_description":"Lean."}}}', encoding="utf-8")

    char_dir = paths.project_root / "assets" / "char" / "jade" / "base"
    char_dir.mkdir(parents=True)
    front = char_dir / "jade_front_base_v01.png"
    profile = char_dir / "jade_profile_base_v01.png"
    Image.new("RGB", (8, 8)).save(front)
    Image.new("RGB", (8, 8)).save(profile)

    monkeypatch.setattr(bb, "derive_settings",
                        lambda segments, **_k: [dict(seg, setting=f"S{i}")
                                                for i, seg in enumerate(segments, start=1)])
    monkeypatch.setattr(bb, "resolve_character_bundle",
                        lambda paths, char_id, phase=None: ReferenceBundle((
                            RefAsset(path=front, role="identity", subject=char_id, kind="turn", view="front"),
                            RefAsset(path=profile, role="identity", subject=char_id, kind="turn", view="profile"),
                        )))
    monkeypatch.setattr(bb, "validate_ref_file", lambda path: None)

    captured = {}

    def fake_dispatch(modality, payload, *, context):
        captured["payload"] = payload
        sd = Path(payload["save_dir"])
        sd.mkdir(parents=True, exist_ok=True)
        png = sd / f"{payload['filename_stem']}.png"
        Image.new("RGB", (20, 20)).save(png)
        Path(f"{png}.json").write_text(json.dumps(payload["sidecar_extra"]), encoding="utf-8")
        return GenerationReceipt(
            receipt_id="rcpt_test", modality=MODALITY_STORYBOARD, caller_id="board_builder",
            project="fixture_project", episode=1, shot_id="EP001_CONT_004",
            timestamp_utc="2026-06-11T00:00:00Z",
            run_result=RunResult(id="r", modality=MODALITY_STORYBOARD, output_path=str(png),
                                 metadata={}, success=True, error=None))

    monkeypatch.setattr(bb, "dispatch", fake_dispatch)

    result = bb.build_and_dispatch_board("fixture_project", 1, "EP001_CONT_004", step_runner=object())
    assert result["success"] is True, result
    payload = captured["payload"]
    refs = payload["reference_images"]
    assert any(Path(r).relative_to(paths.project_root) == Path(f"assets/loc/{loc}/sheets/sheet_v1.png")
               for r in refs), refs
    assert payload["sidecar_extra"]["sublocation_ref"] == "sheets/sheet_v1.png", payload["sidecar_extra"]


# ── 9: board→finish sidecar handoff carries the sheet ──
def test_finish_reuses_sheet_from_sidecar(tmp_path):
    paths = _paths(tmp_path)
    sheet = _stage_valid_sheet(paths)
    rel = f"assets/loc/{_LOC}/sheets/sheet_v1.png"
    sidecar = {"identity_refs": [], "sublocation": _SUB,
               "sublocation_refs": [{"slug": _SUB, "ref": rel}]}
    beat = SimpleNamespace(beat_metadata={})
    refs, ref_layout = bb._finish_refs_from_sidecar(paths, sidecar, beat, _prim())
    assert str(sheet) in refs
    assert ref_layout["sublocation_refs"][0]["slug"] == _SUB
    assert ref_layout["sublocation_refs"][0]["index"] >= 1
