"""표기 정책 — **저작 목록**과 **참조가 정한 글자**를 함께 다룬다.

이 파일은 **두 실패를 같이 태운다.** 하나만 막으면 다른 하나가 살아난다.

### ① 원래 이 래칫이 막던 것 (2026-08-27, 감사 1-A ⑤) — 그대로 둔다

저작 쪽(signage_author v3)을 아무리 조여도 **배경 쪽이 지어냈다**:

    no_text v22           "**Scene-implied** signs … **may appear**"
    bg_reproject_tail v20 "writing that **permanently belongs to this
                           place** may appear"

「Scene-implied」도 「이 장소에 원래 있는 것」도 **장소 유형에서 글자를
끌어내는 통로**다. `_LEAKY` 검사가 그 두 어구를 계속 막는다.

### ② 그런데 그때 쓴 처방이 너무 넓었다 (2026-09-20 사용자 지시)

처방이 「승인 목록이 없으면 **전면 금지**」였는데, 목록을 만드는
`signage_author` 가 실주행 239샷 중 **1샷**에서만 문안을 내서 238샷에
「No readable writing anywhere」가 붙었다. 그 결과 판정기가 기기 로고·
거리 간판을 실격으로 적었다.

> "본래 의도는 자막 처럼 풍선모양 형태로 들어가는등 이상한 문자가
>  들어가는 것을 방지 하기 위함이야, 이제는 글자가 들어가는게 당연한
>  것 같아. 그런데 유빅 처럼 로봇이나 특정 디바이스에 로고등은 대부분
>  영어인게 당연해서 영어인듯한 글자는 영어로, 한국어 형태는 한국어로"

그래서 현행 계약은 **셋으로 갈린다**:

  · 승인 목록의 문안      → 그대로 정확히 그린다
  · 참조·대본이 정한 글자 → 읽혀도 된다. **문자 체계는 그 물건 것**으로
  · 그 밖의 모든 글자     → 지어내지 않는다 (①의 통로 포함)
  · 사진 위에 얹히는 것   → 여전히 금지 (자막·말풍선·워터마크·오버레이)
"""

import pathlib

from app.modules.pipeline.still_recipe import (
    TEXT_POLICY_GROK_PROMPT_VERSION,
    TEXT_POLICY_PROMPT_VERSION,
    resolve_prompt_version,
)
from app.modules.prompt_loader import PROMPTS_BASE, load_prompt

_MODULE = "still_recipe"

# 새 계약에서 **사라져야 하는** 문구들. 이 저장소가 실제로 겪은 통로다.
_LEAKY = ("scene-implied", "permanently belongs to this place")


def _stem(name: str, selector: str) -> str:
    return load_prompt(
        _MODULE, name, version=resolve_prompt_version(selector)).strip()


# ── 저작 목록이 있을 때 / 없을 때 ────────────────────────────────────

def test_with_an_approved_list_only_those_words_are_readable():
    for sel in (TEXT_POLICY_PROMPT_VERSION, TEXT_POLICY_GROK_PROMPT_VERSION):
        text = _stem("no_text", sel).lower()
        assert "words to render" in text, f"v{sel}: 승인 목록을 안 가리킨다"
        assert "nothing else" in text or "no other" in text, (
            f"v{sel}: 목록 밖을 막는 말이 없다")


def test_with_no_approved_list_only_what_the_references_fix_is_readable():
    """목록이 없어도 **참조가 정한 글자**는 읽힌다 — 전면 금지가 아니다.

    ★그러나 그 밖의 글자는 여전히 지어내지 않는다(①). 두 단언을 한
    시험에 같이 둔다 — 한쪽만 두면 반대쪽으로 다시 넘어간다.
    """
    for sel in (TEXT_POLICY_PROMPT_VERSION, TEXT_POLICY_GROK_PROMPT_VERSION):
        text = _stem("no_text_none", sel).lower()
        assert "no readable writing" not in text, (
            f"v{sel}: 전면 금지가 되살아났다")
        assert "references" in text and "readable" in text, (
            f"v{sel}: 참조가 정한 글자를 허용하지 않는다")
        assert "invent no other wording" in text, (
            f"v{sel}: 지어내기 금지가 없다")
        assert "overlay" in text, f"v{sel}: 얹히는 것 금지가 없다"


def test_fixed_figures_are_not_hidden(): 
    """★**확정된 숫자를 미리 가리지 않는다** (2026-09-20 Codex BLOCK 1).

    v20~v26 문안에는 「정확한 숫자가 그 샷을 결정하면 — 지폐 액면, 문서의
    숫자 — 그 면을 안 읽히게 찍어라」가 있었다. 뜻을 풀면 **필요한 정보일
    수록 숨기라**는 말이고, 같은 스템 첫 줄의 「WORDS TO RENDER 는 정확히
    재현」과 정면으로 부딪힌다. 판정 팩과도 어긋난다 —
    `multiroll_judge/16.202608280010/judge_still.md:26-30` 은 「지폐에는
    나라와 액면이 있다. 프롬프트나 참조가 그것을 못박으면 하나씩 확인하라」
    고 한다. 생성은 가리라 하고 판정은 보라 하는 꼴이었다.

    모델이 액면을 못 그린 개별 결과는 **산출을 재는 문제**이지, 중요한 면을
    미리 가리는 일반 계약으로 바꿀 이유가 아니다.

    ★이 시험이 통과한다고 숫자가 실제로 제대로 그려진다는 뜻은 아니다 —
    그것은 다음 판 그림으로 따로 잰다.
    """
    for sel in (TEXT_POLICY_PROMPT_VERSION, TEXT_POLICY_GROK_PROMPT_VERSION):
        for name in ("no_text", "no_text_none"):
            text = _stem(name, sel).lower()
            assert "legibility" not in text, (
                f"v{sel}/{name}: 확정 표기를 미리 가리는 조항이 되살아났다")
            assert "figure" not in text, (
                f"v{sel}/{name}: 숫자만 따로 가리는 예외가 되살아났다")


# ── 배경 세 스템 — 공유 배경이라 샷별 목록이 없다 ────────────────────

def test_shared_background_stems_follow_the_same_policy():
    """배경 세 스템 — **샷별 승인 목록을 배선할 자리가 아니다.**

    ★단위는 셋이 다르다(Codex 정정): `groupbg` 만 여러 샷이 함께 쓰는
    **그룹** 배경이고, `bg_reproject`·`bg_fill` 은 **tag 별** 배경이다.
    공통점은 저작 목록이 안 실린다는 것뿐이다.

    그래도 **앞단이 간판을 지워 놓으면 최종 샷에서 허용해도 잃은 글자는
    안 돌아온다**. 그래서 셋 다 같은 정책을 쓴다 — 참조가 보여주는 것은
    남기고, 지어내지 않는다.
    """
    for name in ("bg_reproject_tail", "groupbg_tail", "bg_fill_tail"):
        text = _stem(name, TEXT_POLICY_PROMPT_VERSION).lower()
        assert "no readable writing" not in text, (
            f"{name}: 전면 금지가 되살아났다")
        assert "invent no other wording" in text, (
            f"{name}: 지어내기 금지가 없다")
        assert "overlay" in text, f"{name}: 얹히는 것 금지가 없다"


# ── 새는 문구가 남아 있지 않은가 ─────────────────────────────────────

def test_no_leaky_phrase_survives_in_the_active_text_policy_pack():
    """활성 팩의 **모든** 스템을 훑는다 — 한 파일만 고치고 놓치기 쉽다."""
    offenders = []
    for sel in (TEXT_POLICY_PROMPT_VERSION, TEXT_POLICY_GROK_PROMPT_VERSION):
        pack = pathlib.Path(PROMPTS_BASE) / _MODULE / resolve_prompt_version(sel)
        for f in sorted(pack.glob("*.md")):
            low = f.read_text(encoding="utf-8").lower()
            for phrase in _LEAKY:
                if phrase in low:
                    offenders.append(f"{f.parent.name}/{f.name}: {phrase!r}")
    assert not offenders, (
        "저작과 무관하게 글자를 허용하는 문구가 남아 있다 — "
        f"{offenders}")


# ── 조립이 실제로 갈리는가 ───────────────────────────────────────────

def _assemble(signage_en: str, guidance_version: str = "") -> str:
    from app.modules.pipeline.still_recipe import build_still_prompt

    return build_still_prompt(
        shot_desc="a man stands", place_text="a room",
        time_of_day_en="night", world_anchor="",
        signage_en=signage_en, guidance_version=guidance_version,
        prompt_version="1", bg_only=False, prev_used=False)


def test_assembly_switches_on_whether_the_list_is_present():
    """★조건이 아니라 **나가는 문안**을 잰다."""
    with_list = _assemble("SIGNAGE TEXT: 금일 휴업")
    without = _assemble("")

    assert "금일 휴업" in with_list
    assert "WORDS TO RENDER" in with_list.upper()
    assert "WORDS TO RENDER" not in without.upper(), (
        "목록이 없는데 승인 문안 절이 나갔다")
    # ★두 갈래가 **같은 글자 정책**을 말해야 한다 — 목록 유무로 같은
    #  로고가 허용됐다 금지됐다 하면 정책 불연속이다(Codex 설계 리뷰).
    for p, name in ((with_list, "목록 있음"), (without, "목록 없음")):
        assert "invent no other wording" in p.lower(), f"{name}: 지어내기 금지 없음"
        assert "overlay" in p.lower(), f"{name}: 얹히는 것 금지 없음"


def test_grok_branch_uses_the_compact_contract():
    """grok 갈래는 같은 계약의 짧은 판을 쓴다 (8,000B 상한)."""
    plain = _assemble("", guidance_version="")
    compact = _assemble("", guidance_version="17")
    assert plain != compact, "grok 갈래가 안 갈렸다"
    assert "invent no other wording" in compact.lower()
    assert "no readable writing" not in compact.lower()
    assert len(compact) < len(plain), "컴팩트 판이 더 짧지 않다"


# ── 끝점: 저작 출력 → builder → 나가는 프롬프트 ──────────────────────
#
# ★2026-08-27 Codex BLOCK: 저작만 재고 「됐다」고 했는데, `build_signage_
#  section` 이 v2 부터 없앤 `surface_native` 를 **필수로 걸러** 산출이
#  통째로 버려지고 있었다. 저작이 성공해도 문안이 한 줄도 안 나가고 그
#  샷은 전면 금지 판으로 갔다. 조각이 아니라 **끝점**을 잰다.

def _author_output_to_prompt(items, **kw):
    """저작 출력 모양 그대로 builder 를 태우고 프롬프트까지 간다."""
    from app.modules.pipeline.still_recipe import build_signage_section

    return _assemble(build_signage_section(items), **kw)


def test_v3_author_output_reaches_the_outgoing_prompt():
    """v3 산출(면 칸 없음)이 실제로 나가는 글에 실린다."""
    v3 = [{"text_native": "금일 휴업", "source": "scene_text_quoted",
           "source_quote": "금일 휴업", "reason_ko": "r"}]
    p = _author_output_to_prompt(v3)

    assert "금일 휴업" in p, "저작 문안이 프롬프트에 안 실렸다"
    assert "WORDS TO RENDER" in p.upper()
    assert "no readable writing" not in p.lower(), (
        "전면 금지가 되살아났다")


def test_v1_shaped_output_still_reaches_the_prompt():
    """옛 모양(면 칸 있음)도 그대로 실린다 — 옛 records 를 읽는 자리가 있다."""
    v1 = [{"surface_native": "문", "text_native": "금일 휴업",
           "reason_ko": "r"}]
    p = _author_output_to_prompt(v1)
    assert '- 문: "금일 휴업"' in p


def test_empty_author_output_still_allows_what_the_references_fix():
    """★이 시험이 2026-09-20 수리의 자리다.

    저작이 빈 목록을 내는 것이 **평상**이다(실주행 239샷 중 238). 그
    갈래가 전면 금지로 떨어지면 기기 로고·거리 간판까지 같이 막힌다.
    """
    p = _author_output_to_prompt([]).lower()
    assert "no readable writing" not in p, "빈 목록이 전면 금지로 떨어졌다"
    assert "invent no other wording" in p, "지어내기 금지까지 같이 빠졌다"


def test_the_chain_candidate_carries_the_same_words():
    """★같은 샷의 두 후보가 **다른 글자 계약**으로 그려지면 안 된다.

    체인 후보 호출이 `signage_en` 을 안 넘기면 그쪽만 전면 금지 판을 쓴다.
    호출부를 소스로 확인한다 — 함수를 직접 태우는 것만으로는 못 본다.
    """
    import ast
    import pathlib

    src = (pathlib.Path(__file__).resolve().parents[2]
           / "app" / "services" / "still_recipe_service.py"
           ).read_text(encoding="utf-8")
    calls = [n for n in ast.walk(ast.parse(src))
             if isinstance(n, ast.Call)
             and isinstance(n.func, ast.Name)
             and n.func.id == "build_still_prompt"]
    assert len(calls) >= 2, "조립 호출이 둘 미만 — 전제 확인"
    for c in calls:
        kws = {k.arg for k in c.keywords if k.arg}
        assert "signage_en" in kws, (
            f"조립 호출(line {c.lineno})이 저작 문안을 안 넘긴다 — "
            "그 후보만 전면 금지 판으로 간다")
