"""`PREVIOUS STILL USAGE` 가 **남의 관할을 말하던 것** (2026-08-27, 감사 1-E).

## 실측이 가리킨 자리

절 사이 충돌 실측(120쌍): 동시 등장 57%인데 실제 충돌은 16%, 그리고
**충돌의 74%가 이 절**이었다. 전역 우선순위 줄을 넣는 안은 기각했다 —
충돌 없는 84% 를 건드리면서 19건 중 14건만 닫는다.

## 뿌리 — 팩 안에서 자기모순

`shot_ref_classify` 의 `prev_system.md` 가

    (a) prev 를 **고를 때**
        「인물의 의상·자세 일관성은 **별도 장치가 담당하므로 고려하지 마라**」
    (b) `usage_en` 을 **쓸 때**
        「TAKE: … 움직일 수 없는 인물의 자세·위치, 인물의 의상」

라고 한다. 그 「별도 장치」가 실제로 있다 —
`still_recipe.build_prev_people_rule`.

## `usage_en` 실측 (체크포인트 1,056 칸)

    인물 축(자세·위치·의상)   707 (66%)  ← `prev_people_rule` 이 이미 담당
    카메라 축(각도·구도)      253 (23%)  ← staging 소유인데 **저작 때 못 본다**
    장소 축(룩·재질·조명)     523 (49%)  ← 이어받아도 안전

표본이 못박는다: `Exclude the prior full standing composition` ·
`Exclude the wide empty-room composition` · `Exclude the upward-looking…`

## 고친 법

    저작(팩 v5)  TAKE 는 **장소와 사물만** · 카메라는 TAKE·EXCLUDE 어디에도
                 금지 · **사람을 빼라**는 말은 EXCLUDE 에 허용(겹치지 않음)
    조립(팩 33)  헤딩을 코드에서 **팩으로** 옮기고 관할을 못박는다

★`shot_staging` 을 `depends_on` 에 **안 넣었다.** 넣으면 완주 CP 재실행
 파장이 크고, 팩 v5 가 이미 「카메라를 쓰지 마라」로 닫는다. staging 을
 보여 주는 것은 「보고 **맞춰 써라**」라서 **더 많은 지시**를 만드는
 방향이다.
"""
from __future__ import annotations

import pathlib
import re

from app.modules.pipeline.shot_ref_classify import (
    PROMPT_VERSION_MAP,
    resolve_prompt_version,
)
from app.modules.pipeline.still_recipe import (
    PREV_USAGE_CLAUSE_PROMPT_VERSION,
    build_still_prompt,
)
from app.modules.pipeline.still_recipe import (
    resolve_prompt_version as recipe_version,
)
from app.modules.prompt_loader import PROMPTS_BASE

_PACKS = pathlib.Path(PROMPTS_BASE)


def _prev_system(sel: str) -> str:
    return (_PACKS / "shot_ref_classify" / resolve_prompt_version(sel)
            / "prev_system.md").read_text(encoding="utf-8")


# ── 저작 쪽 — 남의 관할을 안 말한다 ──────────────────────────────────

def test_the_step_uses_the_new_pack():
    from app.core.steps.shot_ref_classify_step import PROMPT_VERSION

    assert PROMPT_VERSION == "5", "새 팩이 현행이 아니다"
    assert "5" in PROMPT_VERSION_MAP


def test_take_no_longer_asks_for_a_persons_pose():
    """★★**같은 것을 두 곳이 지시하던 자리.**

    `prev_people_rule` 이 이미 「앞 사진과 겹치는 인물의 의상·머리·인상」을
    잠근다. 여기서 또 말하면 어긋날 때 모델이 하나를 버린다.
    """
    txt = _prev_system("5")
    take = txt.split("· TAKE:")[1].split("· EXCLUDE:")[0]
    for banned in ("자세", "의상", "인물의 위치"):
        assert banned not in take, f"TAKE 가 아직 사람을 말한다: {banned}"


def test_taking_a_place_is_still_allowed():
    """★**금지만 쌓으면 그릴 재료가 없다** — 장소 축은 남긴다."""
    take = _prev_system("5").split("· TAKE:")[1].split("· EXCLUDE:")[0]
    for keep in ("벽", "바닥", "재질", "조명"):
        assert keep in take, f"이어받을 재료가 사라졌다: {keep}"


def test_excluding_a_person_is_still_allowed():
    """★「앞 사진의 그 사람은 이 샷에 없다」는 **겹치지 않는다.**

    그건 이 샷에 없어야 할 것을 말하는 것이지 누구인지를 정하는 것이
    아니다. 실측 표본에서 유용했다("Exclude the prior seated version…").
    """
    txt = _prev_system("5")
    assert "EXCLUDE 에 써도 된다" in txt, "사람 배제까지 막았다"
    exclude = txt.split("· EXCLUDE:")[1].split("★")[0]
    assert "인물" in exclude, "배제 축에서 사람이 사라졌다"


def test_the_camera_axis_is_forbidden_on_both_sides():
    """★저작 때 이 샷의 촬영 계획을 **못 본다** — 지시하면 추측이다."""
    txt = _prev_system("5")
    assert re.search(r"카메라·구도·거리·각도를 쓰지 마라", txt), (
        "카메라 축 금지가 없다")
    for example in ("wide composition", "close-up", "the prior framing"):
        assert example in txt, f"무엇이 그것인지 예가 없다: {example}"


def test_the_old_pack_is_untouched():
    """발행된 팩은 안 덮는다."""
    old = _prev_system("4")
    assert "움직일 수 없는 인물의 자세·위치" in old, "옛 팩을 덮어썼다"


def test_only_the_prev_stem_changed():
    """★한 스템만 고쳤는지 본다 — 다른 스템이 딸려 가면 그 축도 다시 돈다."""
    a = _PACKS / "shot_ref_classify" / resolve_prompt_version("4")
    b = _PACKS / "shot_ref_classify" / resolve_prompt_version("5")
    same, diff = [], []
    for f in sorted(a.glob("*")):
        other = b / f.name
        assert other.exists(), f"새 팩에 없다: {f.name}"
        (same if f.read_bytes() == other.read_bytes() else diff).append(f.name)
    assert diff == ["prev_system.md"], f"딸려 온 스템이 있다: {diff}"
    assert len(same) >= 8, "팩이 통째로 바뀌었다"


# ── 조립 쪽 — 헤딩이 팩에 있다 ───────────────────────────────────────

def test_the_heading_lives_in_a_pack_now():
    """★코드에 박힌 문안은 **팩 해시가 안 잡는다** — 고쳐도 지문이 안
    움직여 나가는 글만 바뀌고 옛 그림이 남는다."""
    src = (pathlib.Path(__file__).resolve().parents[2] / "app" / "modules"
           / "pipeline" / "still_recipe.py").read_text(encoding="utf-8")
    assert '"PREVIOUS STILL USAGE (follow exactly' not in src, (
        "헤딩이 아직 코드에 박혀 있다")
    stem = (_PACKS / "still_recipe"
            / recipe_version(PREV_USAGE_CLAUSE_PROMPT_VERSION)
            / "prev_usage_clause.md")
    assert stem.exists(), "팩에 스템이 없다"
    assert "{body}" in stem.read_text(encoding="utf-8")


def test_the_clause_states_its_own_scope():
    """★옛 CP 의 `usage_en` 에는 인물·카메라 말이 들어 있다 — 조립에서
    한 번 더 관할을 한정해 그 옛 값이 남의 것을 못 지시하게 한다."""
    p = build_still_prompt(
        shot_desc="a man stands", place_text="a room", time_of_day_en="night",
        world_anchor="", prompt_version="1", prev_used=True, bg_only=False,
        prev_usage_en="Take the wall. Exclude the prior wide composition.")
    line = next(l for l in p.splitlines() if "PREVIOUS STILL USAGE" in l)
    assert "place and objects only" in line, "관할 한정이 없다"
    assert "never who is in this shot" in line, "인물 관할이 안 빠졌다"
    assert "how the camera sees them" in line, "카메라 관할이 안 빠졌다"


def test_the_clause_still_carries_the_value():
    """★한정을 넣느라 값을 잃으면 안 된다."""
    p = build_still_prompt(
        shot_desc="a man stands", place_text="a room", time_of_day_en="night",
        world_anchor="", prompt_version="1", prev_used=True, bg_only=False,
        prev_usage_en="Take the wall and floor.")
    assert "Take the wall and floor." in p


def test_no_clause_when_there_is_no_usage():
    """빈 값이면 절 자체가 안 나간다 — dangling 헤딩 금지."""
    p = build_still_prompt(
        shot_desc="a man stands", place_text="a room", time_of_day_en="night",
        world_anchor="", prompt_version="1", prev_used=True, bg_only=False,
        prev_usage_en="")
    assert "PREVIOUS STILL USAGE" not in p


# ── 안 한 것 ─────────────────────────────────────────────────────────

def test_staging_was_deliberately_not_added_as_a_dependency():
    """★**안 넣은 것도 계약이다.**

    `shot_staging`(19.5)이 `shot_ref_classify`(21.9)보다 먼저 돌아 넣을 수는
    있다. 안 넣은 이유:

        · `depends_on` 을 바꾸면 완주 CP 재실행 파장이 크다
        · 팩 v5 가 이미 「카메라를 쓰지 마라」로 닫는다
        · staging 을 보여 주는 것은 「보고 **맞춰 써라**」라서
          **더 많은 지시**를 만드는 방향이다

    나중에 넣기로 하면 이 시험을 뒤집고, 그때 재실행 비용을 같이 적을 것.
    """
    from app.core.step_manifest import STEP_MANIFEST

    dep = STEP_MANIFEST["shot_ref_classify"]["depends_on"]
    assert "shot_staging" not in dep, (
        "staging 을 넣었다면 이 시험을 뒤집고 재실행 비용을 적을 것")


# ─────────────────────────────────────────────────────────────────────
# 지문 — Codex BLOCK (2026-08-27)
#
# 「`PREV_USAGE_CLAUSE_PROMPT_VERSION=33` / `prev_usage_clause` 가
#  `_config_hash_base()` 에 전혀 접히지 않습니다. 이미 발행된 pack 33 에
#  새 stem 만 추가한 형태라 selector 도 안 움직입니다.」
#
# ★맞는 지적이다. 팩 33 은 1-H 에서 이미 나갔고 거기에 스템만 더했으니
#  팩 버전도 팩 디렉토리 해시도 안 움직인다 — 완주한 scene_image 가
#  **whole-step clean skip** 해서, 옛 그림을 지키려고 넣은 관할 한정이
#  정작 나가는 프롬프트까지 못 내려간다.
#
# ★**이 판에서 같은 자리를 두 번째 반복했다** (#94 = cine_stage_direction).
#  발행된 팩에 스템을 더할 때는 **언제나** 바이트를 접어야 한다.
# ─────────────────────────────────────────────────────────────────────


def _base_hash(monkeypatch, doctored, real) -> str:
    """`doctored` 스템의 바이트가 바뀐 척하고 잰 지문.

    ★**원본(`real`)은 호출부에서 한 번만 잡아 넘긴다.** 이 헬퍼 안에서
     매번 잡으면 두 번째부터 「이미 씌운 대역」을 원본으로 잡아 사슬이
     겹치고 변조가 묻힌다 (2026-08-27 실측 — 거짓 초록의 원인이었다).
    """
    from app.core.steps.image_steps import SceneImagePipelineStep
    import app.modules.pipeline.still_recipe as sr

    step = SceneImagePipelineStep.__new__(SceneImagePipelineStep)
    step.project_config = {}

    def _spy(selector, stem):
        if doctored and stem == doctored:
            return "CHANGED"
        return real(selector, stem)

    monkeypatch.setattr(sr, "recipe_stem_content_hash", _spy)
    return step._config_hash_base()


def test_editing_the_clause_bytes_moves_the_step_fingerprint(monkeypatch):
    """★끝점 — 스템 바이트가 바뀌면 지문이 **실제로** 움직인다.

    「접은 이름」을 세지 않고 **지문 값**을 본다. 이름만 세면 접기는
    했는데 값이 hash 에 안 섞이는 경우를 못 잡는다.
    """
    import app.modules.pipeline.still_recipe as sr

    real = sr.recipe_stem_content_hash          # ★한 번만
    before = _base_hash(monkeypatch, None, real)
    after = _base_hash(monkeypatch, "prev_usage_clause", real)

    assert before != after, (
        "`prev_usage_clause` 바이트를 고쳐도 지문이 그대로다 — "
        "완주한 scene_image 가 옛 문안 그대로 clean skip 한다")


def test_an_unrelated_stem_does_not_move_it(monkeypatch):
    """반대편 — 없는 스템을 건드리면 지문은 그대로여야 한다.

    ★이게 없으면 위 시험이 「무엇을 고쳐도 매번 다른 값」인 경우에도
     초록이다(예: hash 에 시각이 섞이는 경우).
    """
    import app.modules.pipeline.still_recipe as sr

    real = sr.recipe_stem_content_hash
    a = _base_hash(monkeypatch, None, real)
    b = _base_hash(monkeypatch, "no_such_stem_anywhere", real)

    assert a == b, "지문이 입력과 무관하게 흔들린다 — 위 시험을 못 믿는다"


def test_the_folded_pack_is_the_one_the_clause_reads():
    """접은 팩이 **절이 실제로 읽는** 팩과 같은 selector 인가.

    ★팩을 하나 접어 놓고 다른 팩에서 문안을 읽으면, 고친 바이트가
     지문에 안 잡힌다 — 「접었다」는 사실만으로는 부족하다.
    """
    import inspect
    from app.core.steps import image_steps
    from app.modules.pipeline.still_recipe import (
        PREV_USAGE_CLAUSE_PROMPT_VERSION,
    )

    src = inspect.getsource(image_steps.SceneImagePipelineStep)
    assert "recipe_prev_usage_stem_content" in src
    assert "PREV_USAGE_CLAUSE_PROMPT_VERSION" in src, (
        "selector 를 상수로 안 받고 글자로 박으면 다음 판올림에서 갈린다")
    assert PREV_USAGE_CLAUSE_PROMPT_VERSION == "33"
