"""b 롤 구도 변주 절이 CAMERA & FRAME 계약을 통째로 부정하던 것 (감사 P0-B).

## 무엇이 결함이었나

한 프롬프트 안에 정면으로 부딪치는 두 문장이 있었다.

    CAMERA & FRAME (follow exactly — this is the composition authority
    for this still) ... Compose the frame exactly as specified above
    — subject scale and screen placement.

    COMPOSITION VARIATION ... treat the CAMERA & FRAME contract above as
    a baseline to depart from, **not a lock** ... a different angle,
    height, **distance** or **foreground layer**.

실측(records.json 19개, `_archived` 제외): 변주 절이 실린 후보 **615개 중
284개(46.2%)가 최종 채택**됐고, 그중 **597개가 `- FRAMING SCALE:` 실제
값**을, 309개가 `- FRAME LAYOUT:` 값을 달고 있었다. 부정당한 것은 빈
계약이 아니라 **값이 실린 계약**이다.

## 무엇을 남겼나

카메라를 다시 세우는 것(각도·높이)은 2026-08-13 사용자 확정이 요구한다 —
「a 와 b 가 같은 구도·앵글로 나오면 안 된다」. 그래서 그 두 축은 **열려
있다고 명문화**하고, 프레임(subject scale · screen placement · key
background elements)은 그대로 구속한다고 적는다.

## 이 시험이 재는 것

조립 끝점(`build_ab_roll_prompt_map`)이 내놓는 **후보별 최종 프롬프트**를
잰다 — 스템 파일을 직접 읽지 않는다. 스템만 읽으면 조립이 다른 팩으로
옮겨 가도 초록으로 남는다.
"""
from __future__ import annotations

import pytest

from app.modules.pipeline import still_recipe as sr

# 계약을 부정하던 옛 어구 — 낱말 그대로 다시 들어오면 여기서 걸린다.
BANNED = (
    "not a lock",
    "baseline to depart from",
    "is a start, not a lock",
    # 아래 둘은 계약이 이미 값으로 정한 축이라 변주가 열면 안 된다.
    "distance",
    "foreground layer",
)

# 변주 절이 **지켜야 한다고 말해야** 하는 축. 금지만 남기면 무엇을 지킬지
# 모델이 모른다.
MUST_NAME = (
    "subject scale",
    "screen placement",
    "key background elements",
)

BASE = "BASE PROMPT BODY"
LABELS = ("a", "b")


@pytest.mark.parametrize("selector", ["", sr.STILL_COMPACT_PROMPT_VERSION])
def test_variation_candidate_never_negates_the_camera_contract(selector):
    """두 백엔드 갈래 모두, 둘째 후보 프롬프트에 부정 어구가 없어야 한다."""
    prompts = sr.build_ab_roll_prompt_map(BASE, LABELS, selector)
    b = prompts["b"]
    assert b != prompts["a"], "둘째 후보에 변주 절이 안 붙었다"
    low = b.lower()
    for phrase in BANNED:
        assert phrase not in low, (
            f"selector={selector!r} 후보 b 에 옛 어구가 남아 있다: {phrase!r}")


@pytest.mark.parametrize("selector", ["", sr.STILL_COMPACT_PROMPT_VERSION])
def test_variation_candidate_names_what_stays_locked(selector):
    """무엇을 지킬지 **값으로** 말해야 한다 — 금지형만 남기면 재료가 없다."""
    b = sr.build_ab_roll_prompt_map(BASE, LABELS, selector)["b"].lower()
    for axis in MUST_NAME:
        assert axis in b, (
            f"selector={selector!r} 후보 b 가 지킬 축을 안 적었다: {axis!r}")


@pytest.mark.parametrize("selector", ["", sr.STILL_COMPACT_PROMPT_VERSION])
def test_variation_candidate_still_opens_angle_and_height(selector):
    """사용자 확정(2026-08-13)이 요구하는 두 축은 **살아 있어야** 한다."""
    b = sr.build_ab_roll_prompt_map(BASE, LABELS, selector)["b"].lower()
    assert "angle" in b, "각도 자유가 사라졌다 — a·b 가 수렴한다"
    assert "height" in b, "높이 자유가 사라졌다 — a·b 가 수렴한다"


@pytest.mark.parametrize("selector", ["", sr.STILL_COMPACT_PROMPT_VERSION])
def test_variation_clause_points_at_nothing_that_may_be_absent(selector):
    """「위의 CAMERA 줄」처럼 **없을 수도 있는 것**을 가리키면 안 된다.

    `omit_camera_direction=True`(현행 .env) 면 `- CAMERA:` 줄이 통째로
    빠진다. v25 가 `cinematic_finish` 에서 고친 것과 같은 결함을 여기서
    새로 만들지 않는다.
    """
    clause = sr.build_broll_variation_clause(selector).lower()
    assert "camera line" not in clause
    assert "camera & frame contract above" not in clause


def test_first_roll_is_untouched():
    """첫 롤은 base 그대로다 — 기준이 움직이면 비교가 성립하지 않는다."""
    prompts = sr.build_ab_roll_prompt_map(BASE, LABELS, "")
    assert prompts["a"] == BASE


def test_single_label_gets_no_variation():
    assert sr.build_ab_roll_prompt_map(BASE, ("a",), "") == {"a": BASE}


def test_assembly_and_fingerprint_read_the_same_stem():
    """조립과 지문이 **같은 함수**로 스템을 고른다 (grok 갈래 누락 방지).

    종전에는 지문이 `STILL_COMPACT_PROMPT_VERSION` 을 박아 접어, grok
    조립이 렌더하는 컴팩트 판 바이트를 고쳐도 지문이 안 움직였다.
    """
    import ast
    import pathlib

    src = pathlib.Path(
        __file__).resolve().parents[2] / "app/core/steps/image_steps.py"
    tree = ast.parse(src.read_text(encoding="utf-8"))
    names = {
        n.name for node in ast.walk(tree)
        if isinstance(node, ast.ImportFrom)
        and node.module == "app.modules.pipeline.still_recipe"
        for n in node.names
    }
    assert "broll_variation_stem" in names, (
        "image_steps 가 b 변주 스템을 조립과 같은 함수로 안 고른다")


@pytest.mark.parametrize(
    "grok_backend,expected_stem",
    [(False, "broll_composition_variation"),
     (True, "broll_composition_variation_compact")],
)
def test_stem_split_follows_the_backend(grok_backend, expected_stem):
    sel = sr.still_guidance_selector(grok_backend)
    stem, pack = sr.broll_variation_stem(sel)
    assert stem == expected_stem
    assert pack == sr.BROLL_VARIATION_PROMPT_VERSION
