"""시험용 **현대 원고** — 시대 선언이 **없고** 지역만 있는 판.

## 왜 짓나

사용자 재지시 (2026-09-02): 「"주유소", 한국식 "편의점 + 앞쪽 벤치" 등 현대
배경이라도 고증이 필요한 경우는 되도록 진행해야 해」.

`period_episode` 는 1960년대라 다섯 갈래가 다 지난다. 그런데 고증은 **과거
것만이 아니다** — 기름 넣는 곳·가게 앞 자리·버스 정류장은 나라마다 지붕·
간판·기계·앉는 자리가 다르고, 이미지 모델은 그것을 근거 없이 그리면 다른
나라 것을 섞는다. 그곳을 아는 사람은 곧바로 알아본다. 판별은 시대가
아니라 두 축이다:

    hard_to_generate      근거 없이 그리면 겉모습을 틀릴 만한가
    viewers_would_notice  그곳 사정을 아는 사람이 알아채는가

## 무엇을 골랐나 — **실제 지역 · 시대 없음**

대한민국의 지방 국도변. `ERA` 는 **빈 문자열**이다 — production 의
`visual_world_rules.era` 가 비어 있는 판을 그대로 흉내 낸다. 지역만 선언한다.

    location        기름 넣는 곳 · 24시간 가게 — 꼴이 나라마다 다르다
    location_part   기름 넣는 기계가 선 섬 · 가게 앞 긴 의자와 파라솔 —
                    고정 설비가 지역 특유다

★`character`·`outlook` 은 **대상이 아니어도 된다** — 현대 옷·신발은 어렵지
않다(§2-4 실원고 실측 2026-08-31: 그것이 옳은 판정이다). 이 원고는
「현대라서 빠지는 것이 없다」를 재는 판이지 다섯 갈래를 다 채우는 판이 아니다.

## ★지키는 것

- **작품 고유명사·상표를 안 쓴다.** 역할어(직원·운전자)와 일반명사뿐이다.
- 지역은 **구조화 칸**(`REGION`)과 세계 사실 블록에만 적는다. 시대는 어디에도
  안 적는다 — 「현대」라는 낱말도 규칙으로 안 쓴다.
- ★이 파일의 낱말(주유소·편의점·파라솔…)은 **시험 데이터**다. production
  코드·프롬프트 팩에 이 낱말이 들어가면 안 된다 — `FIXTURE_ONLY_TERMS` 를
  시험이 `app/` 와 활성 팩에서 **0건**으로 잠근다.
- 산출 모양은 **저장 체크포인트와 같다**(`scene_save.segments` ·
  `shot_validator.scenes[].shots[]`).
"""
from __future__ import annotations

from typing import Any, Dict, List, Tuple

#: ★**구조화된 시대 칸** — production 의 `visual_world_rules.era` 와 같은 자리.
#:  **비어 있다.** 시대를 만들지도 요구하지도 않는 판이다.
ERA = ""
REGION = "대한민국"

#: 이 낱말들은 fixture 에만 산다. ★production 코드·활성 팩에 0건이어야 한다.
FIXTURE_ONLY_TERMS: Tuple[str, ...] = ("주유소", "주유기", "편의점", "파라솔",
                                       "세차 터널")

#: 세계 사실 — **실제 지역**의 시각 관례. 시대는 적지 않는다.
WORLD_FACTS = """이 이야기는 대한민국의 한 지방 국도변을 배경으로 한다.
아래는 이 지역의 시각 관례이며, 이 원고 안에서는 이것이 정본이다.

기름 넣는 곳
- 기둥 넷 위에 얹은 넓은 평지붕 아래에 있고, 지붕 테두리에 한 가지 색 띠를
  두른다. 기름 넣는 기계는 낮은 섬 위에 두 대씩 등을 맞대고 서고, 섬 양 끝에
  노란 쇠기둥이 박혀 있다.
- 입구에는 값을 숫자로 띄우는 높은 기둥 간판을 세운다.
- 직원이 있는 곳에서는 직원이 형광 조끼를 입고 차창으로 다가와 넣는다.
  운전자는 내리지 않는다. 옆에 세차 터널이 붙어 있다.

24시간 가게
- 앞면이 통유리이고 자동으로 열리는 미닫이문이다. 유리에는 판촉 그림을
  크게 붙인다. 문 옆 바깥에 아이스크림 냉동고를 둔다.
- 가게 앞에 플라스틱 탁자와 등받이 없는 긴 의자를 내놓고 그 위에 파라솔을
  편다. 산 것을 거기 앉아 먹는다. 안에는 뜨거운 물과 전자레인지를 둔 자리가
  있다.

★이 꼴들은 나라마다 다르다. 근거 없이 그리면 다른 나라 것이 섞여 들어가고,
이곳을 아는 사람은 곧바로 알아본다. 가로등·종이컵처럼 어디나 같은 것은
따로 정하지 않는다."""

#: 장면 = (제목, 본문 문단들, [(샷 설명, [인물])]).
_SCENES: List[Tuple[str, List[str], List[Tuple[str, List[str]]]]] = [
    (
        "S#1. 국도변 주유소 / 낮",
        [
            "국도변 주유소. 기둥 넷이 받친 넓은 평지붕 테두리에 파란 띠가 "
            "둘려 있다. 그 아래 낮은 섬 위에 주유기가 두 대씩 등을 맞대고 "
            "섰고, 섬 끝의 노란 쇠기둥에 긁힌 자국이 있다.",

            "입구 기둥 간판의 숫자가 한 자리 꺼져 있다. 형광 조끼를 입은 "
            "직원이 차창으로 다가온다.",

            "운전자가 창을 내리고 카드를 건넨다. 직원은 받아 들고 기계 쪽으로 "
            "몸을 돌린다.",

            "옆 세차 터널에서 물이 흩뿌려져 바닥에 무지개가 잠깐 선다.",
        ],
        [
            ("평지붕 아래 주유기 섬", []),
            ("형광 조끼 직원이 차창으로 다가온다", ["직원"]),
            ("운전자가 카드를 건넨다", ["운전자"]),
        ],
    ),
    (
        "S#2. 편의점 앞 / 밤",
        [
            "편의점 앞. 통유리에 붙은 판촉 그림이 안쪽 불빛에 번들거린다. "
            "자동문 옆 아이스크림 냉동고 위에 누가 두고 간 우산이 있다.",

            "운전자가 컵라면을 들고 나와 파라솔 아래 긴 의자에 앉는다. "
            "플라스틱 탁자 위에 종이컵 하나가 바람에 굴러다닌다.",

            "벽에 기대 놓은 배달 상자 더미 뒤에서 고양이가 나온다. 운전자가 "
            "면을 한 젓가락 건져 올린다.",
        ],
        [
            ("편의점 앞 파라솔과 긴 의자", []),
            ("운전자가 긴 의자에 앉아 컵라면을 먹는다", ["운전자"]),
            ("통유리의 판촉 그림과 자동문", []),
        ],
    ),
    (
        "S#3. 국도 갓길 / 새벽",
        [
            "직원이 조끼를 벗어 어깨에 걸치고 갓길을 걷는다. 가로등 하나가 "
            "깜박인다.",

            "운전자의 차가 지나가며 헤드라이트가 직원의 등을 한 번 훑는다. "
            "직원은 돌아보지 않는다.",
        ],
        [
            ("가로등 아래 갓길을 걷는 직원", ["직원"]),
            ("헤드라이트가 직원의 등을 훑는다", ["직원"]),
        ],
    ),
]

#: ★엔티티 단위 스텝(entity_t2i 등)의 **선언 상한** — 선언 — 실측 전. 계획표는 이 값을
#:  쓰고, 실제가 이를 넘으면 스텝 문이 provider 앞에서 선다(돈은 안 새고 판은 미완주).
ENTITY_CAP = 20
#: 아웃룩이 생기는 원고인가 — `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 = 2
FLOOR_PLAN_CAP = 4
BACKGROUND_CAP = 8

_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


def bundles() -> List[List[int]]:
    """구간 나누기. ★**production 함수 한 곳**을 쓴다."""
    from app.modules.pipeline.grounding_chunk_plan import bundle_scenes

    return bundle_scenes([{"idx": s["scene_index"], "length": s["length"]}
                          for s in segments()])


#: 표적 장부. `at` = (씬, 원문의 완전한 이름 덩어리, 몇 번째).
EXPECTED_TARGETS: List[Dict[str, Any]] = [
    {"key": "fuel_station", "owner": "location", "anchors": "any",
     "at": [(1, "주유소", 1)], "rows_after_merge": 1, "min_occurrences": 1,
     "why": "현대 장소 — 지붕·섬·간판·직원 응대 꼴이 나라마다 다르다"},
    {"key": "pump_island", "owner": "location_part", "anchors": "any",
     "at": [(1, "주유기", 1)], "rows_after_merge": 1, "min_occurrences": 1,
     "why": "현대 고정 설비 — 섬 위 배치와 노란 쇠기둥이 지역 특유다"},
    {"key": "store", "owner": "location", "anchors": "any",
     "at": [(2, "편의점", 1)], "rows_after_merge": 1, "min_occurrences": 1,
     "why": "현대 장소 — 통유리·자동문·바깥 냉동고 조합이 지역 특유다"},
    {"key": "store_front_seat", "owner": "location_part", "anchors": "any",
     "at": [(2, "파라솔 아래 긴 의자", 1)], "rows_after_merge": 1,
     "min_occurrences": 1,
     "why": "현대 고정 설비 — 가게 앞에 내놓은 자리와 파라솔이 지역 특유다"},
    # ── 음성 대조 — **현대라서가 아니라 쉬워서** 비대상이다 ──
    {"key": "plain_prop", "owner": "prop", "anchors": "any",
     "at": [(2, "종이컵", 1)], "rows_after_merge": 1, "min_occurrences": 1,
     "why": "★음성 — 종이컵은 어디나 같다"},
    {"key": "plain_location_part", "owner": "location_part", "anchors": "any",
     "at": [(3, "가로등", 1)], "rows_after_merge": 1, "min_occurrences": 1,
     "why": "★음성 — 가로등은 형태 규정이 없다"},
]

#: 두 축과 그 **근거**. ★근거는 세계 사실에 적힌 것에서만 온다. 시대는 없다.
AXIS_BASIS: Dict[str, Dict[str, Any]] = {
    "fuel_station": {
        "hard": True, "notice": True,
        "basis": ("기둥 넷 위 평지붕과 테두리 색 띠, 섬 위에 등을 맞댄 기계, "
                  "값을 띄우는 기둥 간판, 차창으로 다가오는 직원 — 근거 없이 "
                  "그리면 다른 나라 것이 섞이고 이곳을 아는 사람은 안다"),
    },
    "pump_island": {
        "hard": True, "notice": True,
        "basis": ("낮은 섬 위에 두 대씩 등을 맞대고 양 끝에 노란 쇠기둥 — "
                  "배치가 정해져 있어 다르게 그리면 곧바로 안다"),
    },
    "store": {
        "hard": True, "notice": True,
        "basis": ("통유리 앞면과 자동 미닫이문, 유리의 큰 판촉 그림, 문 옆 "
                  "바깥 냉동고 — 조합이 지역 특유다"),
    },
    "store_front_seat": {
        "hard": True, "notice": True,
        "basis": ("가게 앞 플라스틱 탁자와 등받이 없는 긴 의자, 그 위 파라솔 — "
                  "밖에 내놓고 앉아 먹는 자리 자체가 지역 특유다"),
    },
    "plain_prop": {
        "hard": False, "notice": False,
        "basis": "종이컵은 어디나 같다 — 세계 사실에 형태 규정이 없다",
    },
    "plain_location_part": {
        "hard": False, "notice": False,
        "basis": "가로등은 형태 규정이 없다",
    },
}


def acquisition_targets() -> List[str]:
    """참조 획득을 지나야 하는 표적. ★**같은 행에서 hard AND notice**."""
    return sorted(k for k, v in AXIS_BASIS.items()
                  if v.get("hard") and v.get("notice"))


def acquisition_owners() -> set:
    by = {t["key"]: t["owner"] for t in EXPECTED_TARGETS}
    return {by[k] for k in acquisition_targets() if k in by}


def span_of(scene: int, word: str, nth: int = 1) -> Dict[str, Any]:
    """그 씬 원문에서 `nth` 번째 자리. ★없으면 **선다**."""
    body = segments()[scene - 1]["text"]
    pos, found = -1, 0
    while found < nth:
        pos = body.find(word, pos + 1)
        if pos < 0:
            raise AssertionError(f"{scene}장에 {word!r} 가 {nth}번째로 없다")
        found += 1
    return {"segment_id": f"scene-{scene}", "start": pos,
            "end": pos + len(word), "quote": word}


def expected_spans(target: Dict[str, Any]) -> List[Dict[str, Any]]:
    return [span_of(sc, w, n) for sc, w, n in target["at"]]


def assert_planted() -> None:
    """심었다고 적은 것이 **원고 안에 실제로 있는지** 본다.

    ★그리고 이 원고의 계약을 본다 — 시대는 **비어 있고** 지역은 있다 ·
    `location`·`location_part` 가 대상이다 · 음성 대조가 있다.
    """
    if ERA != "" or not REGION.strip():
        raise AssertionError("이 원고는 시대 없음·지역만 이 계약이다")
    for t in EXPECTED_TARGETS:
        expected_spans(t)                       # 없으면 여기서 선다
        if t["key"] not in AXIS_BASIS:
            raise AssertionError(f"{t['key']} 의 두 축 근거가 없다")
    got = acquisition_owners()
    if not {"location", "location_part"} <= got:
        raise AssertionError(
            f"현대 배경 갈래가 대상에 없다: {sorted(got)} — 이 원고는 "
            "location·location_part 가 두 축으로 지나라고 지은 것이다")
    neg = [k for k, v in AXIS_BASIS.items()
           if not (v.get("hard") and v.get("notice"))]
    if not neg:
        raise AssertionError("음성 대조가 없다 — 전부 양성이면 축이 무디다")
    for term in FIXTURE_ONLY_TERMS:
        if term not in manuscript() and term not in WORLD_FACTS:
            raise AssertionError(f"{term!r} 는 fixture 어디에도 없다")
