"""REC-31 (A1) Phase 4: the gate fires inside _collect_reference_images - the ONE
function both build_unified_payload and _build_audit_payload converge on. A 2-char
shot with WREN hero-missing BLOCKS via BOTH entry points. No spend.

Fixture pattern is lifted from the existing
recoil/pipeline/_lib/tests/test_dispatch_payload.py::test_r2v_multi_collects_refs_across_batch
(same projects_root/_resolve_start_frame monkeypatching, same tmp-project asset
layout, same CanonicalShot construction)."""
from __future__ import annotations

import sys
from pathlib import Path

import pytest

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

from recoil.pipeline._lib.dispatch_payload import (  # noqa: E402
    _collect_reference_images,
    build_dispatch_payload,
)
from recoil.pipeline._lib.plan_loader import CanonicalShot, CharacterEntry  # noqa: E402
from recoil.core.ref_errors import MissingRequiredRefError  # noqa: E402

PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 32


def _stage_ep001_sh37(tmp_path, monkeypatch):
    """JADE shelf hero present; WREN has legacy front+profile, NO hero
    (mirrors the live EP001_SH37 disease). Returns the 2-char CanonicalShot."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.projects_root", lambda: tmp_path)
    monkeypatch.setattr("recoil.core.paths.projects_root", lambda: tmp_path)
    from recoil.pipeline._lib import dispatch_payload as _dp
    monkeypatch.setitem(_dp._project_config_cache, "tartarus", {})

    proj = tmp_path / "tartarus"
    # JADE - shelf hero (canonical pointer) present.
    jade_look = proj / "assets" / "char" / "jade" / "base"
    jade_look.mkdir(parents=True)
    (jade_look / "jade-identity.png").write_bytes(PNG_BYTES)
    # WREN - legacy-flat front + profile only, NO hero file.
    wren_dir = proj / "assets" / "char" / "wren"
    wren_dir.mkdir(parents=True)
    (wren_dir / "wren_identity_front_v01.png").write_bytes(PNG_BYTES)
    (wren_dir / "wren_identity_profile_v01.png").write_bytes(PNG_BYTES)

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


def test_collector_direct_blocks_on_wren_hero_missing(tmp_path, monkeypatch):
    """Live path entry point: _collect_reference_images is called directly by
    build_unified_payload (dispatch_payload.py:1202). WREN hero-missing -> BLOCK."""
    cs = _stage_ep001_sh37(tmp_path, monkeypatch)
    with pytest.raises(MissingRequiredRefError) as e:
        _collect_reference_images(cs, "tartarus", "r2v_multi", batch_shots=[cs])
    assert "WREN" in e.value.subjects
    assert "JADE" not in e.value.subjects   # JADE has a hero -> not blocked


def test_audit_dry_run_blocks_on_wren_hero_missing(tmp_path, monkeypatch):
    """Audit path entry point: build_dispatch_payload(..., dry_run=True) ->
    _build_audit_payload -> _collect_reference_images (dispatch_payload.py:2117).
    Same BLOCK proves audit == collector (closes codex MAJOR #4)."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload._resolve_start_frame",
        lambda *, shot, project, override: Path("/dev/null"))
    cs = _stage_ep001_sh37(tmp_path, monkeypatch)
    with pytest.raises(MissingRequiredRefError) as e:
        build_dispatch_payload(
            shot=cs, project="tartarus", modality="r2v_multi",
            batch_shots=[cs], episode="ep_001", dry_run=True)
    assert "WREN" in e.value.subjects


def _stage_ep001_sh37_legacy_jade(tmp_path, monkeypatch):
    """The REAL EP001_SH37 disease (codex MAJOR): JADE's hero is LEGACY-FLAT
    (jade_identity_hero_v01.jpeg at subject-root, NO shelf, NO -{phase} suffix),
    WREN is legacy front+profile only. Both carry wardrobe phases. Proves the
    legacy-flat tier retries phase-agnostically so JADE's unphased legacy hero
    still satisfies the gate, while WREN (no hero variant even unphased) blocks."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.projects_root", lambda: tmp_path)
    monkeypatch.setattr("recoil.core.paths.projects_root", lambda: tmp_path)
    from recoil.pipeline._lib import dispatch_payload as _dp
    monkeypatch.setitem(_dp._project_config_cache, "tartarus", {})

    proj = tmp_path / "tartarus"
    # JADE - LEGACY-FLAT hero at subject-root (variant 'hero', NO -{phase}); no shelf.
    jade_dir = proj / "assets" / "char" / "jade"
    jade_dir.mkdir(parents=True)
    (jade_dir / "jade_identity_hero_v01.jpeg").write_bytes(PNG_BYTES)
    # WREN - legacy-flat front + profile only, NO hero file (even unphased).
    wren_dir = proj / "assets" / "char" / "wren"
    wren_dir.mkdir(parents=True)
    (wren_dir / "wren_identity_front_v01.png").write_bytes(PNG_BYTES)
    (wren_dir / "wren_identity_profile_v01.png").write_bytes(PNG_BYTES)

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


def test_legacy_jade_hero_passes_phase_agnostic_wren_blocks(tmp_path, monkeypatch):
    """codex MAJOR: mirrors real EP001_SH37 - JADE has a LEGACY-FLAT unphased hero
    (variant 'hero', NO -{phase} suffix) and a wardrobe_phase_id; WREN has only
    legacy front/profile. The phased resolve drops JADE's hero unless the legacy
    tier retries phase-agnostically. Assert: the collector does NOT block JADE (her
    legacy hero satisfies the gate via the phase-agnostic retry) but DOES raise
    MissingRequiredRefError naming ONLY WREN (no hero variant even unphased)."""
    cs = _stage_ep001_sh37_legacy_jade(tmp_path, monkeypatch)
    with pytest.raises(MissingRequiredRefError) as e:
        _collect_reference_images(cs, "tartarus", "r2v_multi", batch_shots=[cs])
    assert "WREN" in e.value.subjects, "WREN has no hero even unphased -> blocks"
    assert "JADE" not in e.value.subjects, \
        "JADE's unphased legacy hero satisfies the gate via the phase-agnostic retry"


def _stage_two_shelf_heroes(tmp_path, monkeypatch):
    """BOTH chars have a shelf hero ONLY (no legacy-flat, no pool) - the
    positive path CRITICAL-1 demands: dispatched identity refs must come
    FROM the bundle (the shelf hero one dir deeper), not the legacy dict."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload.projects_root", lambda: tmp_path)
    monkeypatch.setattr("recoil.core.paths.projects_root", lambda: tmp_path)
    from recoil.pipeline._lib import dispatch_payload as _dp
    monkeypatch.setitem(_dp._project_config_cache, "tartarus", {})

    proj = tmp_path / "tartarus"
    for subj in ("jade", "wren"):
        look = proj / "assets" / "char" / subj / "base"
        look.mkdir(parents=True)
        (look / f"{subj}-identity.png").write_bytes(PNG_BYTES)  # SHELF hero only

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


def test_two_shelf_heroes_dispatch_both_identity_refs(tmp_path, monkeypatch):
    """CRITICAL-1 positive path: BOTH chars shelf-hero-ONLY -> the collector PASSES
    the gate AND emits both identity_N refs (the shelf heroes, one dir deeper).
    Proves dispatched refs come FROM the bundle, not the legacy subject-root dict
    (which returns {} for a shelf hero and would silently drop the ref)."""
    cs = _stage_two_shelf_heroes(tmp_path, monkeypatch)
    refs, manifest = _collect_reference_images(cs, "tartarus", "r2v_multi", batch_shots=[cs])
    assert "identity_1" in manifest and "identity_2" in manifest
    hero_paths = {refs[manifest["identity_1"] - 1], refs[manifest["identity_2"] - 1]}
    assert any("wren-identity.png" in p for p in hero_paths), "WREN shelf hero dispatched"
    assert any("jade-identity.png" in p for p in hero_paths), "JADE shelf hero dispatched"


def test_two_shelf_heroes_dispatch_via_build_dispatch_payload(tmp_path, monkeypatch):
    """CRITICAL-1 positive path through the audit/dry-run entry point: same two
    shelf heroes resolve + pass the gate via build_dispatch_payload(dry_run=True)."""
    monkeypatch.setattr(
        "recoil.pipeline._lib.dispatch_payload._resolve_start_frame",
        lambda *, shot, project, override: Path("/dev/null"))
    cs = _stage_two_shelf_heroes(tmp_path, monkeypatch)
    payload = build_dispatch_payload(
        shot=cs, project="tartarus", modality="r2v_multi",
        batch_shots=[cs], episode="ep_001", dry_run=True)
    # No raise = gate passed; the dispatched payload carries both shelf heroes.
    assert payload is not None


def test_composite_sheet_bypass_is_gated(tmp_path, monkeypatch):
    """MAJOR-2: the composite-sheet early return (dispatch_payload.py:1885-1887)
    MUST run assert_refs_complete BEFORE `return sheet_result`. Enable composite
    sheets via RECOIL_USE_COMPOSITE_SHEETS=1, stage VALID sheet files for the shot
    + location, but a required subject (WREN) is front/profile-only (no hero) ->
    the collector must raise MissingRequiredRefError BEFORE returning the sheet."""
    import os
    from PIL import Image

    monkeypatch.setenv("RECOIL_USE_COMPOSITE_SHEETS", "1")
    cs = _stage_ep001_sh37(tmp_path, monkeypatch)  # JADE shelf hero; WREN front/profile-only

    # Stage VALID composite sheets (>=100KB, PNG magic, >=512x512) at the canonical
    # paths so _collect_sheet_refs would otherwise short-circuit and return.
    # Use random-noise pixels so the PNG does NOT compress below the 100KB real-sheet
    # floor (_MIN_SHEET_BYTES) - a solid-color 1024x1024 PNG compresses to a few KB.
    from recoil.core.paths import ProjectPaths
    _pp = ProjectPaths.for_project("tartarus")  # sheet_path is now the layout SSOT
    noise = Image.frombytes("RGB", (1024, 1024), os.urandom(1024 * 1024 * 3))
    for entity_type, entity_id in (
        ("characters", "WREN"), ("characters", "JADE"),
        ("locations", "int_lower_decks_maintenance_shaft"),
    ):
        sheet = _pp.sheet_path(entity_type, entity_id)  # normalizes plural -> char/loc/prop
        sheet.parent.mkdir(parents=True, exist_ok=True)
        noise.save(sheet)
        assert sheet.stat().st_size >= 100_000, "sheet must clear the real-sheet floor"

    with pytest.raises(MissingRequiredRefError) as e:
        _collect_reference_images(cs, "tartarus", "r2v_multi", batch_shots=[cs])
    assert "WREN" in e.value.subjects
