"""참조 묶음 유료 canary 의 **최소 원고** — 2씬. ★작품 고유명사 0.

## ★왜 2씬인가 (2026-09-01 실측)

처음엔 **1씬**으로 잡았다(돈과 변수를 줄이려고). 그런데 유료 주행에서
`scene_segmentation` 이 3회 시도 끝에 **구조적으로** 실패했다 —

    segment_rule.py:182   C1 — 둘 미만이면 **분해가 아니다**

씬이 하나면 나눌 것이 없어 **어떤 규칙도 통과할 수 없다**. 그 뒤 35단계가
줄줄이 막혔다. Codex 가 「production 계약상 불가능하다는 **무료 근거**가
나올 때만 2씬으로」라고 정해 뒀고, 그 근거가 이것이다. 사용자 확정: 2씬.

★2씬으로 늘리되 **구조 의무는 그대로** 둔다 — 새 엔티티를 안 만들고 같은
가게·같은 간판을 다른 때에 다시 보여, `location_part` 의 되풀이 출현만 는다.

Codex 가 크기를 못박았다 (2026-08-31) —

> 3씬을 임의 기본값으로 두지 마십시오. 먼저 **1씬·2샷**으로 갑니다.
>   shot 1: location/base background 의무를 세움
>   shot 2: 같은 location 의 location_part 가 보이고 context+detail 의무가 생김
> 이 한 씬으로 selector 가 exactly 1 eligible 과 실제 assembly N 을 내면
> 그대로 확정합니다. 3씬은 근거 없이 비용·중복 엔티티·merge 변수를 늘립니다.

## 무엇을 세우나

    shot 1   가게 **전체**가 보인다        → `location` · 기존 배경 참조 의무
    shot 2   그 가게의 **회전 간판**이 가깝다 → `location_part` ·
             맥락(그 가게가 어떤 곳인가) + 상세(그 부분이 어떻게 생겼나)

★시대·지역·세계 사실은 [[period_episode]] 것을 **그대로 쓴다**. 두 벌로
만들면 한쪽만 고쳐진다.
★등장하는 것은 **역할어**(학생·이발사)와 **일반명사**(회전 간판·미닫이문)뿐이다.
★이 파일은 **시험 데이터**다 — production 지문·코드 분기에 이 낱말이 없다.
"""
from __future__ import annotations

from typing import Any, Dict, List, Tuple

from tests.grounding.fixtures.period_episode import (ERA, REGION,  # noqa: F401
                                                     WORLD_FACTS)

#: 장면 = (제목, 본문 문단들, [(샷 설명, [인물])]). ★한 씬·두 샷.
_SCENES: List[Tuple[str, List[str], List[Tuple[str, List[str]]]]] = [
    (
        "S#1. 이발소 앞 / 낮",
        [
            "골목 어귀에 이발소가 있다. 나무틀에 유리를 끼운 미닫이문이 반쯤 "
            "열려 있고, 문틀은 칠하지 않은 나무 그대로다. 처마 아래 그늘이 "
            "가게 앞을 반쯤 덮는다.",

            "가게 앞에 회전 간판이 서 있다. 붉은 띠와 푸른 띠가 비스듬히 감긴 "
            "원통이 느리게 돌아, 띠가 올라갔다 사라지기를 되풀이한다.",

            "학생이 문 앞에 선다. 간판을 한 번 올려다보고는 문턱을 넘는다.",

            "안에서 이발사가 나와 간판 쪽을 살핀다. 원통이 도는 속도가 "
            "고르지 않아 한 번씩 멈칫한다.",
        ],
        [
            ("이발소 앞 전경 — 미닫이문과 처마가 함께 보인다", []),
            ("가게 앞 회전 간판이 가까이 보인다", []),
        ],
    ),
    (
        "S#2. 이발소 앞 / 해질녘",
        [
            "해가 기울고 골목이 붉어진다. 미닫이문은 닫혔고 처마 밑 그늘이 "
            "길게 늘어졌다.",

            "회전 간판이 멎어 있다. 붉은 띠와 푸른 띠가 감긴 자리에서 그대로 "
            "굳었고, 원통 표면에 잔 흠집이 드러난다.",

            "이발사가 나와 간판 아래를 살피고는 손으로 원통을 한 번 돌린다. "
            "간판이 몇 바퀴 돌다 다시 멎는다.",
        ],
        [
            ("멎은 회전 간판이 가까이 보인다", []),
        ],
    ),
]

#: ★엔티티 단위 스텝(entity_t2i 등)의 **선언 상한** — 선언 — 한 씬 원고. 계획표는 이 값을
#:  쓰고, 실제가 이를 넘으면 스텝 문이 provider 앞에서 선다(돈은 안 새고 판은 미완주).
ENTITY_CAP = 12
#: 아웃룩이 생기는 원고인가 — `composite_image_gen`(`if_has_outlooks`)은 runner 가
#: 있어야 답해 계획표가 production 에 못 묻는다. ★선언은 **안전한 방향**으로만
#: 틀릴 수 있다: True 인데 아웃룩이 0이면 runtime 이 not_applicable(0원)로 내리고,
#: False 인데 있으면 계획에 없는 구매가 된다 — 인물이 있는 원고는 True 로 둔다.
HAS_OUTLOOKS = True
#: (인물, 아웃룩) 쌍의 상한 — `composite_image_gen` 단위. ★production 산출
#: (`outlook_phase3` CP 의 아웃룩 수)과 대조하는 문이 있다(넘으면 선다).
OUTLOOK_PAIR_CAP = 8
#: shot_staging 이 정한 (인물, 상태) 변형의 상한 — `character_state_variant` 단위.
#: ★production 술어 `is_immobilized_state` 로 센 수와 대조하는 문이 있다(넘으면 선다).
STATE_VARIANT_CAP = 0
#: ★배경 사슬 상한 (2026-09-03 05:50) — 원고의 장소 수 기준 상한. production 산출(`background_classify` chain_bg 수 ·
#:  `background_master_plan` floor plan/background 수)과 대조하는 문이 있다(넘으면 provider 앞에서 선다).
CHAIN_GROUP_CAP = 1
FLOOR_PLAN_CAP = 2
BACKGROUND_CAP = 4

_SEP = "\n\n"


def _bodies() -> List[str]:
    return [_SEP.join(p) for _h, p, _s in _SCENES]


def manuscript() -> str:
    """원고 전문. ★조립 순서는 `segments()` 좌표와 **같은 값**에서 온다."""
    return _SEP.join(f"**{h}**{_SEP}{b}"
                     for (h, _p, _s), b in zip(_SCENES, _bodies()))


def segments() -> List[Dict[str, Any]]:
    """`scene_save.segments` 와 **같은 모양**."""
    out: List[Dict[str, Any]] = []
    cursor = 0
    for i, (body, (heading, _p, _s)) in enumerate(zip(_bodies(), _SCENES), 1):
        out.append({"scene_index": i, "heading": f"**{heading}**",
                    "start_char": cursor, "end_char": cursor + len(body),
                    "length": len(body), "text": body})
        cursor += len(body) + len(_SEP)
    return out


def segment_texts() -> Dict[str, str]:
    return {f"scene-{s['scene_index']}": s["text"] for s in segments()}


def shot_scenes() -> List[Dict[str, Any]]:
    """`shot_validator.scenes` 와 **같은 모양**."""
    out: List[Dict[str, Any]] = []
    for i, (heading, _paras, shots) in enumerate(_SCENES, start=1):
        out.append({
            "scene_index": i, "scene_heading": f"**{heading}**",
            "shots": [{"shot_index": j, "description": d,
                       "based_on_beat": j, "characters": list(chars),
                       "character_ids": []}
                      for j, (d, chars) in enumerate(shots, start=1)],
        })
    return out


#: 이 원고가 **구조적으로** 세우는 의무. ★정답을 심는 것이 아니라,
#:  「이것이 안 생기면 원고가 목적을 못 한다」는 **불변식**이다.
STRUCTURAL_OBLIGATIONS: List[Dict[str, str]] = [
    {"owner": "location", "why": "가게 전체가 한 샷의 주제다 — 배경 참조가 선다"},
    {"owner": "location_part",
     "why": "그 가게에 붙은 고정 설비가 다른 샷의 주제다 — 맥락+상세가 선다"},
]


def assert_shape() -> None:
    """★크기가 **승인된 것**과 같은가. 다르면 비용·변수가 달라진다."""
    segs = segments()
    if len(segs) != 2:
        raise AssertionError(f"승인은 2씬인데 {len(segs)}씬이다")
    shots = shot_scenes()[0]["shots"]
    if len(shots) != 2:
        raise AssertionError(f"1씬은 2샷이어야 하는데 {len(shots)}샷이다")
    if not manuscript().strip():
        raise AssertionError("원고가 비었다")
