"""prompt_service ref-role dispatch — Area #11 v1 W3 cascade.

W2 atomic switch (commit 48dc256) 후 `resolve_ref_roles` 는 producer-emitted
`LabeledRefPayload.ref_roles` enum 만 dispatch (substring branch 폐기).

본 파일 = Area #5 v1 closure + Area #11 v1 W2 의 11-enum SOT 검증:

- 11 REF_ROLE_VALUES 각각 dispatch 검증 (per-enum ref_roles_text + ref_instructions).
- metadata-driven extra emit (keep_elements / ignore / state) 검증.
- Common trailing instructions 검증 (3 universal directives).

Cleanup history (Codex iter 4 W2/W3/W4 권고 verbatim):
- W3 (commit 35f9e8a): legacy direct tests removed — substring-classifier
  helper 의 direct unit test 폐기, 11-enum dispatch test 로 rewrite.
- W4 (commit b29cb53): production helper removed — substring-classifier
  cluster (_classify_label / _is_explicit_character_ref_label / regex
  constants / character tag frozensets) 모두 production 에서 폐기.

→ closure 시점 substring matching production = 0, test direct import = 0.

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

from typing import Any, Dict, List, Optional, Tuple

import pytest

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


def _make_payload(
    *,
    labeled_refs: List[Tuple[str, bytes]],
    ref_roles: List[str],
    ref_role_metadata: Optional[List[Dict[str, Any]]] = None,
    attached_meta: Optional[List[Tuple[str, str]]] = None,
) -> LabeledRefPayload:
    """Single-element payload helper — default metadata = empty dict, attached_meta = ('unknown', '')."""
    n = len(labeled_refs)
    if ref_role_metadata is None:
        ref_role_metadata = [{} for _ in range(n)]
    if attached_meta is None:
        attached_meta = [("unknown", "") for _ in range(n)]
    return make_labeled_ref_payload(
        labeled_refs=labeled_refs,
        ref_roles=ref_roles,
        ref_role_metadata=ref_role_metadata,
        attached_meta=attached_meta,
    )


# ── empty payload edge case ──────────────────────────────────────


def test_empty_payload_returns_no_reference_marker_with_trailing_directives():
    """0-element payload — ref_roles 빈 list, roles_text='No reference images.', trailing 3 directive 유지."""
    payload = make_labeled_ref_payload(
        labeled_refs=[],
        ref_roles=[],
        ref_role_metadata=[],
        attached_meta=[],
    )
    res = resolve_ref_roles(payload)
    assert res.ref_roles == []
    assert res.roles_text == "No reference images."
    # 공통 trailing 3 directive 항상 emit
    assert any("do not copy poses" in i for i in res.ref_instructions)
    assert any("do not alter character identities" in i for i in res.ref_instructions)
    assert any("only render what the scene description asks for" in i for i in res.ref_instructions)


# ── 11-enum dispatch (one test per role) ──────────────────────────


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

    앞 판은 사진마다 똑같이 「dress the character」였다. 한 샷에 인물 둘·복장
    둘이면 Image 1·2 가 각각 누구 것인지 모델이 알 길이 없다 (Codex BLOCK).
    """
    payload = _make_payload(
        labeled_refs=[("outfit appearance", b"\x89")],
        ref_roles=["outfit_ref_explicit"],
        ref_role_metadata=[{"subject_final_ids": ["C01O02"]}],
    )
    res = resolve_ref_roles(payload)
    assert any("standalone outfit/costume reference for C01O02" in r
               for r in res.ref_roles), res.ref_roles
    assert any("dress C01O02 in the outfit shown in image 1" in i
               for i in res.ref_instructions), res.ref_instructions


@pytest.mark.parametrize("subjects", [
    None, [], ["C01O02", "C02O03"], [""], "C01O02",
])
def test_outfit_ref_explicit_refuses_to_guess(subjects):
    """★★신원이 없거나 둘 이상이면 **선다** — 짐작하지 않는다."""
    md = {} if subjects is None else {"subject_final_ids": subjects}
    payload = _make_payload(
        labeled_refs=[("outfit appearance", b"\x89")],
        ref_roles=["outfit_ref_explicit"],
        ref_role_metadata=[md],
    )
    with pytest.raises(RefRoleError):
        resolve_ref_roles(payload)


def test_dispatch_outfit_ref_inline():
    payload = _make_payload(
        labeled_refs=[("Image 1 (outfit inline): generic shirt", b"\x89")],
        ref_roles=["outfit_ref_inline"],
    )
    res = resolve_ref_roles(payload)
    assert any("character appearance reference" in i for i in res.ref_instructions), res.ref_instructions
    assert any("match the person's identity and outfit" in i for i in res.ref_instructions)


def test_dispatch_previous_shot_same_frame_zoomed():
    payload = _make_payload(
        labeled_refs=[("previous shot same frame zoomed", b"\x89")],
        ref_roles=["previous_shot_same_frame_zoomed"],
    )
    res = resolve_ref_roles(payload)
    # W3 (2026-06-11): 'reuse the exact frame' 절대 지시 완화 — continuity base,
    # 피사체·포즈는 본문 프롬프트 SOT (S29 sh11 프레임 복제 실측 fix).
    assert any("SAME MOMENT, zoomed-in reframing" in r for r in res.ref_roles)
    assert any("continuity base" in r for r in res.ref_roles)
    assert any("render the subjects and their poses as described in the prompt text" in i
               for i in res.ref_instructions)
    assert any("render the focused region" in i for i in res.ref_instructions)
    assert not any("reuse the exact frame" in r for r in res.ref_roles)


def test_dispatch_previous_shot_same_room():
    payload = _make_payload(
        labeled_refs=[("previous shot same room", b"\x89")],
        ref_roles=["previous_shot_same_room"],
    )
    res = resolve_ref_roles(payload)
    assert any("BACKGROUND from a previous shot (SAME ROOM)" in r for r in res.ref_roles), res.ref_roles
    assert any("use the background, furniture layout, walls, and lighting from image 1 as-is"
               in i for i in res.ref_instructions)
    assert any("do NOT copy any standing/moving people from image 1" in i for i in res.ref_instructions)


def test_dispatch_previous_shot_continuity():
    payload = _make_payload(
        labeled_refs=[("previous shot continuity", b"\x89")],
        ref_roles=["previous_shot_continuity"],
    )
    res = resolve_ref_roles(payload)
    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 from image 1"
               in i for i in res.ref_instructions)
    assert any("do NOT copy characters, people, or their appearances" in i for i in res.ref_instructions)


def test_dispatch_background_chain_ref():
    # W1 (2026-06-11 fresh full E2E 육안 피드백): 'use as-is'/'match ... exactly'
    # 가 카메라 위치·구도까지 강제하던 문구를 environment identity 역할로 완화 —
    # 카메라/프레이밍/액션은 본문 프롬프트가 SOT (S10 문틈/유리 발명 실측 fix).
    payload = _make_payload(
        labeled_refs=[("pre-rendered BACKGROUND chain reference", b"\x89")],
        ref_roles=["background_chain_ref"],
    )
    res = resolve_ref_roles(payload)
    assert any("pre-rendered BACKGROUND environment reference" in r for r in res.ref_roles)
    assert any("layout/material/lighting identity" 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
    )
    assert any("do NOT copy any people from image 1" in i for i in res.ref_instructions)
    # 옛 절대 지시 잔존 금지
    assert not any("use as-is" in r for r in res.ref_roles)
    assert not any("exactly from image 1" in i for i in res.ref_instructions)


def test_dispatch_character_ref():
    payload = _make_payload(
        labeled_refs=[("Image 1 (character reference): generic name brief", b"\x89")],
        ref_roles=["character_ref"],
    )
    res = resolve_ref_roles(payload)
    assert any("character appearance reference" in i for i in res.ref_instructions), 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_dispatch_character_state_ref_with_state_metadata():
    payload = _make_payload(
        labeled_refs=[("character state reference: unconscious", b"\x89")],
        ref_roles=["character_state_ref"],
        ref_role_metadata=[{"state": "unconscious"}],
    )
    res = resolve_ref_roles(payload)
    assert any("state-variant reference (unconscious)" in i for i in res.ref_instructions), res.ref_instructions
    assert any("match the character's body language for this specific state" in i
               for i in res.ref_instructions)


def test_dispatch_character_state_ref_without_state_metadata_falls_back_to_generic():
    """state metadata 가 빈 dict 일 때 generic state directive emit (No Silent Fallback — 명시적 분기)."""
    payload = _make_payload(
        labeled_refs=[("character state reference", b"\x89")],
        ref_roles=["character_state_ref"],
        ref_role_metadata=[{}],
    )
    res = resolve_ref_roles(payload)
    assert any("state-variant reference —" in i for i in res.ref_instructions), res.ref_instructions
    assert not any("state-variant reference (" in i for i in res.ref_instructions), \
        f"빈 state 에 괄호 누설: {res.ref_instructions}"


def test_dispatch_prop_ref():
    payload = _make_payload(
        labeled_refs=[("Image 1 (object reference): generic prop", b"\x89")],
        ref_roles=["prop_ref"],
    )
    res = resolve_ref_roles(payload)
    assert any("include the object shown in image 1" in i for i in res.ref_instructions)


def test_dispatch_background_general():
    payload = _make_payload(
        labeled_refs=[("background general reference", b"\x89")],
        ref_roles=["background_general"],
    )
    res = resolve_ref_roles(payload)
    assert any("background/environment reference" in r for r in res.ref_roles)
    assert any("use the lighting, architecture, and environment mood from image 1"
               in i for i in res.ref_instructions)


def test_dispatch_fallback():
    """fallback 분기 — generic label echo, character/prop 분기 미매치."""
    payload = _make_payload(
        labeled_refs=[("some generic ref text", b"\x89")],
        ref_roles=["fallback"],
    )
    res = resolve_ref_roles(payload)
    assert any("reference image 1: some generic ref text" in i for i in res.ref_instructions)
    assert not any("character appearance reference" in i for i in res.ref_instructions), \
        f"fallback 에서 character 누설: {res.ref_instructions}"
    assert not any("include the object" in i for i in res.ref_instructions), \
        f"fallback 에서 prop 누설: {res.ref_instructions}"


# ── metadata-driven extras (keep_elements / ignore) ─────────────


def test_previous_shot_same_room_emits_keep_and_ignore_from_metadata():
    """SAME ROOM 분기 + metadata 의 keep_elements + ignore 가 inline instruction 으로 emit."""
    payload = _make_payload(
        labeled_refs=[("previous shot SAME ROOM", b"\x89")],
        ref_roles=["previous_shot_same_room"],
        ref_role_metadata=[{
            "keep_elements": [
                {"label": "the dense forest clearing"},
                {"label": "the cold dusk lighting"},
            ],
            "ignore": "the close-up face framing",
        }],
    )
    res = resolve_ref_roles(payload)
    assert any("from image 1: keep the dense forest clearing" in i for i in res.ref_instructions), \
        res.ref_instructions
    assert any("from image 1: keep the cold dusk lighting" in i for i in res.ref_instructions)
    assert any("from image 1: ignore the close-up face framing" in i for i in res.ref_instructions)


def test_previous_shot_same_frame_zoomed_emits_keep_and_ignore_from_metadata():
    payload = _make_payload(
        labeled_refs=[("previous shot SAME FRAME zoomed", b"\x89")],
        ref_roles=["previous_shot_same_frame_zoomed"],
        ref_role_metadata=[{
            "keep_elements": [{"label": "the hand on the table"}],
            "ignore": "the wider room context",
        }],
    )
    res = resolve_ref_roles(payload)
    assert any("from image 1: keep the hand on the table" in i for i in res.ref_instructions)
    assert any("from image 1: ignore the wider room context" in i for i in res.ref_instructions)


def test_previous_shot_same_frame_zoomed_immobilized_subjects_emits_pose_lock():
    """feedback6-C (2026-06-11) + P8 Inc3 (2026-06-23 S12 sh8↔sh13 시신 드리프트
    실측): zoom 프레임에 immobilized (dead/unconscious/severely_injured) 피사체가
    있으면 image1 이 그 피사체의 pose+contact+support surface SOT — 본문 텍스트가
    다른 위치/표면을 암시해도 image1 이 이긴다. mobile 피사체는 W3 완화 유지."""
    payload = _make_payload(
        labeled_refs=[("previous shot SAME FRAME zoomed", b"\x89")],
        ref_roles=["previous_shot_same_frame_zoomed"],
        ref_role_metadata=[{
            "immobilized_subjects": [{"character": "X", "state": "dead"}],
        }],
    )
    res = resolve_ref_roles(payload)
    # P8 Inc3: lock 강화 — image1 wins + has not moved + contact/support surface.
    assert any(
        "immobilized state" in i and "image 1 wins" in i and "has NOT moved" in i
        for i in res.ref_instructions
    ), res.ref_instructions
    assert any(
        "immobilized state" in i and "surface" in i and "hand contacts" in i
        for i in res.ref_instructions
    ), res.ref_instructions
    # P8 Inc3: W3 pose-from-text 절은 immobilized 일 때 standing/moving 한정 —
    # 시신은 본문 SOT 에서 제외 (image1 lock). generic "render the subjects and
    # their poses" 문구는 immobilized 분기에서 미발화.
    assert any("render the poses of any standing or moving subject as described "
               "in the prompt text" in i for i in res.ref_instructions)
    assert not any("render the subjects and their poses as described in the prompt text"
                   in i for i in res.ref_instructions), res.ref_instructions


def test_previous_shot_same_frame_zoomed_without_immobilized_no_pose_lock():
    """feedback6-C 경계: immobilized_subjects metadata 없으면 pose-lock 미발화
    (W3 완화 동작 byte-identical)."""
    payload = _make_payload(
        labeled_refs=[("previous shot SAME FRAME zoomed", b"\x89")],
        ref_roles=["previous_shot_same_frame_zoomed"],
    )
    res = resolve_ref_roles(payload)
    assert not any("immobilized state" in i for i in res.ref_instructions), \
        res.ref_instructions


# ── universal trailing instructions (3 line) ──────────────────────


def test_trailing_universal_directives_always_appended():
    """fallback (가장 minimal 분기) 에서도 3 universal directive emit."""
    payload = _make_payload(
        labeled_refs=[("anything", b"\x89")],
        ref_roles=["fallback"],
    )
    res = resolve_ref_roles(payload)
    assert any("do not copy poses or compositions from reference images" in i for i in res.ref_instructions)
    assert any("do not alter character identities where their face is visible in the scene" in i
               for i in res.ref_instructions)
    assert any("only render what the scene description asks for" in i for i in res.ref_instructions)


# ── multi-ref dispatch (parallel arrays integrity) ────────────────


def test_multi_ref_dispatches_each_role_independently():
    """3 ref parallel list — character + prop + previous_shot_same_room — index-by-index dispatch."""
    payload = _make_payload(
        labeled_refs=[
            ("Image 1 (character reference): name", b"\x89A"),
            ("Image 2 (object reference): prop", b"\x89B"),
            ("Image 3 prev shot SAME ROOM", b"\x89C"),
        ],
        ref_roles=["character_ref", "prop_ref", "previous_shot_same_room"],
        ref_role_metadata=[{}, {}, {}],
        attached_meta=[("character", "C01"), ("prop", "P01"), ("background", "B01")],
    )
    res = resolve_ref_roles(payload)
    # role-1 (character): image 1 directive
    assert any("- use image 1 as character appearance reference" in i for i in res.ref_instructions)
    # role-2 (prop): image 2 directive
    assert any("include the object shown in image 2" in i for i in res.ref_instructions)
    # role-3 (background prev shot SAME ROOM): image 3 directive
    assert any("use the background, furniture layout, walls, and lighting from image 3 as-is" in i
               for i in res.ref_instructions)


# ── REF_ROLE_VALUES sanity (11 enum, no drift) ────────────────────


def test_all_11_enum_values_have_dispatch_branch():
    """REF_ROLE_VALUES 11 enum 각각 resolve_ref_roles 분기 가짐 (drift 차단)."""
    #: ★역할마다 **필요한 metadata** 가 다르다. 빈 dict 로 다 되던 시절의
    #:  가정이었는데, `outfit_ref_explicit` 은 이제 누구의 복장인지를 요구한다
    #:  (없으면 선다 — 그것이 계약이다).
    NEEDS = {"outfit_ref_explicit": {"subject_final_ids": ["C01O02"]}}
    for role in REF_ROLE_VALUES:
        payload = _make_payload(
            labeled_refs=[(f"label for {role}", b"\x89")],
            ref_roles=[role],
            ref_role_metadata=[dict(NEEDS.get(role, {}))],
        )
        # exception 없이 통과 + non-empty result
        res = resolve_ref_roles(payload)
        assert len(res.ref_roles) >= 1, f"role={role}: ref_roles 비어 있음"
        assert len(res.ref_instructions) >= 4, f"role={role}: trailing 3 + 분기 1 이상 필요"
