"""render_prompt_card lookup for single-scene regen — patch on top of Task 3.

Defect: generate_single_scene_image 의 still_data 에 render_prompt_card 가 누락되어
        validate_attached_refs(rpc=None, ...) 호출 → required_refs 검사 skip
        → S13_Shot6 같은 한글 자연어 prompt + character refs 부재 silent 통과.

Fix: scene_detail/manifest.json 에서 (scene_index, shot_index) 로 RPC lookup,
     still_data 에 inject. lookup 실패 시 RefContractError fail-fast (silent skip 금지).

5 가드 (사용자):
1. lookup helper 자체 — file 부재 / shot 미매칭 → RefContractError
2. _build_single_scene_prompt_and_refs — RPC lookup 후 still_data 에 inject
3. lookup 실패 시 RefContractError propagate (silent skip 금지)
4. RPC 정상 inject + required character ref 부재 → RefContractError fail-fast
5. RPC 정상 inject + required character ref 첨부 → 통과 (false positive 차단)
"""
from __future__ import annotations

import json
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from app.core.ref_contract_validator import RefContractError


# ---------------------------------------------------------------------------
# lookup helper (module-level) — 단위 검증
# ---------------------------------------------------------------------------


def test_lookup_rpc_missing_manifest_file_fail_fast(tmp_path, monkeypatch):
    """manifest.json 자체가 부재 → RefContractError (운영자-readable msg)."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    with pytest.raises(RefContractError) as exc:
        lookup_render_prompt_card(
            project_id="pid_a",
            episode_id="eid_a",
            scene_index=13,
            shot_index=6,
        )
    msg = str(exc.value)
    assert "scene_detail" in msg.lower()
    assert "render_prompt_card" in msg.lower()
    assert "rerun scene_detail" in msg.lower() or "custom prompt" in msg.lower()


def test_lookup_rpc_shot_not_in_manifest_fail_fast(tmp_path, monkeypatch):
    """manifest 존재 + (scene_index, shot_index) 미매칭 → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 1, "_shot_index": 1, "render_prompt_card": {"asset_requirements": {}}},
            {"scene_index": 13, "_shot_index": 5, "render_prompt_card": {"asset_requirements": {}}},
        ]}
    }))

    with pytest.raises(RefContractError) as exc:
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )
    msg = str(exc.value)
    assert "13" in msg and "6" in msg
    assert "rerun scene_detail" in msg.lower() or "custom prompt" in msg.lower()


def test_lookup_rpc_returns_card_dict(tmp_path, monkeypatch):
    """정상 lookup → dict 반환 (asset_requirements 포함)."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    expected_rpc = {
        "asset_requirements": {
            "required_refs": [
                {"kind": "character_outlook", "id": "C01O02", "policy": "required"},
            ]
        },
        "render_strategy": {"mode": "scene"},
    }
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 13, "_shot_index": 6, "render_prompt_card": expected_rpc},
        ]}
    }))

    rpc = lookup_render_prompt_card(
        project_id="pid_a", episode_id="eid_a",
        scene_index=13, shot_index=6,
    )
    assert rpc == expected_rpc


def test_lookup_rpc_missing_render_prompt_card_field_fail_fast(tmp_path, monkeypatch):
    """shot 항목은 있는데 render_prompt_card 키 자체 부재 → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 13, "_shot_index": 6},  # render_prompt_card 부재
        ]}
    }))

    with pytest.raises(RefContractError):
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )


# ──────────────────────────────────────────────────────────────────────
# Codex iter1 IMPORTANT — manifest structure type guards
# (raw AttributeError → RefContractError 변환 일관성)
# ──────────────────────────────────────────────────────────────────────


def _write_manifest(tmp_path, payload):
    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True, exist_ok=True)
    (mf_dir / "manifest.json").write_text(json.dumps(payload))


def test_lookup_rpc_root_not_dict_fail_fast(tmp_path, monkeypatch):
    """manifest root 가 list (or 다른 타입) → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))
    _write_manifest(tmp_path, [1, 2, 3])  # root list (not dict)

    with pytest.raises(RefContractError) as exc:
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )
    assert "malformed" in str(exc.value).lower() or "structure" in str(exc.value).lower()


def test_lookup_rpc_data_not_dict_fail_fast(tmp_path, monkeypatch):
    """data 키가 list → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))
    _write_manifest(tmp_path, {"data": [1, 2]})  # data not dict

    with pytest.raises(RefContractError):
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )


def test_lookup_rpc_scenes_not_list_fail_fast(tmp_path, monkeypatch):
    """scenes 가 dict → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))
    _write_manifest(tmp_path, {"data": {"scenes": {"k": "v"}}})  # scenes not list

    with pytest.raises(RefContractError):
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )


def test_lookup_rpc_scene_item_not_dict_fail_fast(tmp_path, monkeypatch):
    """scenes 의 item 이 string → RefContractError."""
    from app.core import config as _cfg
    from app.services.scene_generation_coordinator import lookup_render_prompt_card

    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))
    _write_manifest(tmp_path, {"data": {"scenes": ["not-a-dict"]}})

    with pytest.raises(RefContractError):
        lookup_render_prompt_card(
            project_id="pid_a", episode_id="eid_a",
            scene_index=13, shot_index=6,
        )


# ---------------------------------------------------------------------------
# _build_single_scene_prompt_and_refs integration — RPC inject 검증
# ---------------------------------------------------------------------------


@pytest.fixture
def coord():
    from app.services.scene_generation_coordinator import SceneGenerationCoordinator
    instance = SceneGenerationCoordinator.__new__(SceneGenerationCoordinator)
    instance._db = MagicMock()
    instance._project_id = "pid_a"
    instance._actor_id = "u_test"
    instance._logger = MagicMock()
    instance._persistence_svc = MagicMock()
    instance._reference_svc = MagicMock()
    instance._validation_svc = MagicMock()
    instance._variation_svc = MagicMock()
    instance._provenance_svc = MagicMock()
    return instance


def _make_still():
    still = MagicMock()
    still.camera_json = "{}"
    still.scene_index = 13
    still.still_index = 0
    still.shot_index = 6
    return still


def _ctx_for_helper(still):
    return {
        "entity_lookup": {"c01": {"id": "c01", "short_id": "C01", "entity_type": "character"}},
        "ref_image_map": {"c01": b"face"},
        "scene_paths_by_index_by_id": {},
        "location_scene_history": {},
        "background_chain_bg_map": {},
        "dep_detail_map": {},
        "staging": {"camera_direction": "extreme close-up", "framing_scale": "close"},
        "cached_style_context": "STYLE",
        "cached_entity_text_map": {"C01": "young woman"},
        "stills": [still],
    }


def test_build_single_inject_rpc_into_still_data(coord, tmp_path, monkeypatch):
    """가드 2: _build_single_scene_prompt_and_refs 가 lookup 후 still_data 에 RPC inject.

    inject 결과 validate_attached_refs(rpc=<dict>, ...) 호출됨 (rpc=None 아님).
    """
    from app.core import config as _cfg
    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    rpc = {
        "asset_requirements": {
            "required_refs": [
                {"kind": "character_outlook", "id": "C01O02", "policy": "required"},
            ]
        },
    }
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 13, "_shot_index": 6, "render_prompt_card": rpc},
        ]}
    }))

    still = _make_still()
    still_data = {
        "scene_index": 13,
        "shot_index": 6,
        "still_frame_prompt": "한글 자연어 prompt",
        "beat_title": "",
    }

    coord.build_single_still_context = MagicMock(return_value=_ctx_for_helper(still))

    captured_rpc = {}

    def _spy_validate(rpc_arg, labeled_refs, attached_meta, prompt, *, is_close_framing, chain_bg_lookup=None, reference_phrase_kinds=None):
        captured_rpc["rpc"] = rpc_arg
        captured_rpc["attached_meta"] = attached_meta
        captured_rpc["chain_bg_lookup"] = chain_bg_lookup

    with patch(
        "app.services.scene_generation_coordinator.build_scene_attached_refs",
        return_value=("MOCK_FULL", [("character C01O02 in outfit", b"x")], [("character_outlook", "C01O02")], [], None),  # P0: 5-tuple
    ), patch(
        "app.services.scene_generation_coordinator.load_project_llm_config",
        return_value={},
    ), patch(
        "app.core.ref_contract_validator.validate_attached_refs",
        side_effect=_spy_validate,
    ):
        coord._build_single_scene_prompt_and_refs(
            still=still,
            episode_id="eid_a",
            still_data=still_data,
            visible_entities=[{"id": "c01", "short_id": "C01", "entity_type": "character"}],
            ref_image_map={"c01": b"face"},
        )

    assert captured_rpc.get("rpc") == rpc, (
        "validate_attached_refs must receive RPC injected from manifest, not None"
    )


def test_build_single_propagates_lookup_failure(coord, tmp_path, monkeypatch):
    """가드 3: lookup 실패 시 RefContractError propagate (silent skip 금지)."""
    from app.core import config as _cfg
    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))
    # manifest 부재

    still = _make_still()
    still_data = {
        "scene_index": 13, "shot_index": 6,
        "still_frame_prompt": "x", "beat_title": "",
    }
    coord.build_single_still_context = MagicMock(return_value=_ctx_for_helper(still))

    with patch(
        "app.services.scene_generation_coordinator.build_scene_attached_refs",
        return_value=("MOCK_FULL", [], [], [], None),  # P0: 5-tuple
    ), patch(
        "app.services.scene_generation_coordinator.load_project_llm_config",
        return_value={},
    ):
        with pytest.raises(RefContractError) as exc:
            coord._build_single_scene_prompt_and_refs(
                still=still,
                episode_id="eid_a",
                still_data=still_data,
                visible_entities=[],
                ref_image_map={},
            )
    assert "render_prompt_card" in str(exc.value).lower()


def test_build_single_required_refs_missing_fail_fast(coord, tmp_path, monkeypatch):
    """가드 4: RPC 정상 inject + required character ref 부재 → RefContractError 422.

    S13_Shot6 회귀 — 한글 prompt 에 C##O## 없음 + visible_entities 기반 매칭 0
    → labeled_refs=[] 이지만 required_refs.character_outlook=[C01O02, C02O03]
    → fail-fast.
    """
    from app.core import config as _cfg
    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    rpc = {
        "asset_requirements": {
            "required_refs": [
                {"kind": "character_outlook", "id": "C01O02", "policy": "required"},
                {"kind": "character_outlook", "id": "C02O03", "policy": "required"},
            ]
        },
    }
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 13, "_shot_index": 6, "render_prompt_card": rpc},
        ]}
    }))

    still = _make_still()
    still_data = {
        "scene_index": 13, "shot_index": 6,
        "still_frame_prompt": "자신을 붙잡은 혜수의 얼굴을 멍하게 올려다보는 수리영의 눈물 맺힌 눈동자 클로즈업.",
        "beat_title": "",
    }
    coord.build_single_still_context = MagicMock(return_value=_ctx_for_helper(still))

    with patch(
        "app.services.scene_generation_coordinator.build_scene_attached_refs",
        return_value=("MOCK_FULL_PROMPT", [], [], [], None),  # P0: 5-tuple
    ), patch(
        "app.services.scene_generation_coordinator.load_project_llm_config",
        return_value={},
    ):
        with pytest.raises(RefContractError) as exc:
            coord._build_single_scene_prompt_and_refs(
                still=still,
                episode_id="eid_a",
                still_data=still_data,
                visible_entities=[
                    {"id": "c01", "short_id": "C01", "entity_type": "character"},
                    {"id": "c02", "short_id": "C02", "entity_type": "character"},
                ],
                ref_image_map={"c01": b"face1", "c02": b"face2"},
            )
    assert exc.value.status_code == 422
    assert "character_outlook" in str(exc.value).lower() or "character" in str(exc.value).lower()


def test_build_single_required_refs_present_passes(coord, tmp_path, monkeypatch):
    """가드 5: RPC inject + required character ref 첨부 → 통과 (false positive 차단)."""
    from app.core import config as _cfg
    monkeypatch.setattr(_cfg.settings, "projects_dir", str(tmp_path))

    mf_dir = tmp_path / "pid_a" / "checkpoints" / "episodes" / "eid_a" / "scene_detail"
    mf_dir.mkdir(parents=True)
    rpc = {
        "asset_requirements": {
            "required_refs": [
                {"kind": "character_outlook", "id": "C01O02", "policy": "required"},
            ]
        },
    }
    (mf_dir / "manifest.json").write_text(json.dumps({
        "data": {"scenes": [
            {"scene_index": 13, "_shot_index": 6, "render_prompt_card": rpc},
        ]}
    }))

    still = _make_still()
    still_data = {
        "scene_index": 13, "shot_index": 6,
        "still_frame_prompt": "C01O02 close-up", "beat_title": "",
    }
    coord.build_single_still_context = MagicMock(return_value=_ctx_for_helper(still))

    with patch(
        "app.services.scene_generation_coordinator.build_scene_attached_refs",
        return_value=("MOCK_FULL", [("character C01O02 in outfit", b"x")], [("character_outlook", "C01O02")], [], None),  # P0: 5-tuple
    ), patch(
        "app.services.scene_generation_coordinator.load_project_llm_config",
        return_value={},
    ):
        full, refs, _meta, _pl = coord._build_single_scene_prompt_and_refs(  # P0: 4-tuple
            still=still,
            episode_id="eid_a",
            still_data=still_data,
            visible_entities=[{"id": "c01", "short_id": "C01", "entity_type": "character"}],
            ref_image_map={"c01": b"face"},
        )
    assert full == "MOCK_FULL"
    assert any("C01O02" in lbl for lbl, _ in refs)
