"""인물 **크기**를 견줌으로 받아 그림까지 보낸다 (2026-09-20 사용자 지시).

> "대략적으로 비교될만한 형태로 묘사하는 정도로… 표준 크기 및 덩치 등으로
>  비교하기 쉽게… 또한 프롬프트에서 비교적인 부분은 필요할듯해"

## 무엇이 없었나

정본에 **인물 크기가 아예 없었다.** 키가 성인 남자를 넘지 않는 로봇이
화면에서 3m 병기로 그려졌고 사람이 정본을 손으로 고쳐야 했다.

★막고 있던 것은 추출 팩의 「**바디 묘사 절대 금지**」다. 그 금지는
 몸매·체형 묘사를 막으려던 것인데 크기까지 같이 지웠다 — 칸만 더하면
 그 금지가 다시 지운다(Codex 감사).

## 계약

  · 크기는 **수치가 아니라 견줌** — T2I 는 센티미터를 안 받는다(실측)
  · 근거 인용이 없으면 **빈 배열** — 지어내지 않는다
  · 렌더로 나가는 것은 **근거 있는 견줌만**(`distinctive` 와 같은 게이트)
"""
from __future__ import annotations

import json
import pathlib

import pytest

from app.core.steps.entity_steps import build_build_scale_block
from app.modules.prompt_loader import load_prompt

_PACK = "entity_extractor_v2"


def _live(stem: str) -> str:
    return load_prompt(_PACK, stem)


# ── 팩 ──────────────────────────────────────────────────────────────

def test_the_pack_asks_for_size_as_a_comparison():
    body = _live("turn1_7_detail_batch")
    assert "build_scale" in body, "크기 칸을 안 묻는다"
    assert "견줌" in body or "견주" in body, "견줌으로 받으라는 말이 없다"
    assert "수치" in body, "수치를 쓰지 말라는 말이 없다"


def test_the_pack_says_it_is_not_the_banned_body_description():
    """★칸만 더하면 「바디 묘사 절대 금지」가 다시 지운다 — 관계를 적는다."""
    body = _live("turn1_7_detail_batch")
    assert "바디 묘사 절대 금지" in body, "전제 확인 — 그 금지는 남아 있다"
    flat = " ".join(body.split())          # 하드랩은 서식이지 계약이 아니다
    i = flat.find("build_scale")
    seg = flat[i:i + 700]
    assert "걸리지 않는다" in seg or "다른 축" in seg, (
        "새 칸이 그 금지의 예외임을 안 적었다 — 모델이 또 지운다")


def test_no_evidence_means_empty():
    body = _live("turn1_7_detail_batch")
    flat = " ".join(body.split())
    i = flat.find("build_scale")
    seg = flat[i:i + 700]
    assert "빈 배열" in seg and "지어내지 않는다" in seg


def test_the_schema_carries_the_field():
    """스키마가 **구조 칸**으로 받는다 — 산문에 섞이면 코드가 못 읽는다."""
    live = pathlib.Path(load_prompt.__module__ and "") if False else None
    from app.modules.prompt_loader import load_schema

    sch = load_schema(_PACK, "turn1_7_detail_batch_schema")
    it = sch["properties"]["entities"]["items"]
    assert "build_scale" in it["properties"], "스키마에 칸이 없다"
    assert "build_scale" in it["required"], (
        "strict 스키마인데 required 에 없다 — 모델이 통째로 빠뜨린다")
    item = it["properties"]["build_scale"]["items"]
    for k in ("compared_to", "relation", "source_quote"):
        assert k in item["properties"], f"{k} 칸이 없다"
    assert set(item["required"]) == {"compared_to", "relation", "source_quote"}


# ── 렌더 게이트 ─────────────────────────────────────────────────────

def test_only_quoted_comparisons_reach_the_image():
    """근거 인용이 있는 것만 그림으로 간다 — `distinctive` 와 같은 게이트."""
    rows = [
        {"compared_to": "보통 성인 남자", "relation": "머리 하나만큼 작다",
         "source_quote": "키는 성인 남자를 넘지 않는"},
        {"compared_to": "보통 성인 남자", "relation": "세 배는 크다",
         "source_quote": ""},                      # 근거 없음 — 빠진다
    ]
    block, excluded = build_build_scale_block(rows)
    assert "머리 하나만큼" in block
    assert "세 배는 크다" not in block, "근거 없는 견줌이 그림으로 갔다"
    assert len(excluded) == 1


def test_empty_input_renders_nothing():
    assert build_build_scale_block([]) == ("", [])
    assert build_build_scale_block(None) == ("", [])


def test_the_block_says_what_to_do_with_it():
    """금지형만 남기지 않는다 — **견줌이 보이게 그려라**까지 준다."""
    rows = [{"compared_to": "보통 성인", "relation": "머리 하나 작다",
             "source_quote": "성인 남자를 넘지 않는"}]
    block, _ = build_build_scale_block(rows)
    assert "견줌" in block
    assert "그린다" in block or "보이게" in block


def test_a_row_without_a_relation_is_excluded():
    """인용만 있고 **견줌 문장이 없으면** 그릴 것이 없다."""
    rows = [{"compared_to": "보통 성인", "relation": "",
             "source_quote": "성인 남자를 넘지 않는"}]
    block, excluded = build_build_scale_block(rows)
    assert block == "" and len(excluded) == 1


def test_it_is_wired_into_the_t2i_context():
    """조립부가 아니라 **소비처**가 그것을 쓰는지 본다."""
    import inspect

    from app.core.steps import entity_steps

    src = pathlib.Path(inspect.getfile(entity_steps)).read_text(
        encoding="utf-8")
    i = src.find("_sblock, _s_excluded = build_build_scale_block(")
    assert i > 0, "t2i context 에 안 실린다"
    assert "detail_block += _sblock" in src[i:i + 300]


def test_the_detail_record_keeps_the_raw_field():
    """★생산자가 칸을 **버리면** 스키마·helper 를 다 만들어도 값이 안 간다.

    `_record` 는 묶음 답과 재시도 답을 **같은 모양**으로 담는 자리다.
    여기서 `build_scale` 이 빠지면 entity_detail CP 에 값이 없고
    `EntityT2iStep` 은 언제나 `None` 을 받는다 (2026-09-20 Codex BLOCK).
    """
    import inspect
    import pathlib

    from app.core.steps import entity_steps

    src = pathlib.Path(inspect.getfile(entity_steps)).read_text(
        encoding="utf-8")
    i = src.find("def _record(ent: Dict[str, Any]) -> Dict[str, Any]:")
    assert i > 0, "전제 확인"
    seg = src[i:i + 1400]
    assert '"build_scale"' in seg, (
        "_record 가 build_scale 을 안 담는다 — CP 에 값이 안 남는다")
    # ★담는 자리는 **하나뿐**이어야 한다 — 묶음 답과 재시도 답이 다른
    #  모양으로 담기면 한쪽만 칸이 빠진다(그 사고가 실제로 있었다:
    #  재시도 갈래가 distinctive_visual_traits 를 버렸다).
    assert src.count("= _record(ent)") == 1, (
        "담는 자리가 하나가 아니다 — 갈래마다 모양이 갈린다")


def test_the_scale_reaches_stable_traits_through_visual_traits():
    """샷 문안은 `stable_traits` 를 읽는다 — T2I context 만으로는 안 닿는다.

    새 DB 칼럼을 만들지 않고 **기존 배선**(visual_traits → sync →
    stable_traits → 샷 문안)을 쓰는 최소안이다.
    """
    from app.core.steps.entity_steps import scale_trait_sentences

    rows = [{"compared_to": "보통 성인 남자",
             "relation": "머리 하나만큼 작다",
             "source_quote": "키는 성인 남자를 넘지 않는"}]
    lines = scale_trait_sentences(rows)
    assert len(lines) == 1
    assert "보통 성인 남자" in lines[0] and "머리 하나만큼 작다" in lines[0]
    # 근거 없는 줄은 문장이 안 된다
    assert scale_trait_sentences(
        [{"compared_to": "x", "relation": "y", "source_quote": ""}]) == []


def test_the_t2i_save_carries_both_the_sentence_and_the_raw():
    """T2I 저장이 **문장과 원자료를 둘 다** 넘긴다 — 근거와 렌더는 다르다."""
    import inspect
    import pathlib

    from app.core.steps import entity_steps

    src = pathlib.Path(inspect.getfile(entity_steps)).read_text(
        encoding="utf-8")
    i = src.find('"t2i_prompt": detail.get("t2i_prompt", ""),')
    assert i > 0, "전제 확인"
    seg = src[max(0, i - 400):i + 300]
    assert "_traits_with_scale(src)" in seg, "견줌 문장이 traits 에 안 실린다"
    assert '"build_scale": src.get("build_scale"' in seg, "원자료가 안 남는다"


def test_one_gate_serves_both_consumers():
    """게이트를 **두 곳에 적지 않는다** — 한쪽만 고쳐지는 부류를 막는다."""
    import inspect
    import pathlib

    from app.core.steps import entity_steps

    src = pathlib.Path(inspect.getfile(entity_steps)).read_text(
        encoding="utf-8")
    assert src.count("def _scale_rows(") == 1
    assert src.count("_scale_rows(build_scale)") == 2, (
        "블록과 문장이 같은 게이트를 쓰지 않는다")
