"""손 절이 **프레이밍 권위를 빼앗지 않는다** (2026-08-27, 감사 1-C).

한 프롬프트에 두 선언이 맞섰다:

    CAMERA & FRAME (framing authority; references give identity and
                    place, not framing):
    THE HAND THAT IS DOING THIS: … The framing stays on the object.

실측: 손 절이 나간 571건 중 **558건(98%)** 이 인물 샷 + CAMERA 동반이고,
그때 CAMERA 는 medium shot 208 · close-up 158 을 요구했다.

★**금지를 쌓지 않는다.** 「CAMERA 를 따르라」를 새로 넣지 않고 맞서는
 문장만 뺐다 — CAMERA 절이 이미 권위를 선언한다.

★★**두 실패가 반대편에 있다 — 어느 쪽도 버리지 않는다.**

 그 문장은 **의도된 것**이었다. `bg_only` 샷은 CAMERA 절도 캐릭터 참조도
 자세 계약도 없어서, 프레이밍을 물체에 못박지 않으면 **전신이 끌려
 들어온다**. `test_handled_object_clause_names_the_hand_owner` 가 정확히
 그 갈래(`bg_only=True`)를 잠그고 있었고, 처음에 문장을 통째로 걷었다가
 그 시험에 걸렸다.

 그래서 `bg_only` 로 가른다 — 무인 샷은 그 문장이 프레이밍의 **유일한
 재료**이고, 인물 샷은 CAMERA 가 권위를 선언하므로 거들 필요가 없다.
 실측에서도 손 절 571건 중 **인물 샷이 570건**이라 표적이 그쪽이다.
"""

import pathlib

from app.modules.pipeline.still_recipe import (
    GEOM_AUTHORITY_PROMPT_VERSION,
    HANDLED_OBJECT_PROMPT_VERSION,
    build_still_prompt,
    resolve_prompt_version,
)
from app.modules.prompt_loader import PROMPTS_BASE, load_prompt

_MODULE = "still_recipe"
# 그 절이 프레이밍을 못박던 두 모양. ★v15 는 소문자, v17 은 대문자다 —
#  대문자만 보는 검사를 쓰면 **현재 기본(v15)이 통째로 빠진다.**
_FRAMING = "framing stays on the object"
_NOTHING_ELSE = "nothing else of them needs to be in shot"


def _clause(selector: str) -> str:
    return load_prompt(
        _MODULE, "handled_object_clause",
        version=resolve_prompt_version(selector), handler="연희").strip()


def test_the_shared_clause_does_not_pin_the_framing():
    """공통 절에는 프레이밍 문장이 없다 — 갈래는 조립이 정한다."""
    low = _clause(HANDLED_OBJECT_PROMPT_VERSION).lower()
    assert _FRAMING not in low, "프레이밍을 못박는 문장이 공통 절에 남았다"
    assert _NOTHING_ELSE not in low


def test_the_framing_sentence_lives_in_its_own_stem():
    """무인 샷 전용 스템 — 그 갈래에서는 프레이밍의 유일한 재료다."""
    got = load_prompt(
        _MODULE, "handled_object_framing",
        version=resolve_prompt_version(HANDLED_OBJECT_PROMPT_VERSION)).strip()
    assert _FRAMING in got.lower()
    assert _NOTHING_ELSE in got.lower()


def test_the_active_clause_still_says_whose_hand_it_is():
    """★걷어내다 목적까지 지우면 안 된다 — 이 절은 **손의 신원**이 본체다."""
    got = _clause(HANDLED_OBJECT_PROMPT_VERSION)
    assert "연희" in got
    assert "no one else" in got.lower(), "그 손이 누구 것인지가 사라졌다"
    assert "wrist and forearm" in got.lower(), "손이 어디까지인지가 사라졌다"


def test_the_wrist_reach_stays_subordinate_to_the_framing():
    """`as the framing reaches` — 이미 프레이밍에 종속된 문안이다.

    걷어낸 마지막 문장이 이것을 뒤집고 있었다.
    """
    assert "as the framing reaches" in _clause(
        HANDLED_OBJECT_PROMPT_VERSION).lower()


def test_the_old_packs_are_untouched():
    """발행된 팩은 덮어쓰지 않는다 — 갈래가 selector 로 갈린다."""
    for old in ("15", "17"):
        assert _FRAMING in _clause(old).lower(), (
            f"v{old} 를 덮어썼다 — 발행된 팩은 그대로 둔다")


def test_the_handled_selector_is_split_from_the_geometry_pack():
    """★함께 옮기면 그 팩의 **다른 네 스템**까지 바뀐다.

    `GEOM_AUTHORITY` 팩에는 라벨·머리말 스템이 함께 있고 그것들은 이
    판에서 바뀌지 않았다. 한 팩으로 묶으면 무관한 스템의 바이트가
    지문에 들려 전 샷이 다시 돈다.
    """
    assert HANDLED_OBJECT_PROMPT_VERSION != GEOM_AUTHORITY_PROMPT_VERSION
    new = pathlib.Path(PROMPTS_BASE) / _MODULE / resolve_prompt_version(
        HANDLED_OBJECT_PROMPT_VERSION)
    assert [f.name for f in sorted(new.glob("*.md"))] == [
        "handled_object_clause.md",
        "handled_object_framing.md"], "새 팩에 무관한 스템이 딸려 왔다"


# ── 끝점: 나가는 프롬프트 ────────────────────────────────────────────

def _assemble(**kw) -> str:
    return build_still_prompt(
        shot_desc="a man holds a photo", place_text="a room",
        time_of_day_en="night", world_anchor="", prompt_version="1",
        bg_only=False, prev_used=False, **kw)


def test_a_person_shot_carries_no_framing_pin():
    """★조립 함수만 태우고 「됐다」고 하지 않는다 — 나가는 글을 잰다."""
    p = _assemble(handled_by="연희")
    assert "THE HAND THAT IS DOING THIS" in p
    assert _FRAMING not in p.lower(), "나가는 프롬프트에 아직 남아 있다"


def _plate(**kw) -> str:
    return build_still_prompt(
        shot_desc="a hand on a photo", place_text="a room",
        time_of_day_en="night", world_anchor="", prompt_version="1",
        bg_only=True, prev_used=False, handled_by="연희", **kw)


def test_a_plate_shot_still_pins_the_framing():
    """★**옛 목적을 되살린다.** 프레이밍을 말할 다른 재료가 없는 샷은
    이 문장이 유일한 재료다 — 없으면 전신이 끌려 들어온다.
    """
    assert _FRAMING in _plate().lower(), (
        "무인 샷에서 전신 방지 재료가 사라졌다")


def test_a_camera_clause_takes_the_framing_back():
    """★**`bg_only` 하나로는 못 가른다** (2026-08-27 Codex BLOCK).

    호출자는 bg_only 와 무관하게 `camera_frame_en` 을 만든다. 그 절이
    있으면 프레이밍 권위가 이미 있으므로 거들면 모순이다.
    """
    from app.modules.pipeline.still_recipe import build_camera_frame_clause

    cam = build_camera_frame_clause(
        {"camera_direction": "from below", "framing_scale": "close-up"}, "22")
    p = _plate(camera_frame_en=cam)
    assert "framing authority" in p.lower()
    assert _FRAMING not in p.lower(), (
        "CAMERA 가 권위를 선언한 샷에 프레이밍 문장이 함께 나갔다")


def test_a_person_assignment_takes_the_framing_back():
    """★**`char_names` 가 `bg_only` 를 뒤집는다** (2026-08-06 계약).

    인물 배정이 있으면 무인 조항 대신 인물 절이 나간다 — 사람이 그려질
    샷이므로 물체 인서트를 요구하면 모순이 그대로 남는다.
    """
    p = _plate(char_names=["연희 (한국인 여성)"])
    assert "NO PEOPLE" not in p, "전제 확인 — 인물 배정이 bg_only 를 이긴다"
    assert _FRAMING not in p.lower(), (
        "사람이 그려질 샷에 물체 인서트 요구가 나갔다")


def test_no_handler_means_no_clause():
    p = _assemble(handled_by="")
    assert "THE HAND THAT IS DOING THIS" not in p


def test_the_camera_clause_remains_the_framing_authority():
    """CAMERA 절이 **스스로 권위를 선언**하므로 따로 강제할 것이 없다.

    ★`camera_frame_en` 은 조각이 아니라 `build_camera_frame_clause` 가
     만든 완문이다 — 조립기를 태워야 실제로 나가는 글이 나온다.
    """
    from app.modules.pipeline.still_recipe import build_camera_frame_clause

    cam = build_camera_frame_clause(
        {"camera_direction": "From just below her eye line",
         "framing_scale": "close-up"}, "22")
    assert "framing authority" in cam.lower(), (
        "CAMERA 절이 권위를 선언하지 않는다 — 이 판의 전제가 깨졌다")

    p = _assemble(handled_by="연희", camera_frame_en=cam)
    assert "framing authority" in p.lower()
    assert "THE HAND THAT IS DOING THIS" in p
    assert _FRAMING not in p.lower(), (
        "권위를 선언한 절과 프레이밍을 못박는 문장이 함께 나간다")


# ── 바깥 재개 관문 ───────────────────────────────────────────────────

def test_the_change_is_folded_into_the_step_identity():
    """★per-shot 지문에는 실리지만 **whole-step clean skip 이 먼저**다.

    완주한 에피소드가 새 문안을 한 번도 못 보는 자리다.
    """
    import ast

    src = (pathlib.Path(__file__).resolve().parents[2]
           / "app" / "core" / "steps" / "image_steps.py"
           ).read_text(encoding="utf-8")
    keys = {n.slice.value for n in ast.walk(ast.parse(src))
            if isinstance(n, ast.Subscript)
            and isinstance(n.slice, ast.Constant)
            and isinstance(n.slice.value, str)}
    assert "recipe_handled_object_pack" in keys, (
        "팩 selector 가 스텝 신원에 안 접혔다")
    assert "recipe_handled_object_content" in keys, (
        "스템 바이트가 스텝 신원에 안 접혔다 — 팩 번호만 올리고 문안을 "
        "고치면 그대로 건너뛴다")
    # ★**스템 둘을 다 접는가.** 프레이밍 문장을 따로 떼어 놓고 한 장만
    #  접으면 그 문장을 고쳐도 완주 판이 못 본다.
    stems = {n.value for n in ast.walk(ast.parse(src))
             if isinstance(n, ast.Constant) and isinstance(n.value, str)
             and n.value.startswith("handled_object")}
    assert stems == {"handled_object_clause", "handled_object_framing"}, (
        f"지문에 접힌 손 절 스템: {stems}")


def test_the_grok_branch_gets_the_fix_too():
    """★**컴팩트 갈래에 딸려 가면 이 판이 통째로 no-op 이 된다.**

    `guidance_version` 은 grok2 일 때 `STILL_COMPACT_PROMPT_VERSION`(=17)
    이 되는데 **17 팩에도 이 스템이 있고 그 판에는 프레이밍 문장이 그대로
    있다.** 같은 파일이 `CINEMATIC_FINISH_PROMPT_VERSION` 에서 이미 겪고
    갈라 둔 자리다.
    """
    from app.modules.pipeline.still_recipe import (
        STILL_COMPACT_PROMPT_VERSION,
    )

    # 전제: 컴팩트 팩에도 이 스템이 있고 옛 문안이다
    assert _FRAMING in _clause(STILL_COMPACT_PROMPT_VERSION).lower()

    p = _assemble(handled_by="연희",
                  guidance_version=STILL_COMPACT_PROMPT_VERSION)
    assert "THE HAND THAT IS DOING THIS" in p
    assert _FRAMING not in p.lower(), (
        "grok 갈래에서 이 판이 통째로 no-op 이다")


def test_the_grok_branch_keeps_the_plate_shot_guard():
    from app.modules.pipeline.still_recipe import (
        STILL_COMPACT_PROMPT_VERSION, build_still_prompt,
    )

    p = build_still_prompt(
        shot_desc="a hand on a photo", place_text="a room",
        time_of_day_en="night", world_anchor="", prompt_version="1",
        bg_only=True, prev_used=False, handled_by="연희",
        guidance_version=STILL_COMPACT_PROMPT_VERSION)
    assert _FRAMING in p.lower(), "무인 샷에서 전신 방지 재료가 사라졌다"
