"""FINDING 9 W2 (Cat2) — base_id_required character_outlook compatibility.

post-W4b surfaced compatibility gap: `subject_reference_policy.policy ==
"base_id_required"` subject 는 t2i_prompt 에 bare `C##` 토큰을 쓴다 (W4b
canonicalization, 정상). 그러나 (a) producer `build_asset_requirements` 는 그
subject 를 `character_outlook`(C##O##) required 로 선언했고, (b) resolver 의
composite regex `(C##)(O##)` 는 bare `C##` 를 미매칭 → base ref 조차 attach 0.

W2 fix = single SOT `required_refs`:
- D1 `build_asset_requirements` — subject_reference_policy 분기로 base_id_required
  subject → `{kind:"character"}` / id_and_outlook_required → `character_outlook`
  유지 / generic_descriptor_allowed → 둘 다 안 emit.
- D2 resolver — `required_refs(kind="character")` base-ref force-attach branch.
- D3 validator — step 3b: required character id exact `("character", id)` 검증.

Gates G1-G6. 4 실패 still = f9478ab3 S1.3 C07O09 / 0357a014 S15.2 C04O06 /
b0a64260 S16.3 C04O06 / 04e1a30a S5.10 C02O03 (전부 base_id_required).
"""
from __future__ import annotations

import pytest

from app.core.ref_contract_validator import RefContractError, validate_attached_refs
from app.core.steps.render_prompt_card import build_asset_requirements
from app.core.subject_reference_policy import SubjectReferencePolicy
from app.services.scene_reference_service import SceneReferenceService


def _policy(subject_id: str, policy: str) -> SubjectReferencePolicy:
    return SubjectReferencePolicy(
        subject_id=subject_id,
        policy_type="identity_reference",
        policy=policy,
        reason="(test fixture)",
    )


# ── G1 ────────────────────────────────────────────────────────────────
def test_g1_base_id_required_emits_character_kind() -> None:
    """base_id_required subject → required_refs 에 kind='character' (C##),
    character_outlook (C##O##) 아님."""
    a = build_asset_requirements(
        visible_entities=["C07"],
        outlook_pairs=[{"character_id": "C07", "outlook_id": "O09"}],
        bg_id=None, is_close_framing=False, background_mode_on=False,
        render_contracts=[],
        policy_map={"C07": _policy("C07", "base_id_required")},
    )
    refs = a["required_refs"]
    assert {"kind": "character", "id": "C07", "policy": "required"} in refs
    assert not any(r["kind"] == "character_outlook" for r in refs)


# ── G2 ────────────────────────────────────────────────────────────────
def test_g2_id_and_outlook_required_keeps_character_outlook() -> None:
    """id_and_outlook_required subject → character_outlook 유지 (회귀 0)."""
    a = build_asset_requirements(
        visible_entities=["C01"],
        outlook_pairs=[{"character_id": "C01", "outlook_id": "O02"}],
        bg_id=None, is_close_framing=False, background_mode_on=False,
        render_contracts=[],
        policy_map={"C01": _policy("C01", "id_and_outlook_required")},
    )
    refs = a["required_refs"]
    assert {"kind": "character_outlook", "id": "C01O02", "policy": "required"} in refs
    assert not any(r["kind"] == "character" for r in refs)


def test_g2b_absent_from_policy_map_defaults_to_id_and_outlook() -> None:
    """policy_map 에서 누락된 subject → default id_and_outlook_required —
    character_outlook 유지 (empty policy_map = staging-not-applicable shot)."""
    a = build_asset_requirements(
        visible_entities=["C01"],
        outlook_pairs=[{"character_id": "C01", "outlook_id": "O02"}],
        bg_id=None, is_close_framing=False, background_mode_on=False,
        render_contracts=[],
        policy_map={},
    )
    refs = a["required_refs"]
    assert {"kind": "character_outlook", "id": "C01O02", "policy": "required"} in refs


# ── G3 ────────────────────────────────────────────────────────────────
def test_g3_generic_descriptor_allowed_emits_neither() -> None:
    """generic_descriptor_allowed subject → character / character_outlook 둘 다 없음."""
    a = build_asset_requirements(
        visible_entities=["C03"],
        outlook_pairs=[{"character_id": "C03", "outlook_id": "O05"}],
        bg_id=None, is_close_framing=False, background_mode_on=False,
        render_contracts=[],
        policy_map={"C03": _policy("C03", "generic_descriptor_allowed")},
    )
    refs = a["required_refs"]
    assert not any(r["kind"] in ("character", "character_outlook") for r in refs)


# ── G4 ────────────────────────────────────────────────────────────────
def test_g4_resolver_attaches_required_character_base_ref() -> None:
    """resolver 가 required_refs(kind='character') 의 base ref 를
    attached_meta ('character', sid) 로 force-attach. t2i_prompt 는 bare C##
    (W4b) — composite regex 미매칭이므로 required_refs 가 유일 attach 신호."""
    svc = SceneReferenceService.__new__(SceneReferenceService)
    visible_entities = [
        {"id": "uuid-c07", "short_id": "C07", "entity_type": "character", "name": "Hero"},
    ]
    payload = svc.resolve_refs_for_prompt(
        t2i_prompt="A wide shot. C07 stands by the window.",
        visible_entities=visible_entities,
        scene_ref_image_map={"uuid-c07": b"c07_face"},
        entity_lookup={},
        required_refs=[{"kind": "character", "id": "C07", "policy": "required"}],
    )
    assert ("character", "C07") in payload.attached_meta
    assert len(payload.labeled_refs) == len(payload.attached_meta)
    assert len(payload.ref_roles) == len(payload.labeled_refs)


# ── G5 ────────────────────────────────────────────────────────────────
def test_g5_validator_step3b_missing_character_raises() -> None:
    """validator step 3b — required character 미첨부 → RefContractError."""
    rpc = {"asset_requirements": {"required_refs": [
        {"kind": "character", "id": "C07", "policy": "required"},
    ]}}
    with pytest.raises(RefContractError, match="C07"):
        validate_attached_refs(
            rpc, [], [], prompt="C07 stands.", is_close_framing=False,
            reference_phrase_kinds=[],
        )


def test_g5_validator_step3b_present_character_passes() -> None:
    """validator step 3b — required character 첨부됨 → 통과."""
    rpc = {"asset_requirements": {"required_refs": [
        {"kind": "character", "id": "C07", "policy": "required"},
    ]}}
    validate_attached_refs(
        rpc,
        labeled_refs=[("character C07 identity", b"x")],
        attached_meta=[("character", "C07")],
        prompt="C07 stands.", is_close_framing=False,
        reference_phrase_kinds=[],
    )


# ── G6 ────────────────────────────────────────────────────────────────
def test_g6_base_id_required_still_reproduction_no_violation() -> None:
    """4 실패 still 재현 (f9478ab3 S1.3 C07O09 형태) — D1→D2→D3 end-to-end:
    base_id_required subject, bare C## t2i_prompt → ref-contract 위반 0."""
    policy_map = {"C07": _policy("C07", "base_id_required")}
    ar = build_asset_requirements(
        visible_entities=["C07"],
        outlook_pairs=[{"character_id": "C07", "outlook_id": "O09"}],
        bg_id=None, is_close_framing=False, background_mode_on=False,
        render_contracts=[],
        policy_map=policy_map,
    )
    # D1
    assert {"kind": "character", "id": "C07", "policy": "required"} in ar["required_refs"]

    svc = SceneReferenceService.__new__(SceneReferenceService)
    prompt = "A medium shot. C07 turns toward the door."
    payload = svc.resolve_refs_for_prompt(
        t2i_prompt=prompt,
        visible_entities=[
            {"id": "uuid-c07", "short_id": "C07", "entity_type": "character", "name": "Hero"},
        ],
        scene_ref_image_map={"uuid-c07": b"c07_face"},
        entity_lookup={},
        required_refs=ar["required_refs"],
    )
    # D2
    assert ("character", "C07") in payload.attached_meta

    # D3 — validator 통과 (RefContractError 없음)
    validate_attached_refs(
        {"asset_requirements": ar},
        labeled_refs=list(payload.labeled_refs),
        attached_meta=list(payload.attached_meta),
        prompt=prompt, is_close_framing=False,
        reference_phrase_kinds=["character"],
    )
