"""C01 zero-gate regression — Task 6 of single-vs-batch reference contract fix.

spec: docs/superpowers/specs/2026-05-08-single-batch-reference-contract-design.md §5.2
plan: docs/superpowers/plans/2026-05-08-single-batch-reference-contract-implementation.md Task 6

가드 (사용자 binding):
- 실제 이미지 재생성 X (fixture 기반 contract 검증)
- DB 25 still 건드리지 X
- S13 fail-fast / S8 phantom from-the-reference / S19/S21 actual refs / S15 prev_shot
"""
from __future__ import annotations

import json
from pathlib import Path

import pytest


FIXTURE_PATH = Path(__file__).parent.parent / "fixtures" / "c01_zero_gate_regression.json"


@pytest.fixture
def regression_fixture():
    return json.loads(FIXTURE_PATH.read_text(encoding="utf-8"))


def test_fixture_has_5_stills(regression_fixture):
    """C01 zero-gate NG 5 stills (S8/S13/S15/S19/S21)."""
    labels = [s["label"] for s in regression_fixture["stills"]]
    assert sorted(labels) == sorted(["S8_Shot4", "S13_Shot6", "S15_Shot5", "S19_Shot1", "S21_Shot6"])


def test_fixture_has_required_fields(regression_fixture):
    """각 still entry: still_id / visible_short_ids / expected refs / is_close_framing /
    user_reported_defect / root_cause / task_4_relevant."""
    required = {"still_id", "visible_short_ids", "is_close_framing",
                "user_reported_defect", "root_cause", "task_4_relevant"}
    for entry in regression_fixture["stills"]:
        missing = required - set(entry.keys())
        assert not missing, f"{entry['label']} missing fields: {missing}"


# ──────────────────────────────────────────────────────────────────────
# S13 Shot6 face close-up exemption test 폐기 (Area #1 W7c, 2026-05-16):
# W4 가 `_forward_enforcement_exempt` function + `body_part_focus_rule`
# trigger_phrases 검사 path 자체 폐기. 새 structured exemption 흐름은
# W4 unit tests (`_is_forward_exempt_by_render_strategy` +
# `_is_forward_exempt_by_reproduction_surface_rule`) 가 직접 cover.
# Task 4 S13 회귀 차단은 새 path 의 negative gate (subject_reference_policy
# !=== generic_descriptor_allowed AND render_strategy.mode != partial_focus AND
# reproduction_surface_rule.applies == False) 가 담당.
# ──────────────────────────────────────────────────────────────────────


# ──────────────────────────────────────────────────────────────────────
# S8/S19 — Task 3 phantom 'from the reference' fail-fast 가드
# ──────────────────────────────────────────────────────────────────────


def test_s8_shot4_phantom_from_reference_raises():
    """S8 Shot4 production prompt 의 'sliding door from the reference' 패턴 →
    background classifier 매칭 + bg meta 부재 + NOT close → RefContractError.

    D5 §4.4: phantom guard 의 ref 존재 판정은 attached_meta 기반 (label X).
    """
    from app.core.ref_contract_validator import validate_attached_refs, RefContractError
    rpc = {"asset_requirements": {"required_refs": {"background": ["L09"]}}}
    labeled_refs = [("character C01O02 in outfit", b"x")]
    attached_meta = [("character_outlook", "C01O02")]  # bg meta 부재
    prompt = (
        "Two figures occupy the shared aisle near the checkout counter from the reference. "
        "The glass entrance doors of the large supermarket from the reference are brightly "
        "illuminated."
    )
    with pytest.raises(RefContractError):
        validate_attached_refs(
            rpc, labeled_refs, attached_meta, prompt, is_close_framing=False,
            reference_phrase_kinds=[],
        )


def test_s19_shot1_phantom_storefront_raises():
    """S19 Shot1: 'storefront from the reference' 패턴 + bg meta 부재 → fail."""
    from app.core.ref_contract_validator import validate_attached_refs, RefContractError
    rpc = {"asset_requirements": {"required_refs": {"background": ["L10"]}}}
    labeled_refs = [("character C01O02 in outfit", b"x")]
    attached_meta = [("character_outlook", "C01O02")]
    prompt = "She stands outside the storefront from the reference, the sliding door directly behind her."
    with pytest.raises(RefContractError):
        validate_attached_refs(
            rpc, labeled_refs, attached_meta, prompt, is_close_framing=False,
            reference_phrase_kinds=[],
        )


# ──────────────────────────────────────────────────────────────────────
# S15 Shot5 — Task 1+2 prev_shot_ref helper contract 가드
# ──────────────────────────────────────────────────────────────────────


def test_s15_shot5_helper_invokes_prev_shot_when_no_chain():
    """S15 Shot5: chain_bg 부재 + close framing X → helper 가 prev_shot_ref 시도 (Task 1 5c)."""
    from unittest.mock import MagicMock, patch
    from app.services.scene_generation_coordinator import build_scene_attached_refs

    ref_svc = MagicMock()
    ref_svc.build_scene_ref_image_map.return_value = {"c01": b"x"}
    ref_svc.detect_state_variant_sids.return_value = {}
    # Area #11 v1 W3: 2-tuple → LabeledRefPayload (W2 cascade).
    from app.services.prompt_service import make_labeled_ref_payload
    _resolve_return = make_labeled_ref_payload(
        labeled_refs=[("character C01O02 in outfit", b"x")],
        ref_roles=["character_ref"],
        ref_role_metadata=[{}],
        attached_meta=[("character_outlook", "C01O02")],
    )
    ref_svc.resolve_refs_for_prompt.return_value = _resolve_return
    ref_svc.resolve_refs_for_prompt_set.return_value = _resolve_return
    # Area #11 v1 W3: 3-tuple → 5-tuple (label, bytes, loc_id, ref_role, ref_role_metadata)
    ref_svc.build_prev_shot_background_ref.return_value = (
        "previous shot SAME ROOM", b"prev", "L05", "previous_shot_same_room", {},
    )

    still = MagicMock()
    still.camera_json = "{}"
    still.scene_index = 15
    still.still_index = 0
    still.shot_index = 5

    with patch(
        "app.services.scene_generation_coordinator.load_shot_t2i_variations",
        # Area #5 W3: producer (scene_detail v26) emit sidecar — fixture default = []
        return_value=[{"t2i_prompt": "C02O03 shouts", "reference_phrase_kinds": []}],
    ), patch(
        "app.services.scene_generation_coordinator._build_final_scene_prompt",
        side_effect=lambda var_t2i, payload, style_ctx, **kw: f"FINAL[{var_t2i}]",
    ), patch(
        "app.services.scene_generation_coordinator._build_image_index_helper",
        # D5 §4.3: 4-tuple return + attached_meta passthrough
        side_effect=lambda labels, lookup, *, ref_roles=None, ref_role_metadata=None, attached_meta=None: (labels, {}, {}, list(ref_roles or []), list(ref_role_metadata or []), list(attached_meta or [])),
    ), patch(
        "app.services.scene_generation_coordinator._rewrite_t2i_helper",
        side_effect=lambda var, sid_map, sid_info: var,
    ), patch(
        # FINDING 11: build_scene_attached_refs 가 'character' over-declaration
        # normalization helper 호출 전 render_prompt_card 를 resolve.
        "app.services.scene_generation_coordinator.lookup_render_prompt_card",
        return_value={"asset_requirements": {"required_refs": []}},
    ):
        full_prompt, labeled_refs, _attached_meta, _phrase_kinds = build_scene_attached_refs(  # Area #5 W3: 4-tuple
            still=still, episode_id="ep_test",
            still_data={"scene_index": 15, "shot_index": 5,
                        "still_frame_prompt": "test", "beat_title": ""},
            visible_entities=[
                {"id": "c01", "short_id": "C01", "entity_type": "character"},
                {"id": "c02", "short_id": "C02", "entity_type": "character"},
            ],
            ref_image_map={"c01": b"face"},
            cached_style_context="",
            cached_entity_text_map={},
            scene_paths_by_index_by_id={},
            location_scene_history={},
            background_chain_bg_map={},  # chain_bg 없음
            dep_detail_map={},
            staging={"camera_direction": "medium shot", "framing_scale": "medium"},  # NOT close framing
            entity_lookup={
                "c01": {"id": "c01", "short_id": "C01", "entity_type": "character"},
                "c02": {"id": "c02", "short_id": "C02", "entity_type": "character"},
            },
            project_id="p_test",
            project_config={},
            reference_svc=ref_svc,
            stills=[still],
        )

    # 5c 분기 — prev_shot_ref 호출 + 결과 첫 번째에 insert
    assert ref_svc.build_prev_shot_background_ref.call_count == 1
    assert labeled_refs[0][0] == "previous shot SAME ROOM"


# ──────────────────────────────────────────────────────────────────────
# S21 Shot6 — Task 1 helper close framing skip 정책
# ──────────────────────────────────────────────────────────────────────


def test_s21_shot6_close_framing_skips_chain_bg():
    """S21 Shot6: close framing 매칭 → chain_bg skip + prev_shot try (Task 1 5b)."""
    from unittest.mock import MagicMock, patch
    from app.services.scene_generation_coordinator import build_scene_attached_refs

    ref_svc = MagicMock()
    ref_svc.build_scene_ref_image_map.return_value = {"c01": b"x"}
    ref_svc.detect_state_variant_sids.return_value = {}
    # Area #11 v1 W3: 2-tuple → LabeledRefPayload (W2 cascade).
    from app.services.prompt_service import make_labeled_ref_payload
    _resolve_return = make_labeled_ref_payload(
        labeled_refs=[("character C01O02 in outfit", b"x")],
        ref_roles=["character_ref"],
        ref_role_metadata=[{}],
        attached_meta=[("character_outlook", "C01O02")],
    )
    ref_svc.resolve_refs_for_prompt.return_value = _resolve_return
    ref_svc.resolve_refs_for_prompt_set.return_value = _resolve_return
    ref_svc.build_prev_shot_background_ref.return_value = None  # prev_shot 도 없으면 entity-only fallback

    still = MagicMock()
    still.camera_json = "{}"
    still.scene_index = 21
    still.still_index = 0
    still.shot_index = 6

    with patch(
        "app.services.scene_generation_coordinator.load_shot_t2i_variations",
        # Area #5 W3: producer sidecar — fixture default = []
        return_value=[{"t2i_prompt": "C01O02 frozen at desk", "reference_phrase_kinds": []}],
    ), patch(
        "app.services.scene_generation_coordinator._build_final_scene_prompt",
        side_effect=lambda var_t2i, payload, style_ctx, **kw: f"FINAL[{var_t2i}]",
    ), patch(
        "app.services.scene_generation_coordinator._build_image_index_helper",
        # D5 §4.3: 4-tuple return + attached_meta passthrough
        side_effect=lambda labels, lookup, *, ref_roles=None, ref_role_metadata=None, attached_meta=None: (labels, {}, {}, list(ref_roles or []), list(ref_role_metadata or []), list(attached_meta or [])),
    ), patch(
        "app.services.scene_generation_coordinator._rewrite_t2i_helper",
        side_effect=lambda var, sid_map, sid_info: var,
    ), patch(
        # FINDING 11: build_scene_attached_refs 가 'character' over-declaration
        # normalization helper 호출 전 render_prompt_card 를 resolve.
        "app.services.scene_generation_coordinator.lookup_render_prompt_card",
        return_value={"asset_requirements": {"required_refs": []}},
    ):
        full_prompt, labeled_refs, _attached_meta, _phrase_kinds = build_scene_attached_refs(  # Area #5 W3: 4-tuple
            still=still, episode_id="ep_test",
            still_data={"scene_index": 21, "shot_index": 6,
                        "still_frame_prompt": "test", "beat_title": ""},
            visible_entities=[{"id": "c01", "short_id": "C01", "entity_type": "character"}],
            ref_image_map={"c01": b"face"},
            cached_style_context="",
            cached_entity_text_map={},
            scene_paths_by_index_by_id={},
            location_scene_history={},
            background_chain_bg_map={
                "21_6": {"label": "BACKGROUND chain reference", "image_bytes": b"bg"},
            },
            dep_detail_map={},
            staging={"camera_direction": "extreme close-up", "framing_scale": "close"},  # close framing
            entity_lookup={
                "c01": {"id": "c01", "short_id": "C01", "entity_type": "character"},
            },
            project_id="p_test",
            project_config={},
            reference_svc=ref_svc,
            stills=[still],
        )

    # 5b 분기 — chain_bg skip (close framing) + prev_shot try (None) → entity-only
    assert not any("BACKGROUND chain reference" == lbl for lbl, _ in labeled_refs)
    assert ref_svc.build_prev_shot_background_ref.call_count == 1
    assert labeled_refs == [("character C01O02 in outfit", b"x")]
