"""FINDING 9 W3 (Cat1) — close framing × ref_usage cross-step 정합.

scene_image_pipeline E2E partial 의 close_ref_usage_violation 12건은 단일 원인이
아니라 2 sub-cause 다 (W0 pin).

sub-cause A (producer, 10건): shot_dependency_t2i 가 shot_staging.framing_scale 를
모른 채 LLM 이 ref_usage 를 선택 → close shot 의 location_ref 에
exact_background / atmosphere_reference 가 붙음. consumer close×ref_usage matrix
가 image-gen 직전 fail-fast.
  W3 fix = deterministic post-process (`_apply_close_framing_ref_usage_policy`),
  close + location_ref present 기준:
    - exact_background    → zoom_in_detail (normalize, 같은 방 배경 연속)
    - atmosphere_reference → location_ref drop (다른 방 — zoom_in_detail 의미모순)
    - zoom_in_detail      → keep
    - invalid/empty       → AppError fail-fast (silent normalize 금지)
  non-close → unchanged.

sub-cause B (consumer, 2건 S9.1/S27.2 + drop 수렴분): producer 가 location_refs=[]
를 정상 emit 한 close shot 이 best_prev_bytes(location_history) 와 결합 시 consumer
matrix 가 'dep entry 부재' 를 'malformed ref_usage' 와 동일 취급 → false-positive.
  W3 fix = `build_prev_shot_background_ref` 가 'dep entry 부재(location_refs=[])'
  를 'malformed ref object' 와 구분 — close + dep entry 부재 → return None.
  실제 declared ref 의 close×ref_usage matrix 는 보존.
"""
from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from app.core.errors import AppError
from app.core.framing_scale import FRAMING_CLOSE, FRAMING_WIDE
from app.core.ref_contract_validator import RefContractError
from app.core.steps.shot_dependency_t2i_step import (
    _apply_close_framing_ref_usage_policy,
    _build_framing_map,
)
from app.services.scene_reference_service import SceneReferenceService


# ── producer fixtures ─────────────────────────────────────────────────
def _loc_ref(ref_usage: str, scene_index: int = 1, shot_index: int = 1) -> dict:
    return {
        "scene_index": scene_index,
        "shot_index": shot_index,
        "reason": "(test fixture)",
        "ref_usage": ref_usage,
        "ignore_elements": "",
        "keep_elements": [],
    }


def _dep(scene_index: int, shot_index: int, location_refs: list) -> dict:
    return {
        "scene_index": scene_index,
        "shot_index": shot_index,
        "location_refs": location_refs,
        "character_refs": [],
    }


# ══ sub-cause A — producer deterministic post-process ══════════════════

# ── G1 ────────────────────────────────────────────────────────────────
def test_g1_close_exact_background_normalized_to_zoom_in_detail() -> None:
    """close + location_ref(exact_background) → ref_usage='zoom_in_detail'."""
    deps = [_dep(12, 5, [_loc_ref("exact_background", 5, 1)])]
    _apply_close_framing_ref_usage_policy(deps, {(12, 5): "close", (5, 1): "wide"})
    assert deps[0]["location_refs"][0]["ref_usage"] == "zoom_in_detail"


# ── G2 ────────────────────────────────────────────────────────────────
def test_g2_close_atmosphere_reference_dropped() -> None:
    """close + location_ref(atmosphere_reference) → location_ref drop.

    atmosphere_reference 는 'different room' 의미 — zoom_in_detail('same frame
    zoomed') 로 강제하면 의미모순. close shot 에서는 ref 자체를 제거.
    """
    deps = [_dep(7, 2, [_loc_ref("atmosphere_reference", 7, 1)])]
    _apply_close_framing_ref_usage_policy(deps, {(7, 2): "close", (7, 1): "wide"})
    assert deps[0]["location_refs"] == []


# ── G3 ────────────────────────────────────────────────────────────────
def test_g3_close_zoom_in_detail_kept() -> None:
    """close + location_ref(zoom_in_detail) → 그대로 유지."""
    deps = [_dep(5, 10, [_loc_ref("zoom_in_detail", 5, 7)])]
    _apply_close_framing_ref_usage_policy(deps, {(5, 10): "close", (5, 7): "close"})
    assert deps[0]["location_refs"][0]["ref_usage"] == "zoom_in_detail"


# ── G4 ────────────────────────────────────────────────────────────────
def test_g4_close_invalid_ref_usage_fail_fast() -> None:
    """close + location_ref 의 ref_usage 가 enum 밖/빈값 → AppError fail-fast.

    silent normalize 금지 — malformed location_ref object 는 fail-fast 대상.
    """
    deps = [_dep(1, 2, [_loc_ref("", 1, 1)])]
    with pytest.raises(AppError) as exc:
        _apply_close_framing_ref_usage_policy(deps, {(1, 2): "close", (1, 1): "wide"})
    assert "close_ref_usage_invalid" in exc.value.code


# ── G5 ────────────────────────────────────────────────────────────────
def test_g5_non_close_exact_background_unchanged() -> None:
    """non-close shot → ref_usage 변경 0 (close 만 정합 대상)."""
    deps = [_dep(3, 4, [_loc_ref("exact_background", 3, 1)])]
    _apply_close_framing_ref_usage_policy(deps, {(3, 4): "wide", (3, 1): "wide"})
    assert deps[0]["location_refs"][0]["ref_usage"] == "exact_background"


# ── G6 ────────────────────────────────────────────────────────────────
def test_g6_close_empty_location_refs_noop() -> None:
    """close + location_refs=[] → no-op, error 없음 (producer-side 처리 없음).

    location_refs=[] 는 producer 의 정상 verdict — sub-cause B (consumer) 영역.
    """
    deps = [_dep(9, 1, [])]
    _apply_close_framing_ref_usage_policy(deps, {(9, 1): "close"})
    assert deps[0]["location_refs"] == []


# ── G7 ────────────────────────────────────────────────────────────────
def test_g7_missing_framing_fail_fast() -> None:
    """dependency shot 이 framing_map(shot_staging)에 없으면 AppError fail-fast."""
    deps = [_dep(99, 9, [_loc_ref("exact_background", 1, 1)])]
    with pytest.raises(AppError) as exc:
        _apply_close_framing_ref_usage_policy(deps, {})
    assert "framing_scale_missing" in exc.value.code


# ── G8 ────────────────────────────────────────────────────────────────
def test_g8_build_framing_map_from_staging_cp() -> None:
    """_build_framing_map: shot_staging cp → {(si,shi): framing_scale}."""
    staging_cp = {
        "data": {
            "shots": [
                {"scene_index": 9, "shot_index": 1, "framing_scale": "close"},
                {"scene_index": 5, "shot_index": 7, "framing_scale": "wide"},
            ]
        }
    }
    fm = _build_framing_map(staging_cp)
    assert fm == {(9, 1): "close", (5, 7): "wide"}


# ══ sub-cause B — consumer empty-list vs malformed 구분 ════════════════


def _make_svc() -> SceneReferenceService:
    """Synthetic SceneReferenceService — build_prev_shot_background_ref 는 이
    test path 에서 DB 미사용 (best_prev_bytes/dep_detail_map/staging argument 만
    read), db=MagicMock() 로 충분."""
    return SceneReferenceService(db=MagicMock(), project_id="p_test")


# ── G9 ────────────────────────────────────────────────────────────────
def test_g9_close_no_dep_entry_returns_none_not_raises() -> None:
    """close + dep entry 부재(location_refs=[]) + best_prev_bytes 존재 →
    return None (matrix false-positive 없음).

    producer 가 location_refs=[] 를 정상 emit 한 close shot 이 location_history
    best_prev_bytes 와 결합 시, consumer 는 prev-shot bg 를 합성하지 않고
    return None — declared no-ref 를 fallback 으로 되살리지 않는다.
    """
    svc = _make_svc()
    still = {"scene_index": 9, "shot_index": 1, "visible_entities_json": "[]"}
    staging = {"framing_scale": FRAMING_CLOSE, "camera_direction": "x"}
    result = svc.build_prev_shot_background_ref(
        best_prev_bytes=b"\x89PNG fake bytes",
        bytes_source_kind="location_history",
        still_data=still,
        visible_entities=[],
        current_location_ids=["L06"],
        dep_scene_id=None,
        stills=[],
        location_scene_history={
            "L06": (None, {"id": "S0", "visible_entities_json": "[]"})
        },
        dep_detail_map={},  # dep entry 부재 = location_refs=[]
        staging=staging,
        state_variant_sids=set(),
        entity_lookup={},
    )
    assert result is None


# ── G10 ───────────────────────────────────────────────────────────────
def test_g10_close_malformed_ref_object_still_raises() -> None:
    """close + declared location_ref object 가 malformed(ref_usage='') → 여전히
    RefContractError. empty-list 와 malformed 는 다른 상태 — matrix 보존."""
    svc = _make_svc()
    still = {"scene_index": 1, "shot_index": 2, "visible_entities_json": "[]"}
    staging = {"framing_scale": FRAMING_CLOSE, "camera_direction": "x"}
    dep_detail_map = {
        "1_2": {"ref_usage": "", "ignore_elements": "", "keep_elements": []}
    }
    with pytest.raises(RefContractError) as exc:
        svc.build_prev_shot_background_ref(
            best_prev_bytes=b"\x89PNG fake bytes",
            bytes_source_kind="location_history",
            still_data=still,
            visible_entities=[],
            current_location_ids=["loc1"],
            dep_scene_id=None,
            stills=[],
            location_scene_history={
                "loc1": (None, {"id": "S0", "visible_entities_json": "[]"})
            },
            dep_detail_map=dep_detail_map,
            staging=staging,
            state_variant_sids=set(),
            entity_lookup={},
        )
    assert "close_ref_usage_violation" in str(exc.value)


# ── G11 ───────────────────────────────────────────────────────────────
def test_g11_non_close_no_dep_entry_still_attaches() -> None:
    """non-close + dep entry 부재 + best_prev_bytes → 기존 location_history
    fallback 유지 (return None 분기는 close 한정, 회귀 0)."""
    svc = _make_svc()
    still = {"scene_index": 1, "shot_index": 2, "visible_entities_json": "[]"}
    staging = {"framing_scale": FRAMING_WIDE, "camera_direction": "x"}
    result = svc.build_prev_shot_background_ref(
        best_prev_bytes=b"\x89PNG fake bytes",
        bytes_source_kind="location_history",
        still_data=still,
        visible_entities=[],
        current_location_ids=["loc1"],
        dep_scene_id=None,
        stills=[],
        location_scene_history={
            "loc1": (None, {"id": "S0", "visible_entities_json": "[]"})
        },
        dep_detail_map={},
        staging=staging,
        state_variant_sids=set(),
        entity_lookup={},
    )
    assert result is not None
