"""상상해서 만든 것에 사진을 요구하지 않는다 — 대상별 판정이 **검색보다 먼저**.

## 무엇이 결함이었나 (실측 2026-09-05)

2419년을 배경으로 한 대본에서 `outdoor_structure_form_reference` 가 필수 그룹
`bg_mine_complex` 를 `쓸 만한 후보 없음 (VLM 선택 0)` 로 실패시키고 파이프라인을
세웠다. 재개해도 같은 자리에서 또 섰다.

원인은 두 겹이었다.

1. **사진이 존재할 수 없는 대상에 사진을 요구했다.** 사전조사가 두 문항 모두
   「양쪽 심판 근거 미발견 — 폐기」로 끝났는데도 검색이 나갔다. 2419년 폐허의
   실제 사진은 세상에 없다.
2. 시대 딱지가 두 시대를 합친 문자열(`2026년 현대와 2419년 원미래`)이라
   질의가 `숲 2026년 현대와 2419년 원미래` 로 나갔다 — found 3, 후보 8장,
   VLM 선택 0.

## 계약 (Codex DESIGN_REVIEW 2026-09-05)

고증(외부 실사 정본)과 일관성(같은 것이 매번 같게 보이기)은 **직교**한다.

- 관객이 알아볼 수 있는 것(실재하거나 널리 본 작품이 모습을 정해 둔 것)
  → 사진을 찾아 잠근다
- 이 이야기가 지어낸 것 → **사진을 안 산다.** 실패가 아니라 다른 확정이고,
  일관성 정본은 하류 `outdoor_structure_seed` 가 **생성**으로 만들어 잠근다

★조사 채택 수로 대신하지 않는다 (Codex BLOCK) — 그것은 「형태 문항에 사실을
 채택했나」이지 「관객이 알아볼 시각 정본이 있나」가 아니다.
★모르는 값을 조용히 생성으로 접지 않는다 — 그러면 실사 정본이 있는 대상까지
 사진 없이 내려간다.
"""
from __future__ import annotations

from pathlib import Path
from typing import Any, Dict

import pytest

import app.core.steps.outdoor_structure_form_reference_step as fr
import app.core.steps.outdoor_structure_seed_step as sd


def _step() -> Any:
    st = fr.OutdoorStructureFormReferenceStep.__new__(
        fr.OutdoorStructureFormReferenceStep)
    st.project_id, st.episode_id = "p", "e"
    st.project_config = {}
    return st


def _brief(**over: Any) -> Dict[str, Any]:
    base = {
        "source_language": "한국어",
        "search_directive_native": "x" * 60,
        "search_terms_native": ["숲", "산림", "삼림"],
        "language_lock_native": "한국어로만 검색하라",
        "visual_authority": "observable_real",
        "authority_reason_ko": "실재하는 숲이다",
        "search_period_native": "2026년 현대",
    }
    base.update(over)
    return base


def _run(monkeypatch, brief: Dict[str, Any], tmp_path: Path,
         search_spy: list) -> Dict[str, Any]:
    """`_run_group` 을 브리핑만 바꿔 부른다 — 검색이 실제로 나가는지 본다."""
    monkeypatch.setattr(
        "app.modules.llm.llm_client.call_structured",
        lambda *a, **k: brief)

    def _search(*a, **k):
        search_spy.append((a, k))
        return {"images": [], "queries": []}

    monkeypatch.setattr(
        "app.modules.pipeline.search_grounded_ref.search_reference_images",
        _search)
    return _step()._run_group(
        group_id="bg_mine_complex", structure_desc="폐광 갱도 복합체",
        world_facts_block="지역/시대", source_text="원문", client=object(),
        candidate_dir=tmp_path)


# ── ① 창작 대상 — 검색을 아예 안 산다 ────────────────────────────────


def test_지어낸_것에는_검색을_안_산다(monkeypatch, tmp_path):
    """★핵심. 실패가 아니라 **다른 확정**이고, 유료 검색이 0회여야 한다."""
    spy: list = []
    rec = _run(monkeypatch,
               _brief(visual_authority="generative_authority",
                      authority_reason_ko="이 이야기가 지어낸 폐허다",
                      search_terms_native=[], search_period_native=""),
               tmp_path, spy)

    assert rec["status"] == fr.GENERATIVE_STATUS, rec
    assert rec["status"] != "failed", "생성 확정을 실패로 세면 파이프라인이 선다"
    assert spy == [], "지어낸 것에 검색을 샀다"
    assert rec["candidate_count"] == 0
    assert rec["authority_reason_ko"], "판정 근거를 안 남겼다"


def test_실재하는_것은_그대로_검색한다(monkeypatch, tmp_path):
    """양성 확인 — 판정이 생겼다고 실사 경로가 막히면 안 된다."""
    spy: list = []
    _run(monkeypatch, _brief(), tmp_path, spy)
    assert len(spy) == 1, "실재 대상인데 검색이 안 나갔다"


# ── ② 모르는 값·모순은 **명시 실패** ─────────────────────────────────


@pytest.mark.parametrize("bad", ["", "unresolved", "maybe", None])
def test_모르는_판정은_조용히_생성으로_안_접는다(monkeypatch, tmp_path, bad):
    """★조용히 접으면 실사 정본이 있는 대상까지 사진 없이 내려간다."""
    from app.core.errors import AppError

    spy: list = []
    with pytest.raises(AppError) as ei:
        _run(monkeypatch, _brief(visual_authority=bad), tmp_path, spy)
    assert ei.value.code == "structure_form_ref.authority_unresolved"
    assert spy == [], "판정이 서기 전에 검색을 샀다"


def test_외부_정본인데_검색어가_없으면_선다(monkeypatch, tmp_path):
    """사진을 찾겠다고 해 놓고 질의가 없으면 계약 위반이다."""
    from app.core.errors import AppError

    spy: list = []
    with pytest.raises(AppError) as ei:
        _run(monkeypatch, _brief(search_terms_native=[]), tmp_path, spy)
    assert ei.value.code == "structure_form_ref.no_search_terms"
    assert spy == []


# ── ③ 하류: 생성 확정 그룹에서 안 서고 가짜 계보를 안 만든다 ──────────


def test_seed_는_생성_확정_그룹에서_안_선다():
    """★필수 그룹이라도 `generative` 면 「참조 결손」이 아니다."""
    st = sd.OutdoorStructureSeedStep.__new__(sd.OutdoorStructureSeedStep)
    refs, asset = st._resolve_form_ref(
        gid="bg_mine_complex",
        entry={"status": fr.GENERATIVE_STATUS,
               "authority_reason_ko": "지어낸 폐허"},
        mandatory=True)
    assert refs == [] and asset is None, "가짜 참조·계보를 만들었다"


def test_seed_는_진짜_참조_결손에서는_그대로_선다():
    """양성 확인 — 사진이 있어야 하는 그룹의 결손은 여전히 막는다."""
    from app.core.errors import AppError

    st = sd.OutdoorStructureSeedStep.__new__(sd.OutdoorStructureSeedStep)
    with pytest.raises(AppError) as ei:
        st._resolve_form_ref(gid="bg_real_place",
                             entry={"status": "failed"}, mandatory=True)
    assert ei.value.code == "structure_seed.form_ref_missing"


# ── ④ 계약이 지문에 실린다 ───────────────────────────────────────────


def test_판정_계약이_지문에_실린다(monkeypatch):
    """안 실으면 옛 완료 CP 가 새 계약인 것처럼 재사용된다."""
    st = _step()
    before = st._config_hash()
    monkeypatch.setattr(fr, "VISUAL_AUTHORITY_CONTRACT_VERSION", "9.999")
    assert st._config_hash() != before, "판정 계약이 바뀌었는데 지문이 그대로다"


# ── ④ 널리 알려진 작품의 정본 — 심판 지시문이 달라야 한다 ────────────
#
# ★Codex BLOCK 2 (2026-09-08). v13 은 `established_visual_canon` 을 외부
#  권위로 **허용해 놓고**, 심판에게는 `pick_system` 하나만 줬다. 그 지시문은
#  「one-off signature design 은 탈락」·「사진이 아니면 탈락(그림·렌더·
#  포스터)」이라고 적혀 있다. 널리 알려진 만화의 고유 구조물은 바로 이
#  분기의 정상 입력인데, **그 작품의 공식 도판조차 반드시 탈락한다.**

class TestCanonAuthorityReachesTheJudge:
    def test_권위마다_다른_심판_지시문을_쓴다(self):
        from app.modules.pipeline.search_grounded_ref import load_pick_system

        real = load_pick_system(visual_authority="observable_real")
        canon = load_pick_system(visual_authority="established_visual_canon")
        assert real != canon, "두 권위가 같은 심판 지시문을 쓴다"

    def test_실물_지시문은_그림을_탈락시키고_정본_지시문은_안_그런다(self):
        """★글자로 뜻을 판단하지 않는다 — **어느 파일이 실렸는가**를 본다.
        두 파일은 서로 반대 계약이라 파일이 갈리는 것이 곧 계약이 갈리는 것.
        """
        from app.modules.pipeline.search_grounded_ref import (
            _PICK_SYSTEM_STEM, load_pick_system,
        )

        assert _PICK_SYSTEM_STEM["observable_real"] == "pick_system"
        assert _PICK_SYSTEM_STEM["established_visual_canon"] == \
            "pick_system_canon"
        # 정본 지시문은 실제로 존재해야 한다 — 없으면 FileNotFoundError.
        assert load_pick_system(
            visual_authority="established_visual_canon").strip()

    def test_모르는_권위는_조용히_실물로_접지_않는다(self):
        from app.modules.pipeline.search_grounded_ref import load_pick_system

        with pytest.raises(ValueError):
            load_pick_system(visual_authority="generative_authority")
        with pytest.raises(ValueError):
            load_pick_system(visual_authority="")

    def test_스텝이_권위를_심판까지_운반한다(self, monkeypatch, tmp_path):
        """★★★끝점 — `_run_group` 이 심판에게 **무엇을 실었는지** 본다.
        여기서 끊기면 정본 대상이 실물 지시문으로 판정된다."""
        from app.modules.pipeline.search_grounded_ref import load_pick_system

        seen: list = []

        def _pick(_tag, system, _parts, _schema, **_kw):
            seen.append(system)
            return {"verdicts": [{"index": 1, "usable": True, "score": 90,
                                  "reason_ko": "ok"}], "chosen_index": 1}

        brief = _brief(visual_authority="established_visual_canon",
                       authority_reason_ko="널리 본 작품이 모습을 정해 뒀다")
        monkeypatch.setattr(
            "app.modules.llm.llm_client.call_structured",
            lambda tag, *a, **k: brief if tag == "structure_form_ref_brief"
            else _pick(tag, *a, **k))
        monkeypatch.setattr(
            "app.modules.pipeline.search_grounded_ref.search_reference_images",
            lambda *a, **k: {"images": [], "queries": []})

        # 후보가 0장이면 심판 앞에서 끝난다 — 그건 이 시험의 관심이 아니다.
        # 그래서 후보 0으로 끝나더라도 **심판이 불렸다면** 무엇을 받았는지만 본다.
        try:
            _step()._run_group(
                group_id="bg_known_work", structure_desc="그 작품의 구조물",
                world_facts_block="지역/시대", source_text="원문",
                client=object(), candidate_dir=tmp_path)
        except Exception:
            pass

        if seen:
            want = load_pick_system(
                visual_authority="established_visual_canon")
            assert seen[0] == want, (
                "정본 대상인데 실물 심판 지시문이 실렸다")
