"""A2 Phase 3: a board-gated shot with NO board_ref_path BLOCKS; a finished board
flows into the payload; an approved RAW-only board (no finish) does NOT ship as the
r2v board ref; locked boards state the setting ONCE with no Caption dupes. No spend.

Staging mirrors A1's recoil/pipeline/_lib/tests collector tests (same projects_root /
_resolve_start_frame monkeypatching), but every cast HERO resolves so the only
remaining gate is the BOARD gate.
"""
from __future__ import annotations

import sys
from pathlib import Path

import pytest

_REPO_ROOT = Path(__file__).resolve().parents[3]   # recoil/pipeline/tests -> repo root
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from recoil.pipeline._lib.dispatch_payload import build_dispatch_payload  # noqa: E402
from recoil.pipeline._lib.plan_loader import CanonicalShot, CharacterEntry  # noqa: E402
from recoil.pipeline._lib.board_builder import preferred_board_artifact  # noqa: E402
from recoil.pipeline._lib.prompt_engine import build_storyboard_strip_prompt  # noqa: E402
from recoil.core.ref_errors import MissingBoardRefError  # noqa: E402
from PIL import Image  # noqa: E402


def _write_png(path: Path) -> None:
    """Write a REAL (PIL-valid) tiny PNG — a fake PNG header byte-blob false-fails
    if any collector/validator opens the file with PIL (codex review MAJOR 5)."""
    path.parent.mkdir(parents=True, exist_ok=True)
    Image.new("RGB", (8, 8)).save(path)


def _stage_jade_shot(tmp_path, monkeypatch):
    """JADE-only shot, JADE shelf hero present (cast gate passes); returns the shot."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.projects_root", lambda: tmp_path)
    monkeypatch.setattr("recoil.core.paths.projects_root", lambda: tmp_path)
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload._resolve_start_frame",
        lambda *, shot, project, override: Path("/dev/null"))
    from recoil.pipeline._lib import dispatch_payload as _dp
    monkeypatch.setitem(_dp._project_config_cache, "tartarus", {})

    look = tmp_path / "tartarus" / "assets" / "char" / "jade" / "base"
    look.mkdir(parents=True)
    _write_png(look / "jade-identity.png")

    raw = {
        "shot_id": "EP001_SH06", "scene_index": 1,
        "routing_data": {"target_editorial_duration_s": 3},
        "asset_data": {"location_id": "int_lab",
                       "characters": [{"char_id": "JADE", "wardrobe_phase_id": "p1"}]},
        "compiled_prompts": {"seeddance_t2v": "p"}, "prompt_data": {"shot_type": "MS"},
    }
    return CanonicalShot(
        shot_id="EP001_SH06", scene_index=1, sequence_id=None, pipeline="r2v",
        previs_model="gemini-3-pro-image-preview", video_model="seedream-v4.5",
        location_id="int_lab",
        characters=[CharacterEntry(char_id="JADE", wardrobe_phase_id="p1")],
        shot_type="MS", duration_s=3.0, is_env_only=False, has_dialogue=False,
        aspect_ratio=None, raw=raw)


def test_board_gated_no_board_blocks(tmp_path, monkeypatch):
    """board_gated=True + board_ref_path absent -> MissingBoardRefError, through the
    public build_dispatch_payload entry (dry_run audit path)."""
    cs = _stage_jade_shot(tmp_path, monkeypatch)
    with pytest.raises(MissingBoardRefError):
        build_dispatch_payload(
            shot=cs, project="tartarus", modality="r2v_multi", batch_shots=[cs],
            episode="ep_001", board_gated=True, dry_run=True)


def test_finished_board_flows_into_payload(tmp_path, monkeypatch):
    """board_gated=True + a finished board_ref_path -> payload's reference_images
    ENDS with the board path and ref_manifest carries 'board_1' (the live append
    at dispatch_payload.py:1214-1225)."""
    cs = _stage_jade_shot(tmp_path, monkeypatch)
    board = tmp_path / "tartarus" / "prep" / "ep_001" / "storyboards" / "B_v01_photoreal.png"
    _write_png(board)
    p = build_dispatch_payload(
        shot=cs, project="tartarus", modality="r2v_multi", batch_shots=[cs],
        episode="ep_001", board_gated=True, board_ref_path=str(board), dry_run=True)
    refs = p.get("reference_images", [])
    assert refs and refs[-1].endswith("B_v01_photoreal.png"), f"board not last ref: {refs}"
    assert p.get("ref_manifest", {}).get("board_1") == len(refs)


def test_approved_raw_only_board_does_not_ship_for_r2v():
    """An APPROVED board with NO photoreal (finish) artifact must NOT fall back to
    the raw pencil artifact for a board-gated r2v shot -- preferred_board_artifact
    returns None (-> board_ref_path None -> blocks). FAILS against current live
    preferred_board_artifact; PASSES only after the Phase-3 deliverable-3
    (board_builder.py preferred_board_artifact) edit."""
    approved_raw_only = {"status": "approved", "artifact": "prep/ep_001/storyboards/B_v01.png"}
    assert preferred_board_artifact(approved_raw_only) is None


def test_finish_preferred_over_raw_when_present(tmp_path):
    """When a finished (photoreal) artifact exists on disk, it is preferred over raw."""
    finish = tmp_path / "B_v01_photoreal.png"
    _write_png(finish)
    board = {"status": "approved", "artifact": "B_v01.png", "photoreal_artifact": finish.name}
    got = preferred_board_artifact(board, project_root=tmp_path)
    assert got == finish.name


def test_locked_board_states_setting_once_no_caption_dupes():
    """REC builder-dedup: a locked multi-panel board states the setting ONCE (not
    per-panel) and emits no duplicate Caption concatenation."""
    segs = [{"shot_id": f"p{i}", "setting": "INT. lower decks maintenance shaft",
             "intent": f"beat {i}"} for i in range(4)]
    prompt = build_storyboard_strip_prompt(
        segs, slots=4, char_descs={}, ref_layout={}, sublocation_locked=True,
        grid=(2, 2))
    assert prompt.count("INT. lower decks maintenance shaft") <= 1
    assert "Caption:" not in prompt
