"""load_outdoor_direct_context 결정론 테스트 (W22 W4a).

flag/cp 게이트, 9절 조립 관통, fallback(캐논 결측·샷 원문 결측) — 전부
tmp_path 체크포인트 파일 기반. fixture 시나리오 중립.
"""

import json
from unittest.mock import patch

from app.services.scene_checkpoint_loaders import load_outdoor_direct_context


def _write_cp(root, project_id, episode_id, step_id, data):
    p = root / project_id / "checkpoints" / "episodes" / episode_id / step_id
    p.mkdir(parents=True, exist_ok=True)
    (p / "manifest.json").write_text(
        json.dumps({"data": data}, ensure_ascii=False), encoding="utf-8"
    )


def _setup(tmp_path, *, canon_ok=True, with_shot_desc=True):
    pid, eid = "SAMPLE_P", "SAMPLE_E"
    master = tmp_path / "master.png"
    master.write_bytes(b"MASTER")
    map_png = tmp_path / "map.png"
    map_png.write_bytes(b"MAP")

    _write_cp(tmp_path, pid, eid, "outdoor_place_spec", {"groups": {
        "g1": {"spec": {
            "layout_narration_en": "yard before structure",
            "zone_labels_en": ["Front Yard"],
            "items": [
                {"code": "P1", "name_en": "front entry gate",
                 "placement_en": "south wall", "kind": "gate",
                 "inferred": True, "evidence": None},
            ],
        }},
    }})
    _write_cp(tmp_path, pid, eid, "outdoor_place_canon", {"groups": {
        "g1": {
            "status": "ok" if canon_ok else "failed",
            "master_png_path": str(master), "map_png_path": str(map_png),
            "master_asset_id": "asset-master", "map_asset_id": "asset-map",
        },
    }})
    _write_cp(tmp_path, pid, eid, "outdoor_shot_grounding", {"groups": {
        "g1": {"map_asset_id": "asset-map", "shots": {
            "3_1": {"status": "ok", "ground": {
                "map_zone": "Front Yard", "anchor_markers": ["P1"],
                "camera_position_en": "inside the gate",
                "look_direction_en": "toward the structure",
                "in_frame_en": "gate, yard", "rationale_ko": "근거",
                "moment_context_en": "the parked vehicle is a patrol car",
            }},
            "3_2": {"status": "failed", "error": "x"},
        }},
    }})
    _write_cp(tmp_path, pid, eid, "shot_validator", {"scenes": [
        {"scene_index": 3, "scene_heading": "SAMPLE 마당 / 밤", "shots": [
            ({"shot_index": 1, "description": "대문으로 들어서는 인물",
              "characters": ["인물A"]} if with_shot_desc
             else {"shot_index": 1, "description": ""}),
            {"shot_index": 2, "description": "실패 샷"},
        ]},
    ]})
    return pid, eid


def _load(tmp_path, pid, eid, flag=True):
    with patch(
        "app.core.config.settings.outdoor_direct_compose_enabled",
        flag, create=True,
    ), patch(
        "app.core.creator_corrections.project_corrections_block",
        return_value="",
    ):
        return load_outdoor_direct_context(str(tmp_path), pid, eid)


def test_flag_off_returns_empty(tmp_path):
    pid, eid = _setup(tmp_path)
    assert _load(tmp_path, pid, eid, flag=False) == {}


def test_missing_cps_returns_empty(tmp_path):
    with patch(
        "app.core.config.settings.outdoor_direct_compose_enabled",
        True, create=True,
    ):
        assert load_outdoor_direct_context(str(tmp_path), "NOPE", "NOPE") == {}


def test_direct_entry_assembled(tmp_path):
    pid, eid = _setup(tmp_path)
    ctx = _load(tmp_path, pid, eid)
    # grounding ok 샷만 (3_2 failed 제외)
    assert set(ctx) == {"3_1"}
    entry = ctx["3_1"]
    assert entry["master_bytes"] == b"MASTER"
    assert entry["map_bytes"] == b"MAP"
    assert entry["master_asset_id"] == "asset-master"
    assert entry["map_asset_id"] == "asset-map"
    assert entry["place_id"] == "g1"
    prompt = entry["prompt"]
    # 9절 관통: SPOT ID-free 치환 + 샷 원문 + moment_context + FREE_CAMERA
    assert 'the "Front Yard" area of the property, by front entry gate' in prompt
    assert "scene_heading: SAMPLE 마당 / 밤" in prompt
    assert "shot: 대문으로 들어서는 인물" in prompt
    assert "people in shot: 인물A" in prompt
    assert "context: the parked vehicle is a patrol car" in prompt
    # v3 (2026-07-11): grounding 카메라 soft guidance — fixture 에 camera 필드
    # 존재 → grounded 절이 free_camera 대체.
    assert "CAMERA (grounded)" in prompt
    assert "inside the gate" in prompt
    assert "toward the structure" in prompt
    assert "YOU choose the camera" not in prompt
    assert "P1" not in prompt  # ID-free 봉인
    # 접점 계약: REF_NOTE 제외(role 지시문 담당), NO_ANNOTATION 유지
    assert "REFERENCES: the attached PHOTOGRAPH" not in prompt
    assert "ZERO overlay annotations" in prompt  # v2: 오버레이 한정 + diegetic signage 보존


def test_canon_failed_group_skipped(tmp_path):
    pid, eid = _setup(tmp_path, canon_ok=False)
    assert _load(tmp_path, pid, eid) == {}


def test_shot_without_description_skipped(tmp_path):
    """샷 원문 없이 직행 프롬프트를 만들면 발명 — 기존 경로 fallback."""
    pid, eid = _setup(tmp_path, with_shot_desc=False)
    assert _load(tmp_path, pid, eid) == {}


def test_camera_fields_missing_skips_direct(tmp_path):
    """v3 (2026-07-11): grounded camera 필드 결손 → free_camera 조용 강등
    금지 — 해당 샷 direct 제외(기존 경로 fallback)."""
    pid, eid = _setup(tmp_path)
    # grounding 에서 카메라 필드 제거
    import json as _json
    gp = (tmp_path / pid / "checkpoints" / "episodes" / eid /
          "outdoor_shot_grounding" / "manifest.json")
    d = _json.loads(gp.read_text(encoding="utf-8"))
    g = d["data"]["groups"]["g1"]["shots"]["3_1"]["ground"]
    g.pop("camera_position_en", None)
    gp.write_text(_json.dumps(d, ensure_ascii=False), encoding="utf-8")
    assert _load(tmp_path, pid, eid) == {}


def test_direct_entry_carries_prompt_version(tmp_path):
    pid, eid = _setup(tmp_path)
    ctx = _load(tmp_path, pid, eid)
    assert ctx["3_1"]["prompt_version"] == "3"
