"""outdoor_map_conti — 복잡 구도 판별·맵 기반 플레이트 결정론 로직 테스트."""
from __future__ import annotations

from pathlib import Path
from typing import Any, Dict, List

from app.modules.pipeline.outdoor_map_conti import (
    build_map_bg_prompt,
    judge_complex_shots,
    run_outdoor_map_conti,
)


SPEC = {
    "zone_labels_en": ["yard", "street"],
    "items": [
        {"code": "A1", "name_en": "bus shelter", "placement_en": "on the street edge"},
        {"code": "B2", "name_en": "main gate", "placement_en": "at the yard entry"},
    ],
}
GROUND = {
    "map_zone": "street",
    "anchor_markers": ["A1"],
    "camera_position_en": "Across the street from the shelter.",
    "look_direction_en": "Looking toward the shelter.",
    "in_frame_en": "shelter, road, gate far behind.",
    "moment_context_en": "A police car idles nearby.",
    "rationale_ko": "",
}


def test_judge_missing_defaults_false():
    def fake_llm(step_tag, system, user, schema, **kw):
        return {"items": [{"shot": "S1sh1", "complex": True, "reason_ko": "복잡"}]}

    out = judge_complex_shots(
        shot_desc_by_tag={"S1sh1": "버스 정류장 건너", "S1sh2": "클로즈업"},
        call_structured_fn=fake_llm,
    )
    assert out["S1sh1"]["complex"] is True
    assert out["S1sh2"]["complex"] is False  # 누락 = 안전 기본 false


def test_build_map_bg_prompt_assembly():
    prompt = build_map_bg_prompt(
        ground=GROUND, spec=SPEC, time_of_day_en="night"
    )
    idx = {
        "head": prompt.index("photorealistic BACKGROUND PLATE"),
        "camera": prompt.index("CAMERA: Across the street"),
        "map_point": prompt.index('MAP POINT: this plate is taken in the "street" area'),
        "anchor": prompt.index("- bus shelter: on the street edge"),
        "in_frame": prompt.index("IN FRAME (near to far): shelter"),
        "moment": prompt.index("MOMENT CONTEXT: A police car idles nearby."),
        "time": prompt.index("TIME & LIGHT: night"),
        "invention": prompt.index("INVENTION BOUNDARY"),
        "look": prompt.index("LOOK REFERENCE:"),
        "map_note": prompt.index("SITE PLAN REFERENCE:"),
        "no_anno": prompt.index("ZERO annotations"),
    }
    order = ["head", "camera", "map_point", "anchor", "in_frame", "moment",
             "time", "invention", "look", "map_note", "no_anno"]
    assert sorted(idx, key=idx.get) == order
    # 앵커 마커 코드 자체는 프롬프트 서술에 없음 (ID-free)
    assert "A1" not in prompt


class FakeBgGen:
    def __init__(self):
        self.calls: List[Dict[str, Any]] = []

    def __call__(self, tag, prompt, labeled_refs, out_path: Path) -> Path:
        self.calls.append({"tag": tag, "refs": labeled_refs})
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_bytes(b"plate")
        return out_path


def _assets(tmp_path):
    master = tmp_path / "master.png"
    site = tmp_path / "map.png"
    master.write_bytes(b"m")
    site.write_bytes(b"s")
    return {"g1": {"spec": SPEC, "master_png": master, "map_png": site}}


def test_run_generates_and_skips(tmp_path):
    bg_gen = FakeBgGen()
    grounds = {}

    def ground_fn(tag, spec, shot_desc, scene_text, map_png):
        grounds[tag] = True
        return GROUND

    out = run_outdoor_map_conti(
        conti_target_tags=["S1sh1", "S1sh2", "S3sh1"],
        shot_desc_by_tag={"S1sh1": "정류장", "S1sh2": "클로즈업", "S3sh1": "실내"},
        group_by_tag={"S1sh1": "g1", "S1sh2": "g1"},  # S3sh1 = 야외 아님
        group_assets=_assets(tmp_path),
        complex_by_tag={
            "S1sh1": {"complex": True, "reason_ko": ""},
            "S1sh2": {"complex": False, "reason_ko": ""},
        },
        time_of_day_by_scene={"1": {"time_of_day_en": "night"}},
        scene_texts={1: "씬1 전문"},
        ground_fn=ground_fn,
        bg_gen_fn=bg_gen,
        out_dir=tmp_path / "conti",
    )
    plates = out["map_plates"]
    assert plates["S3sh1"]["skipped_reason"] == "not_outdoor"
    assert plates["S1sh2"]["skipped_reason"] == "not_complex"
    assert plates["S1sh1"]["skipped_reason"] is None
    assert plates["S1sh1"]["plate_path"].endswith("map_plate_S1sh1.png")
    assert grounds == {"S1sh1": True}
    # nb2 참조 = [룩 마스터 + 사이트 맵] 라벨 순서
    refs = bg_gen.calls[0]["refs"]
    assert "LOOK REFERENCE" in refs[0][0] and "SITE PLAN" in refs[1][0]


def test_run_no_canon_assets_skips(tmp_path):
    out = run_outdoor_map_conti(
        conti_target_tags=["S1sh1"],
        shot_desc_by_tag={"S1sh1": "정류장"},
        group_by_tag={"S1sh1": "g9"},  # 자산 없음
        group_assets={},
        complex_by_tag={"S1sh1": {"complex": True, "reason_ko": ""}},
        time_of_day_by_scene={},
        scene_texts={},
        ground_fn=lambda *a, **k: GROUND,
        bg_gen_fn=FakeBgGen(),
        out_dir=tmp_path / "conti",
    )
    assert out["map_plates"]["S1sh1"]["skipped_reason"] == "no_canon_assets"


def test_spec_canon_gate_direct_or_map_or_lane(monkeypatch):
    """Codex 1차 리뷰 BLOCKING-4 회귀 가드 + Stage D5 확장:
    outdoor_place_spec/canon 은 direct/map/lane pipe 어느 플래그로도
    applicable (기존 두 조합 의미 보존 + lane OR)."""
    from app.core import applicability as ap
    from app.core.config import settings
    from app.core.step_manifest import STEP_MANIFEST

    for sid in ("outdoor_place_spec", "outdoor_place_canon"):
        assert STEP_MANIFEST[sid]["applicability"] == (
            "if_outdoor_direct_or_map_or_lane")
    fn = ap.APPLICABILITY_VALIDATORS["if_outdoor_direct_or_map_or_lane"]
    monkeypatch.setattr(settings, "outdoor_direct_compose_enabled", False)
    monkeypatch.setattr(settings, "outdoor_map_conti_enabled", False)
    monkeypatch.setattr(
        settings, "outdoor_lane_pipe_enabled", False, raising=False)
    monkeypatch.setattr(
        settings, "outdoor_lane_plan_enabled", False, raising=False)
    assert fn(None) is False
    # 기존 의미 보존 (BLOCKING-4)
    monkeypatch.setattr(settings, "outdoor_map_conti_enabled", True)
    assert fn(None) is True
    monkeypatch.setattr(settings, "outdoor_map_conti_enabled", False)
    monkeypatch.setattr(settings, "outdoor_direct_compose_enabled", True)
    assert fn(None) is True
    # Stage D5: lane pipe (plan 선행 필수 — pipe 단독은 불충분)
    monkeypatch.setattr(settings, "outdoor_direct_compose_enabled", False)
    monkeypatch.setattr(
        settings, "outdoor_lane_pipe_enabled", True, raising=False)
    assert fn(None) is False
    monkeypatch.setattr(
        settings, "outdoor_lane_plan_enabled", True, raising=False)
    assert fn(None) is True


def _run_map(tmp_path, bg_gen, out_dir, **over):
    # group_assets 미지정 시에만 생성 — 지정 시 파일 재기입으로 내용 변경이
    # 되돌려지는 것 방지
    assets = over.pop("group_assets", None)
    if assets is None:
        assets = _assets(tmp_path)
    kw = dict(
        conti_target_tags=["S1sh1"],
        shot_desc_by_tag={"S1sh1": "정류장"},
        group_by_tag={"S1sh1": "g1"},
        group_assets=assets,
        complex_by_tag={"S1sh1": {"complex": True, "reason_ko": ""}},
        time_of_day_by_scene={},
        scene_texts={1: "t"},
        ground_fn=lambda *a, **k: GROUND,
        bg_gen_fn=bg_gen,
        out_dir=out_dir,
    )
    kw.update(over)
    return run_outdoor_map_conti(**kw)


def test_run_resume_skips_with_matching_fingerprint(tmp_path):
    """지문 일치 sidecar + PNG 존재 → ground 재호출 없이 skip (2차 B3)."""
    out_dir = tmp_path / "conti"
    _run_map(tmp_path, FakeBgGen(), out_dir)
    grounds = {"n": 0}

    def counting_ground(*a, **k):
        grounds["n"] += 1
        return GROUND

    bg_gen2 = FakeBgGen()
    out = _run_map(tmp_path, bg_gen2, out_dir, ground_fn=counting_ground)
    assert bg_gen2.calls == [] and grounds["n"] == 0
    assert out["map_plates"]["S1sh1"]["skipped_reason"] is None
    assert out["map_plates"]["S1sh1"]["ground"]  # sidecar 에서 복원


def test_run_regenerates_on_input_change(tmp_path):
    """맵/마스터 내용·샷 텍스트 변경 → stale 아카이브 후 재생성."""
    out_dir = tmp_path / "conti"
    assets = _assets(tmp_path)
    _run_map(tmp_path, FakeBgGen(), out_dir, group_assets=assets)
    assets["g1"]["map_png"].write_bytes(b"updated-map")
    bg_gen2 = FakeBgGen()
    out = _run_map(tmp_path, bg_gen2, out_dir, group_assets=assets)
    assert len(bg_gen2.calls) == 1  # 재생성
    assert out["map_plates"]["S1sh1"]["skipped_reason"] is None
    assert list(out_dir.glob("map_plate_S1sh1.stale_*.png"))


def test_run_extra_fingerprint_change_regenerates(tmp_path):
    """4차 TEST GAP: corrections/grounding pack/project_config 등 extra
    지문 입력이 바뀌면 맵 플레이트 재생성."""
    out_dir = tmp_path / "conti"
    _run_map(tmp_path, FakeBgGen(), out_dir,
             extra_fingerprint={"grounding_pack": "1", "corrections": ""})
    bg_gen2 = FakeBgGen()
    _run_map(tmp_path, bg_gen2, out_dir,
             extra_fingerprint={"grounding_pack": "2", "corrections": ""})
    assert len(bg_gen2.calls) == 1


def test_run_force_regenerates(tmp_path):
    out_dir = tmp_path / "conti"
    _run_map(tmp_path, FakeBgGen(), out_dir)
    bg_gen2 = FakeBgGen()
    _run_map(tmp_path, bg_gen2, out_dir, force=True)
    assert len(bg_gen2.calls) == 1


def test_run_existing_png_without_sidecar_regenerates(tmp_path):
    bg_gen = FakeBgGen()
    out_dir = tmp_path / "conti"
    out_dir.mkdir()
    (out_dir / "map_plate_S1sh1.png").write_bytes(b"old")
    out = _run_map(tmp_path, bg_gen, out_dir)
    assert len(bg_gen.calls) == 1
    assert out["map_plates"]["S1sh1"]["skipped_reason"] is None
