"""밀폐 공간 계약에 **탈것 전용 낱말이 없다** (2026-08-27, 감사 1-D).

`confined_structure=true` 로 판별된 샷에 이 문안이 나갔다:

    Before you place anyone, count what the background shows: how many
    steering wheels or control surfaces, how many seats and which way
    each faces …

판별은 「좁고 복잡한 실내」 일반인데 문안은 **자동차 캐빈 전용**이다.
자전거 정비소·엘리베이터 같은 공간에서는 없는 조종 장치를 세라는 말이
되고, 없는 것을 그리라는 암시가 된다.

★**더 나쁜 것은 판정 프롬프트에도 같은 문구가 있었다는 점이다.** 그리는
 쪽과 재는 쪽이 같이 기울면 재시도해도 안 걸린다 — 그래서 **한 커밋에서
 둘 다** 올린다.

실측(records 전수): 그 절이 나간 프롬프트 **42건**. 대부분은 차·버스·
기차처럼 조종 장치가 실제로 있는 곳이지만 그 사이에 섞여 있었다:

    Inside the cramped work bay of a bicycle repair shop …
    Inside the compact prosecution-building elevator …
    Inside the compact radio studio at the broadcast microphone …

> 감사 보고서는 「7샷 중 4샷」이라 적었는데 그 판(골목 끝)에서는 **1건**
> 이다. 결함은 실재하지만 그 수치는 재현되지 않는다.
"""

import re

from app.modules.pipeline.multiroll_gemini import (
    STILL_JUDGE_PACK_VERSION,
    resolve_judge_pack_version,
)
from app.modules.pipeline.still_recipe import (
    GEOM_AUTHORITY_PROMPT_VERSION,
    resolve_prompt_version,
)
from app.modules.prompt_loader import load_prompt

# 탈것 하나를 지목하는 낱말. **업종·기종 이름을 계약에 박지 않는다.**
#
# ★**처음 목록이 덜 잡았다** (2026-08-27 Codex BLOCK). `steering wheel`
#  만 보면 bare `wheel`("which side of the wheel")과 `which seat` 이
#  그대로 남는다 — 같은 파일의 다른 두 줄에 실제로 남아 있었다.
#  **한 자리를 고치고 그 낱말만 검사하면 나머지 자리를 못 본다.**
#
# ★**낱말 경계로 본다** (2026-08-27, 두 번째 판). `"helm" in text` 는
#  `helmet` 을 잡는다 — 판정 팩의 "costume head, mask, helmet" 이 실제로
#  걸렸다. substring 으로 뜻을 재면 이런 오탐이 나고, 오탐을 피하려고
#  목록을 줄이면 이번엔 진짜를 놓친다.
#
# ★★**목록에 `control surface` 가 없었다** (2026-08-27, 세 번째 판).
#  1-D 의 **핵심 어구**인데 빠져 있어서, 그 낱말이 되살아나도
#  이 시험은 초록이었다. 실제로 v6 팩의 `critique` 를 훑었더니
#  "a duplicated control surface" 가 그대로인데 `ok` 로 나왔다.
#  **내가 고친 문안을 내 시험이 안 지키고 있었다.**
_VEHICLE = ("steering wheel", "car cabin", "cockpit", "wheelhouse",
            "the vehicle has", "which seat", "which side of the wheel",
            "dashboard", "helm", "control surface", "control surfaces",
            "wrong seat", "gear lever", "yoke")
_VEHICLE_RE = {w: re.compile(r"(?<!\w)" + re.escape(w) + r"(?!\w)", re.I)
               for w in _VEHICLE}


def _vehicle_words_in(text: str) -> list:
    return [w for w, rx in _VEHICLE_RE.items() if rx.search(text)]


# bare `wheel` — 위 구절에 안 걸리는 모양이 남을 수 있다.
_BARE_WHEEL = re.compile(r"\bwheels?\b", re.I)
# ★설비를 **열거**하는 것도 종류를 좁힌다. 통칭 이행 래칫과 같은 논리 —
#  열거는 통칭을 여러 낱말로 펼친 것이고 그것들을 그리라는 암시가 된다.
_ENUM = re.compile(
    r"\b(panels?|controls?|seats?|mirrors?|windows?)\b\s*,\s*\w+\s*,",
    re.I)


def _gen() -> str:
    return load_prompt(
        "still_recipe", "stage_head_geom",
        version=resolve_prompt_version(GEOM_AUTHORITY_PROMPT_VERSION))


def _judge() -> str:
    return load_prompt(
        "multiroll_judge", "judge_still",
        version=resolve_judge_pack_version(STILL_JUDGE_PACK_VERSION))


def test_the_generator_names_no_vehicle():
    low = _gen().lower()
    found = _vehicle_words_in(low)
    assert not found, f"생성 계약에 탈것 전용 낱말이 남았다: {found}"
    m = _BARE_WHEEL.search(low)
    assert not m, f"생성 계약에 바퀴가 남았다: …{low[max(0,m.start()-40):m.end()+40]}…"


def test_the_generator_does_not_enumerate_fittings():
    """★열거는 종류를 좁힌다 — 통칭 이행 래칫과 같은 논리."""
    m = _ENUM.search(_gen())
    assert not m, f"생성 계약이 설비를 열거한다: {m.group(0)!r}"


def test_the_judge_names_no_vehicle():
    """★**재는 쪽도 같이 고친다.** 한쪽만 고치면 판정이 옛 기준으로
    계속 반려해 재시도가 돈다."""
    low = _judge().lower()
    found = _vehicle_words_in(low)
    assert not found, f"판정 계약에 탈것 전용 낱말이 남았다: {found}"
    m = _BARE_WHEEL.search(low)
    assert not m, f"판정 계약에 바퀴가 남았다: …{low[max(0,m.start()-40):m.end()+40]}…"
    m = _ENUM.search(_judge())
    assert not m, f"판정 계약이 설비를 열거한다: {m.group(0)!r}"


def test_both_sides_still_ask_for_the_count():
    """★걷어내다 **재료까지 지우면 안 된다.**

    이 절의 목적은 「배경에 있는 설비를 세고 그대로 두라」이다. 그것이
    사라지면 좁은 공간에서 설비가 늘거나 움직이는 것을 아무도 안 본다.
    """
    for name, text in (("생성", _gen()), ("판정", _judge())):
        low = text.lower()
        assert "fixed fitting" in low, f"{name}: 무엇을 세라는지가 사라졌다"
        assert "how many" in low, f"{name}: 세라는 말이 사라졌다"


def test_the_generator_still_forbids_adding_or_moving():
    low = _gen().lower()
    assert "adding a second one of anything" in low
    assert "moving a fitting" in low


def test_the_judge_still_calls_a_duplicate_a_hard_violation():
    low = _judge().lower()
    assert "duplicated fitting" in low, (
        "중복 설비가 즉시 탈락 사유에서 빠졌다")


def test_the_old_packs_are_untouched():
    """발행된 팩은 덮어쓰지 않는다 — 갈래가 selector 로 갈린다."""
    old_gen = load_prompt("still_recipe", "stage_head_geom",
                          version=resolve_prompt_version("15"))
    old_judge = load_prompt("multiroll_judge", "judge_still",
                            version=resolve_judge_pack_version("12"))
    assert "steering wheel" in old_gen.lower()
    assert "steering wheel" in old_judge.lower()


def test_the_two_sides_move_together():
    """★**한 커밋에서 둘 다 올린다**는 계약을 잠근다.

    생성만 일반화하면 판정기가 여전히 「운전대를 세라」로 재고, 그 반려가
    재시도를 돌려 **돈이 나간다.**
    """
    gen_low, judge_low = _gen().lower(), _judge().lower()
    g, j = set(_vehicle_words_in(gen_low)), set(_vehicle_words_in(judge_low))
    for w in _VEHICLE:
        assert (w in g) == (w in j), (
            f"{w!r} 가 한쪽에만 있다 — 두 쪽이 따로 움직였다")


# ─────────── 팩 **전체**를 훑는다 — 한 파일만 보면 못 본다 ───────────


def _live_stems(module: str, version: str):
    """그 판 디렉토리의 모든 stem — 로더가 실제로 고를 수 있는 것 전부."""
    from pathlib import Path

    root = (Path(__file__).resolve().parents[3] / "prompts" / "_base"
            / module / version)
    for f in sorted(root.glob("*.md")):
        yield f.stem, load_prompt(module, f.stem, version=version)


def test_no_live_stem_in_either_pack_names_a_vehicle():
    """★**한 자리를 고치고 그 낱말만 검사하면 나머지 자리를 못 본다.**

    앞 판(v30/v14)이 정확히 그랬다 — `stage_head_geom` 과 `judge_still`
    만 일반화하고 시험도 그 둘만 봤다. 로더는 stem 별로 최신판을 따로
    고르므로 같은 팩의 `conti_label_geom`·`sketch_label_geom`·
    `critique`·`gq_observe_sys` 는 그대로 살아 있었다.

    ★`critique_sys` 는 **조건 없이** 실리므로 좁은 실내가 아닌 샷도
     그 낱말을 받는다. 다만 **닿는 범위는 이 selector 를 넘기는
     자리뿐**이다 — `still_recipe_service.py` 4곳. 배경 플레이트와
     구조물 씨드는 전역 기본 v6 를 쓰고 거기엔 그대로 남아 있다
     (전역을 안 올린 것은 의도, 2026-08-07 리뷰).

    그래서 이제 팩 **디렉토리 안 모든 stem** 을 훑는다.
    """
    from app.modules.pipeline.multiroll_gemini import JUDGE_PACK_VERSION

    offenders = []
    for module, version in (
        ("still_recipe", resolve_prompt_version(GEOM_AUTHORITY_PROMPT_VERSION)),
        ("multiroll_judge",
         resolve_judge_pack_version(STILL_JUDGE_PACK_VERSION)),
        # ★**전역 갈래도 본다.** `plate_multiroll`·구조물 씨드는
        #  `pack_version` 을 안 넘겨 이쪽을 쓴다 — 스틸 selector 만
        #  보면 배경 쪽 탈것 낱말을 영영 못 본다.
        ("multiroll_judge", resolve_judge_pack_version(JUDGE_PACK_VERSION)),
    ):
        for stem, text in _live_stems(module, version):
            low = text.lower()
            found = _vehicle_words_in(low)
            bare = _BARE_WHEEL.search(low)
            enum = _ENUM.search(text)
            if found or bare or enum:
                offenders.append(
                    f"{module}/{stem}: 낱말={found} "
                    f"바퀴={bare.group(0) if bare else None} "
                    f"열거={enum.group(0) if enum else None}")
    assert offenders == [], "\n".join(offenders)
