"""Area #11 v1 W4 — canary 17 scenario (11 enum dispatch + 4 fail-fast + 1 full pipeline + 1 sanity).

`prompt_service.resolve_ref_roles(payload)` 가 LabeledRefPayload 의 `ref_roles`
enum 만으로 dispatch (substring branch 폐기) — end-to-end canary.

11 enum dispatch (각 REF_ROLE_VALUES entry, canary 1-11):
  1. outfit_ref_explicit
  2. outfit_ref_inline
  3. previous_shot_same_frame_zoomed
  4. previous_shot_same_room
  5. previous_shot_continuity
  6. background_chain_ref
  7. character_ref
  8. character_state_ref
  9. prop_ref
  10. background_general
  11. fallback

4 factory fail-fast (canary 12-15, REF_ROLE_VALUES 분류 별 1 representative):
  12. make_labeled_ref_payload non-list labeled_refs → RefRoleError
  13. make_labeled_ref_payload length mismatch (ref_roles vs labeled_refs) → RefRoleError
  14. make_labeled_ref_payload invalid enum value → RefRoleError
  15. make_labeled_ref_payload non-dict ref_role_metadata entry → RefRoleError

1 happy path (canary 16):
  16. build_final_scene_prompt with payload full pipeline → no exception,
      expected output shape (ref directive + trailing universal + ID substitution).

1 REF_ROLE_VALUES sanity (canary_extras):
  17. REF_ROLE_VALUES 11 count + content invariant.

NO VLM. structured SOT only (LabeledRefPayload 4 parallel list + 11 enum dispatch).

spec: docs/superpowers/specs/2026-05-18-area-11-classify-label-substring-replacement-design.md
plan: docs/superpowers/plans/2026-05-18-area-11-classify-label-substring-replacement-implementation.md
"""
from __future__ import annotations

import pytest

from app.services.prompt_service import (
    LabeledRefPayload,
    REF_ROLE_VALUES,
    RefRoleError,
    build_final_scene_prompt,
    make_labeled_ref_payload,
    resolve_ref_roles,
)


def _payload(role: str, label: str = "test label", metadata: dict | None = None) -> LabeledRefPayload:
    return make_labeled_ref_payload(
        labeled_refs=[(label, b"\x89PNG")],
        ref_roles=[role],
        ref_role_metadata=[metadata or {}],
        attached_meta=[("test_kind", "T01")],
    )


# ── 11 enum dispatch canary ───────────────────────────────────────


def test_canary_1_outfit_ref_explicit_dispatch():
    """★2026-09-02 뒤집었다 — 지시가 **누구의 복장인지** 말해야 한다.

    고증 조사가 고른 복장 사진이 이 역할로 붙으면서, 한 샷에 인물 둘·복장
    둘이면 Image N 이 누구 것인지 모델이 알 길이 없던 것이 드러났다
    (Codex BLOCK). 신원 없이 부르면 이제 **선다**.
    """
    res = resolve_ref_roles(_payload(
        "outfit_ref_explicit", "outfit appearance",
        {"subject_final_ids": ["C01O02"]}))
    assert any("standalone outfit/costume reference for C01O02" in r
               for r in res.ref_roles)
    assert any("dress C01O02 in the outfit" in i for i in res.ref_instructions)


def test_canary_2_outfit_ref_inline_dispatch():
    res = resolve_ref_roles(_payload("outfit_ref_inline", "Image 1 (outfit inline): shirt"))
    assert any("character appearance reference" in i for i in res.ref_instructions)
    assert any("match the person's identity and outfit" in i for i in res.ref_instructions)


def test_canary_3_previous_shot_same_frame_zoomed_dispatch():
    # W3 (2026-06-11): exact-frame 절대 지시 완화 (S29 sh11 프레임 복제 실측 fix).
    res = resolve_ref_roles(_payload("previous_shot_same_frame_zoomed", "prev shot SAME FRAME zoomed"))
    assert any("SAME MOMENT, zoomed-in reframing" in r for r in res.ref_roles)
    assert any("render the focused region" in i for i in res.ref_instructions)


def test_canary_4_previous_shot_same_room_dispatch():
    res = resolve_ref_roles(_payload("previous_shot_same_room", "prev shot SAME ROOM"))
    assert any("BACKGROUND from a previous shot (SAME ROOM)" in r for r in res.ref_roles)
    assert any("do NOT copy any standing/moving people" in i for i in res.ref_instructions)


def test_canary_5_previous_shot_continuity_dispatch():
    res = resolve_ref_roles(_payload("previous_shot_continuity", "prev shot continuity"))
    assert any("BACKGROUND/ENVIRONMENT from a previous shot at the same location" in r
               for r in res.ref_roles)
    assert any("use ONLY the lighting, color palette, and environment mood" in i
               for i in res.ref_instructions)


def test_canary_6_background_chain_ref_dispatch():
    # W1 (2026-06-11): 'use as-is'/'exactly' → environment identity 역할로 완화
    # (카메라/구도는 본문 프롬프트가 SOT — S10 실측 fix).
    res = resolve_ref_roles(_payload("background_chain_ref", "BG chain reference"))
    assert any("pre-rendered BACKGROUND environment reference" in r for r in res.ref_roles)
    assert any("wall/floor/ceiling materials" in i for i in res.ref_instructions)
    assert any("do NOT copy the camera position or composition" in i
               for i in res.ref_instructions)


def test_canary_7_character_ref_dispatch():
    res = resolve_ref_roles(_payload("character_ref", "Image 1 (character reference): name"))
    assert any("character appearance reference" in i for i in res.ref_instructions)
    assert any("match the person's identity where visible in the scene" in i
               for i in res.ref_instructions)


def test_canary_8_character_state_ref_dispatch():
    res = resolve_ref_roles(_payload(
        "character_state_ref",
        "char state ref unconscious",
        metadata={"state": "unconscious"},
    ))
    assert any("state-variant reference (unconscious)" in i for i in res.ref_instructions)


def test_canary_9_prop_ref_dispatch():
    res = resolve_ref_roles(_payload("prop_ref", "Image 1 (object reference): prop"))
    assert any("include the object shown in image 1" in i for i in res.ref_instructions)


def test_canary_10_background_general_dispatch():
    res = resolve_ref_roles(_payload("background_general", "background reference"))
    assert any("background/environment reference" in r for r in res.ref_roles)
    assert any("use the lighting, architecture, and environment mood" in i
               for i in res.ref_instructions)


def test_canary_11_fallback_dispatch():
    res = resolve_ref_roles(_payload("fallback", "some generic label"))
    assert any("reference image 1: some generic label" in i for i in res.ref_instructions)
    # 다른 분기 누설 0
    assert not any("character appearance reference" in i for i in res.ref_instructions)
    assert not any("include the object" in i for i in res.ref_instructions)


# ── 4 factory fail-fast canary ────────────────────────────────────


def test_canary_12_factory_non_list_labeled_refs_raises():
    with pytest.raises(RefRoleError, match="labeled_refs is str"):
        make_labeled_ref_payload(
            labeled_refs="not-list",  # type: ignore[arg-type]
            ref_roles=["character_ref"],
            ref_role_metadata=[{}],
            attached_meta=[("character", "C01")],
        )


def test_canary_13_factory_length_mismatch_raises():
    with pytest.raises(RefRoleError, match="ref_roles length 2 != labeled_refs length 1"):
        make_labeled_ref_payload(
            labeled_refs=[("a", b"\x89")],
            ref_roles=["character_ref", "prop_ref"],  # 2 vs 1
            ref_role_metadata=[{}],
            attached_meta=[("character", "C01")],
        )


def test_canary_14_factory_invalid_enum_raises():
    with pytest.raises(RefRoleError, match=r"ref_roles\[0\]='UNKNOWN' not in REF_ROLE_VALUES"):
        make_labeled_ref_payload(
            labeled_refs=[("a", b"\x89")],
            ref_roles=["UNKNOWN"],
            ref_role_metadata=[{}],
            attached_meta=[("character", "C01")],
        )


def test_canary_15_factory_non_dict_metadata_raises():
    with pytest.raises(RefRoleError, match=r"ref_role_metadata\[0\] is list, not dict"):
        make_labeled_ref_payload(
            labeled_refs=[("a", b"\x89")],
            ref_roles=["fallback"],
            ref_role_metadata=[["not", "dict"]],  # type: ignore[list-item]
            attached_meta=[("character", "C01")],
        )


# ── 16: happy path full pipeline ──────────────────────────────────


def test_canary_16_full_pipeline_no_korean_no_id_substitution():
    """`build_final_scene_prompt(payload)` end-to-end: T2I 영어 본문 + payload (1 ref) →
    완성 prompt 가 ref directive + trailing universal directive 모두 포함."""
    payload = make_labeled_ref_payload(
        labeled_refs=[("C01 wearing regular clothes", b"\x89")],
        ref_roles=["outfit_ref_inline"],
        ref_role_metadata=[{}],
        attached_meta=[("character", "C01")],
    )
    out = build_final_scene_prompt(
        t2i_prompt="C01 stands at the threshold.",
        payload=payload,
        style_context="cinematic",
    )
    # ref role line
    assert "Reference image 1: C01 wearing regular clothes" in out
    # outfit_ref_inline dispatch directive
    assert "match the person's identity and outfit where visible" in out
    # trailing universal directives (3 line)
    assert "do not copy poses or compositions from reference images" in out
    assert "do not alter character identities where their face is visible" in out
    assert "only render what the scene description asks for" in out
    # ID substitution: C01 → "the character shown in image 1"
    assert "the character shown in image 1 stands" in out
    # close framing CRITICAL footer
    assert "CRITICAL" in out


# ── REF_ROLE_VALUES sanity ────────────────────────────────────────


def test_canary_extras_ref_role_values_11_count_invariant():
    """REF_ROLE_VALUES = 12 enum (Area #11 v1 spec §3.1 baseline 11
    + W21B-W8 composition_guide 2026-06-13)."""
    assert len(REF_ROLE_VALUES) == 12
    # dispatch 가 cover 하는 enum 과 정확히 일치
    covered = {
        "outfit_ref_explicit", "outfit_ref_inline",
        "previous_shot_same_frame_zoomed", "previous_shot_same_room",
        "previous_shot_continuity", "background_chain_ref",
        "character_ref", "character_state_ref", "prop_ref",
        "background_general", "fallback", "composition_guide",
    }
    assert set(REF_ROLE_VALUES) == covered


def test_canary_18_composition_guide_dispatch():
    """W21B-W8 composition guide (재배선) — 마네킹 POSE+COMPOSITION 스케치:
    구도 매칭 + 포즈/머리방향 재현 + 마네킹→실제 인물 치환 + 관절/구조선·톤
    복사 금지 (실루엣 gaze fix supersede: 마네킹이 facing SOT)."""
    res = resolve_ref_roles(_payload("composition_guide", "storyboard sketch"))
    assert any("POSE + COMPOSITION STORYBOARD SKETCH" in r for r in res.ref_roles)
    assert any("match this frame's composition to image 1" in i
               for i in res.ref_instructions)
    # 마네킹은 포즈/머리방향까지 재현 (실루엣 'NOT facing' supersede)
    assert any("body pose, stance and head-facing direction exactly" in i
               for i in res.ref_instructions)
    assert any("render it as a real clothed person" in i
               for i in res.ref_instructions)
    # 마네킹/관절/구조선·톤은 최종 이미지에 그리지 말 것
    assert any("do NOT draw" in i and "mannequin" in i
               for i in res.ref_instructions)
    assert any("do NOT copy its flat tones, line style" in i
               for i in res.ref_instructions)


def test_canary_19_same_room_composition_relaxed_dispatch():
    """W21B-W8 — same_room + composition_relaxed metadata: 'use as-is' 잠금이
    빠지고 camera/composition 복사 금지 + place identity 지시로 대체."""
    res = resolve_ref_roles(_payload(
        "previous_shot_same_room", "prev shot SAME ROOM",
        metadata={"composition_relaxed": True},
    ))
    joined = "\n".join(res.ref_roles + res.ref_instructions)
    assert "as-is" not in joined
    assert any("do NOT copy the camera position or composition" in i
               for i in res.ref_instructions)
    assert any("place identity reference" in r for r in res.ref_roles)
    # 완화여도 인물 복사 금지는 유지
    assert any("do NOT copy any standing/moving people" in i
               for i in res.ref_instructions)
