"""seed 품질 2R — LLM 3종 프롬프트 변형 저작 결정론 계약 (2026-07-16).

진단 4건 매핑:
  ① 규모 위임 저작 = author_sys 계약(팩) — 여기서는 수치 하드코딩 부재만 잠금
  ② conformance 축 = brief 조립 + addendum 존재
  ③ THE LOCATION 저작 = assemble 이 raw 씬 헤딩을 받지 않음(location_line_en 만)
  ④ 원어 근거 = author 입력에 시나리오 원문 전문 무절단 포함
"""
import pytest

from app.modules.pipeline.seed_prompt_variants import (
    AUTHOR_MODEL,
    assemble_variant_roll_prompts,
    build_author_schema,
    build_author_user_content,
    build_conformance_prompt,
    load_variant_addendums,
    resolve_variants_pack_version,
    validate_variant_output,
)


def _norm(text: str) -> str:
    """md 줄바꿈이 계약 문구를 쪼개므로 공백 정규화 후 substring 비교."""
    return " ".join(text.lower().split())


def _valid_output(count: int = 3) -> dict:
    return {
        "location_line_en": "a low-rise residential alley in a hillside "
                            "neighborhood",
        "conformance_brief_en": "one small annex on the roof slab; openings "
                                "exactly as evidenced; massing typical of "
                                "the building type",
        "variants": [
            {
                "variant_id": f"v{i + 1}",
                "approach_ko": f"접근 {i + 1}",
                "evidence_ko": ["원문 인용 예시"] if i == 0 else [],
                "prompt_en": f"variant body number {i + 1} describing the "
                             "structure massing and openings",
            }
            for i in range(count)
        ],
    }


def test_unknown_pack_version_raises():
    with pytest.raises(ValueError):
        resolve_variants_pack_version("99")


def test_author_model_is_sol_alias():
    assert AUTHOR_MODEL == "gpt"


def test_author_schema_locks_variant_count_and_ids():
    schema = build_author_schema(3)
    v = schema["properties"]["variants"]
    assert v["minItems"] == 3 and v["maxItems"] == 3
    assert v["items"]["properties"]["variant_id"]["enum"] == [
        "v1", "v2", "v3"]
    assert set(v["items"]["required"]) == {
        "variant_id", "approach_ko", "evidence_ko", "prompt_en"}
    assert set(schema["required"]) == {
        "location_line_en", "conformance_brief_en", "variants"}
    assert schema["additionalProperties"] is False


def test_author_user_content_full_scene_texts_uncut():
    """④ 원어 질감 — 씬 원문 전문 무절단 + 근거 입력 전부 포함."""
    long_scene = "긴 원문 문장. " * 800  # 절단 시 깨지는 길이
    content = build_author_user_content(
        scene_indices=[4, 17],
        scene_texts={4: long_scene, 17: "두 번째 씬 원문"},
        scene_headings={4: "S#4. SAMPLE / D", 17: "S#17. SAMPLE / N"},
        structure_desc="A structure on a slab.\n- stair: along one side",
        interior_note_en="a living room and a bathroom",
        exterior_note_en="aged brick, weathered cement",
        world_anchor=" — a SAMPLE country city",
        excluded_transient_en="a parked cart",
        count=3,
    )
    assert long_scene.strip() in content          # 무절단
    assert "두 번째 씬 원문" in content
    assert "S#4. SAMPLE / D" in content           # 헤딩=명명 evidence 로 전달
    assert "A structure on a slab." in content
    assert "a living room and a bathroom" in content
    assert "aged brick, weathered cement" in content
    assert "a SAMPLE country city" in content
    assert "a parked cart" in content


def test_author_user_content_missing_scene_text_fail_closed():
    with pytest.raises(ValueError):
        build_author_user_content(
            scene_indices=[4],
            scene_texts={},
            scene_headings={},
            structure_desc="x",
            interior_note_en="",
            exterior_note_en="",
            world_anchor="",
            excluded_transient_en="",
            count=3,
        )


def test_validate_variant_output_pass():
    assert validate_variant_output(_valid_output(), 3) == []


def test_validate_variant_output_violations():
    bad_count = _valid_output()
    bad_count["variants"] = bad_count["variants"][:2]
    assert validate_variant_output(bad_count, 3)

    dup = _valid_output()
    dup["variants"][1]["prompt_en"] = dup["variants"][0]["prompt_en"]
    assert validate_variant_output(dup, 3)

    empty_body = _valid_output()
    empty_body["variants"][2]["prompt_en"] = "   "
    assert validate_variant_output(empty_body, 3)

    wrong_ids = _valid_output()
    wrong_ids["variants"][0]["variant_id"] = "v9"
    assert validate_variant_output(wrong_ids, 3)

    no_brief = _valid_output()
    no_brief["conformance_brief_en"] = ""
    assert validate_variant_output(no_brief, 3)

    no_loc = _valid_output()
    no_loc["location_line_en"] = " "
    assert validate_variant_output(no_loc, 3)

    assert validate_variant_output("not a dict", 3)
    assert validate_variant_output({"variants": "x"}, 3)


def test_assemble_roll_prompts_invariant_clauses_and_order():
    """불변 안전절=코드 조립(LLM 저작이 못 떨어뜨림) + 라벨↔변형 순서 고정."""
    data = _valid_output()
    prompts = assemble_variant_roll_prompts(
        data=data,
        labels=["A", "B", "C"],
        world_anchor=" — a SAMPLE country city",
        excluded_transient_en="a parked cart",
        seed_pack_version="4",
    )
    assert list(prompts) == ["A", "B", "C"]
    for i, lab in enumerate(["A", "B", "C"]):
        p = prompts[lab]
        n = _norm(p)
        assert f"variant body number {i + 1}" in p          # 순서 고정
        assert p.startswith("A photographic plate — a SAMPLE country city.")
        assert "look authority" in n                        # seed_head v4
        assert "no cutaway" in n
        assert "THE LOCATION: a low-rise residential alley" in p  # ③ 저작본
        assert "S#" not in p                                # ③ raw 헤딩 부재
        assert "a parked cart" in p                         # EXCLUDED 불변절
        assert "no people" in n                             # no_people 불변절
        # 규모 수치 하드코딩 부재(코드/팩 어디에도 층수 수치 주입 없음)
        assert "3-story" not in n and "three-story" not in n


def test_assemble_roll_prompts_empty_excluded_omits_clause():
    data = _valid_output()
    prompts = assemble_variant_roll_prompts(
        data=data,
        labels=["A", "B", "C"],
        world_anchor="",
        excluded_transient_en="",
        seed_pack_version="4",
    )
    assert "EXCLUDED" not in prompts["A"]


def test_assemble_roll_prompts_label_count_mismatch_raises():
    with pytest.raises(ValueError):
        assemble_variant_roll_prompts(
            data=_valid_output(3),
            labels=["A", "B"],
            world_anchor="",
            excluded_transient_en="",
            seed_pack_version="4",
        )


def test_conformance_prompt_contains_brief_location_excluded():
    """② 판정 축 — judge/critique 가 공유 불변 사실 대비 검출 가능해야."""
    p = build_conformance_prompt(
        data=_valid_output(),
        excluded_transient_en="a parked cart",
    )
    n = _norm(p)
    assert "openings exactly as evidenced" in n
    assert "the location: a low-rise residential alley" in n
    assert "a parked cart" in p
    # v1/v2=기존 절대 배제 문구 byte 유지 (Codex v3 리뷰 HIGH-1 잠금)
    assert "EXCLUDED — TRANSIENT ELEMENTS (never depict): a parked cart" \
        in p
    p2 = build_conformance_prompt(
        data=_valid_output(), excluded_transient_en="")
    assert "never depict" not in _norm(p2)


def test_conformance_prompt_v3_scoped_exclusion():
    """Codex v3 리뷰 HIGH-1 — v3 브리프=생성과 동일 scoped 배제 문구.

    생성 계약이 허용한 ordinary generic equivalents(생활 전형)를
    판정·수정이 'never depict' 로 도로 제거하지 못하게 잠근다.
    """
    p = build_conformance_prompt(
        data=_valid_output(),
        excluded_transient_en="a parked cart",
        variants_pack_version="3",
    )
    n = _norm(p)
    assert "scene transients" in n
    assert "does not forbid the ordinary" in n
    assert "a parked cart" in p
    assert "never depict) :" not in n
    assert "excluded — transient elements (never depict)" not in n
    # 배제 없음=절 생략 (v3 도 동일)
    p2 = build_conformance_prompt(
        data=_valid_output(), excluded_transient_en="",
        variants_pack_version="3")
    assert "scene transients" not in _norm(p2)


def test_addendums_conformance_first_contract():
    add = load_variant_addendums("1")
    jn = _norm(add["judge_addendum"])
    cn = _norm(add["critique_addendum"])
    assert "conformance brief" in jn
    assert "before" in jn and "aesthetic" in jn   # 미학보다 선행 판정
    assert "conformance brief" in cn
    # ② 개구부 증식 검출 계약 (generic — 수치·대상 하드코딩 없음)
    assert "opening" in jn and "opening" in cn


def test_variant_judge_texts_seed_only_contract():
    """Codex 재리뷰 BLOCKING-1 잠금 — v2 판정 system 은 seed 전용:
    film-still 계약(동일 프롬프트 전제/샷 프레이밍/인물 identity·
    wardrobe/pose·carried) 부재 + conformance 선행 + 변형 전제 명시."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    texts = load_variant_judge_texts(3, "2")
    js = _norm(texts["judge_sys"])
    cs = _norm(texts["critique_sys"])
    # 포맷 변수 치환 확인
    assert "three candidate photographic plates" in js
    assert "a, b, c" in js
    # 변형 전제+conformance 선행
    assert "different authored phrasing" in js
    assert "brief conformance" in js
    assert "hard violations" in js
    # 개구부·규모 검출 축 (진단 ①②)
    assert "opening counts" in js and "opening counts" in cs
    assert "massing" in js and "massing" in cs
    # film-still 계약 부재 잠금 — 구별력 있는 문구 단위(substring 함정
    # 회피: 'propose'⊃pose 류)
    for forbidden in ("all generated from that exact prompt",
                      "shot text", "wardrobe", "carried-state",
                      "immobility", "framing and shot scale",
                      "close-up", "over-the-shoulder"):
        assert forbidden not in js, forbidden
        assert forbidden not in cs, forbidden


def test_author_user_content_prior_violations_block():
    """Codex 재리뷰 NARROW-4 — 재시도 입력에 교정 블록 병기."""
    kw = dict(
        scene_indices=[4],
        scene_texts={4: "원문"},
        scene_headings={4: "S#4. SAMPLE / D"},
        structure_desc="A structure.",
        interior_note_en="",
        exterior_note_en="",
        world_anchor="",
        excluded_transient_en="",
        count=3,
    )
    base = build_author_user_content(**kw)
    assert "PREVIOUS ATTEMPT REJECTED" not in base
    retry = build_author_user_content(
        **kw, prior_violations=["v2: prompt_en 공백", "  ", ""])
    assert "PREVIOUS ATTEMPT REJECTED" in retry
    assert "- v2: prompt_en 공백" in retry
    assert retry.startswith(base)  # 씬 전문 등 기존 입력은 그대로 유지


def test_assemble_v3_location_scout_contract():
    """v3(로케이션 헌팅, 육안 반려 반영): 동네 맥락 포함+고립 금지+
    생활감 위임+씬 transient 만 배제+world_anchor 원문(인물 문구) 미병기.
    """
    data = _valid_output()
    prompts = assemble_variant_roll_prompts(
        data=data,
        labels=["A", "B", "C"],
        world_anchor=" — a SAMPLE country; all people are SAMPLE",
        excluded_transient_en="a parked cart",
        seed_pack_version="4",
        variants_pack_version="3",
    )
    for i, lab in enumerate(["A", "B", "C"]):
        p = prompts[lab]
        n = _norm(p)
        assert f"variant body number {i + 1}" in p
        # 로케이션 헌팅 head — 동네 맥락+고립 금지+look authority 유지
        assert "location-scouting photograph" in n
        assert "lived-in neighborhood" in n
        assert "never an isolated building" in n
        assert "look authority" in n and "no cutaway" in n
        # v4 고립 플레이트 계약 문구 부재
        assert "do not invent extra surroundings" not in n
        # world_anchor 원문(인물 문구) 미병기 — 사람 언급 유입 차단
        assert "all people are sample" not in n
        # 배제=씬 순간 연출만, 생활 전형은 허용 명시
        assert "scene transients" in n
        assert "a parked cart" in p
        assert "does not forbid the ordinary" in n
        # plate rules: 무인 유지+생활 흔적 위임
        assert "no people and no animals" in n
        assert "signs of habitation" in n
        assert "THE LOCATION: a low-rise residential alley" in p
        assert "S#" not in p


def test_author_sys_v3_scout_style_contract():
    """v3 저작 계약: 층수·색·자재 확정 저작+소설체 금지+동네 포함."""
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    sys3 = _norm(load_author_sys(3, "3"))
    assert "location scout" in sys3
    assert "storey count" in sys3 and "colors and materials" in sys3
    assert "commit to one definite structure" in sys3
    assert "never narrative or literary prose" in sys3
    assert "lived-in neighborhood" in sys3
    assert "no one would scout a building without its neighborhood" \
        in sys3
    # 과제약이던 '정확 수치 발명 금지' 문구 제거(확정 저작 위임으로 대체)
    assert "never invent exact storey counts" not in sys3


def test_variant_judge_texts_v3_neighborhood_axis():
    """v3 판정: 실존 거주 동네 축(고립·유기·시설감=위반)+film-still 부재."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    texts = load_variant_judge_texts(3, "3")
    js = _norm(texts["judge_sys"])
    cs = _norm(texts["critique_sys"])
    assert "inhabited" in js and "neighborhood" in js
    assert "isolated structure" in js
    assert "abandoned" in js and "institutional" in js
    assert "storey count" in js
    assert "isolated" in cs and "neighborhood" in cs
    assert "erasing its neighborhood" in cs
    for forbidden in ("all generated from that exact prompt",
                      "shot text", "wardrobe", "carried-state",
                      "immobility", "framing and shot scale",
                      "close-up", "over-the-shoulder"):
        assert forbidden not in js, forbidden
        assert forbidden not in cs, forbidden


def test_v4_regional_authenticity_contract():
    """v4(육안: '한국에 저런 색 없어'): 색·재질=지역 실물 전형 계약 —
    저작·판정·수정 3계약 전부에 존재, 특정 색/국가 하드코딩은 0."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_author_sys,
        load_variant_judge_texts,
    )

    sys4 = _norm(load_author_sys(3, "4"))
    assert "regional authenticity of every color and material" in sys4
    assert "water tanks" in sys4
    assert "foreign, invented or merely picturesque" in sys4
    assert "most common real-world standard" in sys4

    texts = load_variant_judge_texts(3, "4")
    js = _norm(texts["judge_sys"])
    cs = _norm(texts["critique_sys"])
    assert "regional authenticity" in js
    assert "tanks" in js and "pavement" in js
    assert "would not exist for that element type" in cs
    assert "regional standard finish" in cs
    # v3 계약(동네 축 등)은 승계 유지
    assert "lived-in neighborhood" in _norm(
        load_author_sys(3, "4"))
    assert "isolated structure" in js


def test_v5_period_anchored_authenticity_contract():
    """Codex v4 리뷰 HIGH-1 — 색·재질 전형 기준=세계관의 지역+시대.

    'today' 하드코딩 부재(시대극에서 현대 마감 승인 차단),
    anachronistic 금지, judge/critique 도 region+period 기준.
    """
    from app.modules.pipeline.seed_prompt_variants import (
        load_author_sys,
        load_variant_judge_texts,
    )

    sys5 = _norm(load_author_sys(3, "5"))
    assert "time period the world anchor and the scenario establish" \
        in sys5
    assert "in that region and period" in sys5
    assert "anachronistic" in sys5
    assert " today" not in sys5.replace("then.", "")
    # location_line=지역+시대 앵커, 인물 문구 금지
    assert "its time period" in sys5
    assert "never include statements about people" in sys5

    texts = load_variant_judge_texts(3, "5")
    js = _norm(texts["judge_sys"])
    cs = _norm(texts["critique_sys"])
    assert "region and time period the brief and the location establish" \
        in js
    assert "anachronistic" in js and "anachronistic" in cs
    assert "in that period" in cs
    assert " today" not in js and " today" not in cs
    # v4 지역 사실성·v3 동네 축 승계
    assert "regional authenticity" in js
    assert "isolated structure" in js


def test_packs_are_scenario_neutral():
    """팩 파일에 구조물 종류·수치 하드코딩 0 (규율: 시나리오 의존 금지).

    Codex 리뷰 HIGH-2: 하드코딩 tuple 순회가 신규 활성 팩(v6)을 빠뜨리던
    사각 제거 — VARIANTS_PACK_VERSION_MAP 전체 selector 를 순회해 이후
    팩도 자동 잠금.
    """
    from pathlib import Path

    from app.modules.pipeline.seed_prompt_variants import (
        VARIANTS_PACK_VERSION_MAP,
    )

    repo = Path(__file__).resolve().parents[3]
    for sel in sorted(VARIANTS_PACK_VERSION_MAP):
        ver = resolve_variants_pack_version(sel)
        files = list(
            (repo / "prompts" / "_base" / "seed_prompt_variants"
             / ver).glob("*.md"))
        assert len(files) >= 3
        for f in files:
            text = f.read_text(encoding="utf-8").lower()
            for word in ("rooftop", "villa", "compact", "tiny", "3-story",
                         "three-story", "two-story", "korea"):
                assert word not in text, f"{f.name} 에 {word!r} 하드코딩"


# ── v6 (E2E11 ①): 규모 계약 — variants 경로 코드 조립 ──────────────


def test_assemble_v6_scale_clauses_with_interior():
    """v6: human_scale 상시 + scale_clause/INTERIOR(증거 있을 때) 조립."""
    prompts = assemble_variant_roll_prompts(
        data=_valid_output(),
        labels=["A", "B", "C"],
        world_anchor="",
        excluded_transient_en="a parked cart",
        seed_pack_version="5",
        variants_pack_version="6",
        interior_note_en="SAMPLE interior: living room, kitchen, bedroom",
    )
    for p in prompts.values():
        n = _norm(p)
        assert "human-scale calibration" in n
        assert "structure scale spec" in n
        assert "INTERIOR: SAMPLE interior: living room" in p
        # 규모 절이 배제 절보다 앞 (THE LOCATION 직후 계약 블록)
        assert p.index("HUMAN-SCALE") < p.index("a parked cart")
        # v5 와 동일한 로케이션 헌팅 계약은 유지
        assert "location-scouting photograph" in n


def test_assemble_v6_empty_interior_keeps_human_scale_only():
    """v6: interior 증거 결손 → INTERIOR/scale_clause 생략(dangling 금지),
    human_scale 앵커는 상시 유지 — E2E11 '결손 시 규모 계약 0' 재발 차단."""
    prompts = assemble_variant_roll_prompts(
        data=_valid_output(),
        labels=["A", "B", "C"],
        world_anchor="",
        excluded_transient_en="",
        seed_pack_version="5",
        variants_pack_version="6",
        interior_note_en="   ",
    )
    for p in prompts.values():
        n = _norm(p)
        assert "human-scale calibration" in n
        assert "structure scale spec" not in n
        assert "INTERIOR:" not in p


def test_assemble_v5_ignores_interior_note_byte_identical():
    """v5 이하: interior_note_en 전달해도 기존 조립 byte-identical."""
    kw = dict(
        data=_valid_output(),
        labels=["A", "B", "C"],
        world_anchor="",
        excluded_transient_en="a parked cart",
        seed_pack_version="5",
        variants_pack_version="5",
    )
    base = assemble_variant_roll_prompts(**kw)
    with_note = assemble_variant_roll_prompts(
        **kw, interior_note_en="SAMPLE interior evidence")
    assert base == with_note
    for p in with_note.values():
        assert "HUMAN-SCALE" not in p and "INTERIOR:" not in p


def test_conformance_v6_scale_clauses():
    """v6 conformance 브리프: 판정·critique 규모 축 확보 + v5 불변."""
    p6 = build_conformance_prompt(
        data=_valid_output(),
        excluded_transient_en="",
        variants_pack_version="6",
        interior_note_en="SAMPLE interior: two rooms and a hall",
    )
    n6 = _norm(p6)
    assert "human-scale calibration" in n6
    assert "structure scale spec" in n6
    assert "INTERIOR: SAMPLE interior: two rooms" in p6
    p5 = build_conformance_prompt(
        data=_valid_output(),
        excluded_transient_en="",
        variants_pack_version="5",
        interior_note_en="SAMPLE interior: two rooms and a hall",
    )
    assert "HUMAN-SCALE" not in p5 and "INTERIOR:" not in p5


def test_v6_pack_copy_integrity_and_scale_stems_locked():
    """v6 팩=v5 사본+scale 스템 2종. scale_clause=structure_seed v5 원문
    byte 사본(드리프트 잠금). human_scale_clause 는 Codex 리뷰 HIGH-2 로
    개정 — 구조물 종류 예시('rooftop room …=one small room') 제거,
    '한 입면 개구부 수로 수용 규모 추론 금지+INTERIOR 증거 우선' 계약."""
    from pathlib import Path

    repo = Path(__file__).resolve().parents[3]
    v5 = repo / "prompts" / "_base" / "seed_prompt_variants" / (
        resolve_variants_pack_version("5"))
    v6 = repo / "prompts" / "_base" / "seed_prompt_variants" / (
        resolve_variants_pack_version("6"))
    for f in v5.glob("*.md"):
        assert (v6 / f.name).read_text(encoding="utf-8") == f.read_text(
            encoding="utf-8"), f"{f.name} 이 v5 와 다름"
    seed_v5 = repo / "prompts" / "_base" / "structure_seed" / "5.202607221100"
    assert (v6 / "scale_clause.md").read_text(encoding="utf-8") == (
        seed_v5 / "scale_clause.md").read_text(encoding="utf-8"), (
        "scale_clause 가 structure_seed v5 원문과 드리프트")
    hs = _norm((v6 / "human_scale_clause.md").read_text(encoding="utf-8"))
    # 개구부 수 → 규모 추론 금지 + INTERIOR 증거 우선 (HIGH-2 계약)
    assert "never infer a structure's total capacity" in hs
    assert "outranks any impression taken from the exterior" in hs
    # 규모 축소 유도 예시 문구 부재 (구조물 종류 명사는
    # test_packs_are_scenario_neutral 전 selector 순회가 잠근다)
    assert "barely one small room" not in hs


# ── 팩 v7 (E2E13 fix②): 규모 근거·본체 정합·전면부 지역 전형 ──────────


def test_v7_author_sys_has_size_and_frontage_contracts():
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    s = load_author_sys(3, "7")
    assert "SIZE IS EVIDENCE-DERIVED, NEVER AN ADJECTIVE" in s
    assert "THE MAIN MASS SUPPORTS ITS PARTS" in s
    assert "REGIONAL FRONTAGE TYPOLOGY" in s
    # 기존 계약 승계
    assert "REGIONAL AUTHENTICITY OF EVERY COLOR AND MATERIAL" in s
    assert "COMMIT TO ONE DEFINITE STRUCTURE" in s


def test_v6_author_sys_unchanged_by_v7():
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    s = load_author_sys(3, "6")
    assert "SIZE IS EVIDENCE-DERIVED, NEVER AN ADJECTIVE" not in s
    assert "REGIONAL FRONTAGE TYPOLOGY" not in s


def test_v7_scale_variant_pack_membership():
    """v7 도 규모 계약 조립 팩 — v6 계약 승계."""
    from app.modules.pipeline.seed_prompt_variants import (
        _SCALE_VARIANT_PACKS,
        resolve_variants_pack_version,
    )

    assert "7" in _SCALE_VARIANT_PACKS
    assert resolve_variants_pack_version("7").startswith("7.")


# ── 팩 v8 (캔ary 육안 3건): 방 전수 규모·간판 문안·생활감 ─────────────


def test_v8_author_sys_room_inventory_signage_habitation():
    """v8=4e565239 복원판(지시문 전용) — in-place 수정분은 v9 소유."""
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    s = load_author_sys(3, "8")
    assert "ROOM INVENTORY FROM THE SCENARIO TEXT" in s
    assert "EVERYDAY HABITATION IS PART OF THE PLACE" in s
    assert "PRESENT and LEGIBLE" in s
    # v7 계약 승계
    assert "SIZE IS EVIDENCE-DERIVED, NEVER AN ADJECTIVE" in s
    assert "REGIONAL FRONTAGE TYPOLOGY" in s


def test_v8_judge_generic_habitation_required():
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    t = load_variant_judge_texts(3, "8")
    j = t["judge_sys"]
    assert "SPECIFIC staged items" in j
    assert "REQUIRED, never a violation" in j
    assert "In-world signage" in j
    c = t["critique_sys"]
    assert "required" in c and "signs of habitation" in c


# ── 팩 v9 (Codex v8b 재리뷰): v8 수정분 재발행+evidence-bound 구조화 ──


def test_v9_author_sys_structured_inventory_signage_contracts():
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    s = load_author_sys(3, "9")
    assert "ROOM INVENTORY FROM THE SCENARIO TEXT" in s
    assert "room_inventory_applicable" in s
    assert "scene_index" in s
    assert "INTERIOR SCENES" in s
    assert "signage_required" in s and "signage_reason_ko" in s
    assert "signage_text" in s
    # 유형 중립 생활감(HIGH-4)·창 수 발명 금지(BLOCKING-2) 승계
    assert "SIGNS OF ACTIVE USE ARE PART OF THE PLACE" in s
    assert "NEVER by inventing" in s
    assert "enumerate them and never reproduce" in s
    # 시설 유형 일반화(v8b ④): 상업 한정 'business name' 문구 폐기
    assert "appropriate to that facility type" in s
    # v7/v8 계약 승계
    assert "SIZE IS EVIDENCE-DERIVED, NEVER AN ADJECTIVE" in s
    assert "REGIONAL FRONTAGE TYPOLOGY" in s


def test_v9_judge_type_appropriate_use_required():
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    t = load_variant_judge_texts(3, "9")
    j = t["judge_sys"]
    assert "SPECIFIC staged items" in j
    assert "REQUIRED, never a violation" in j
    assert "type-appropriate" in j
    assert "In-world signage" in j
    c = t["critique_sys"]
    assert "type-appropriate signs" in c
    assert "lifeless-place violation" in c


def test_v8_stems_unchanged_by_v9():
    """v8 복원 잠금 — 구조화 필드·유형 중립 문구는 v9 전용."""
    from app.modules.pipeline.seed_prompt_variants import load_author_sys

    s8 = load_author_sys(3, "8")
    assert "room_inventory_applicable" not in s8
    assert "signage_required" not in s8
    assert "INTERIOR SCENES" not in s8
    assert "SIGNS OF ACTIVE USE ARE PART OF THE PLACE" not in s8


def test_v9_inventory_pack_membership():
    """구조화 계약 게이트=v9+ (v8 은 복원으로 지시문 전용).

    ★집합을 통째로 고정하지 않는다 — 팩이 발행될 때마다 원소가 늘어나
    단언이 깨진다(v11~v13 발행에서 실제로 깨졌다). 계약은 "v9 이상은
    구조화 게이트를 탄다"이므로 경계만 검사한다.
    """
    from app.modules.pipeline.seed_prompt_variants import (
        _INVENTORY_PACKS,
        _SCALE_VARIANT_PACKS,
    )

    assert {"9", "10"} <= _INVENTORY_PACKS
    assert "8" not in _INVENTORY_PACKS
    assert all(int(v) >= 9 for v in _INVENTORY_PACKS)
    assert "9" in _SCALE_VARIANT_PACKS
    assert resolve_variants_pack_version("9").startswith("9.")


# ── 팩 v10 (슬라이스 E 육안 — 주거 생활감 sterile 근본 대응) ──


def test_v10_judge_lexicographic_structure_then_active_use():
    """judge — axis 3 lexicographic(Codex 재리뷰 HIGH-2): 구조 위반
    수·심각도 최소화 우선 → 구조 동급 후보 사이에서만 active-use
    tie-break. sterile 은 구조 tie 를 못 이기고, 생활감은 구조 위반을
    면책 못함(구조 재건 역전 결정론 차단). axis 2 는 v9 원문 유지."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    t = load_variant_judge_texts(3, "10")
    j = t["judge_sys"]
    assert "REQUIRED whenever the brief commits active use" in j
    assert "FIRST minimise committed structural violations" in j
    assert (
        "only between candidates whose structural\n"
        "   conformance is equal" in j
    )
    assert "sterile target never wins a structural tie" in j
    assert "never excuse a structural violation" in j
    # 객체 열거 금지 유지 — 무엇으로 보일지는 후보 자율 + verdict 명시
    assert "candidate's own choice and never judged" in j
    assert "state in\n   the verdict" in j
    # 브리프의 empty/derelict 사실 우선 승계 + axis 2 trump 문구 부재
    assert "empty, derelict or institutional" in j
    assert "must equally read as currently in use" not in j
    assert "equal standing" not in j
    assert "In-world signage" in j


def test_v10_critique_bare_target_threshold():
    """critique — 'total absence' 문턱을 타깃(표면·출입부·옥외 영역)
    bare/미사용 검출로 강화 + 수정=unstaged 일상 흔적(선택은 이미지).
    브리프 active-use 커밋 조건부(빈집·폐시설 명시 시 그 사실 우선)."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )

    c = load_variant_judge_texts(3, "10")["critique_sys"]
    assert "when the brief commits active use" in c
    assert "reads bare, unused or freshly emptied" in c
    assert "lifeless-place violation" in c
    assert "their selection left to the image itself" in c
    # 브리프가 공지/유기 시설을 명시하면 그 사실이 우선(승계)
    assert "empty, derelict or institutional" in c


def test_v10_plate_rules_active_use_conditional_fact():
    """plate_rules — 생활 흔적 '환영(방임)'→브리프 active-use 커밋
    조건부 사실 서술(Codex BLOCKING-2: 무조건 서술은 최후미 배치라
    브리프의 empty/derelict 예외를 덮음). 금지절 승계, 객체 열거 0."""
    from app.modules.prompt_loader import load_prompt

    pr = load_prompt(
        "seed_prompt_variants", "plate_rules",
        version=resolve_variants_pack_version("10"))
    assert "no people and no animals" in pr
    assert "are NOT staging" in pr
    assert "when\nthe SHARED CONFORMANCE BRIEF commits current active use" in pr
    assert "accumulated traces of the use they serve" in pr
    # Codex 재리뷰 HIGH-3: 위반=committed use 와 '불일치'하는 bare 만 —
    # 신축(freshly built) 자체를 위반으로 두지 않음
    assert (
        "reads bare or emptied out in a way inconsistent with that "
        "committed\nactive use is wrong" in pr
    )
    assert "freshly built" not in pr
    assert "image's own choice" in pr
    # 브리프 명시 상태·연식(신축 포함)은 항상 보존 사실
    assert "The brief's own statements always win over this\nrule" in pr
    assert "any committed age or\ncondition, including new construction" in pr
    # v9 의 방임 문구는 제거됨
    assert "are welcome" not in pr


def test_assemble_v10_scale_inventory_clauses_intact():
    """NARROW-4: v10 조립에 scale(HUMAN-SCALE/SCALE SPEC)+inventory
    (ROOM CAPACITY) 절이 실제 포함 — selector membership 누락 잠금."""
    from app.modules.pipeline.seed_prompt_variants import (
        _INVENTORY_PACKS,
        _SCALE_VARIANT_PACKS,
    )

    assert "10" in _SCALE_VARIANT_PACKS
    assert "10" in _INVENTORY_PACKS
    data = _valid_output()
    data["room_inventory"] = [
        {"label_en": f"room {i}", "scene_index": 1,
         "evidence_ko": "원문 인용"} for i in range(5)
    ]
    prompts = assemble_variant_roll_prompts(
        data=data,
        labels=["A", "B", "C"],
        world_anchor="",
        excluded_transient_en="a parked cart",
        seed_pack_version="5",
        variants_pack_version="10",
        interior_note_en="SAMPLE interior: living room, kitchen, bedroom",
    )
    for p in prompts.values():
        n = _norm(p)
        assert "human-scale calibration" in n
        assert "structure scale spec" in n
        assert "room capacity" in n
        assert "INTERIOR: SAMPLE interior: living room" in p
        # v10 plate_rules 조건부 사실 서술이 최후미 계약으로 포함
        assert "SHARED CONFORMANCE BRIEF commits current active use" in p


def test_v10_other_stems_byte_identical_to_v9():
    """v10 개정=judge/critique/plate_rules 3파일만 — author_sys 등
    나머지는 v9 byte-identical (최소 diff 계약)."""
    from app.modules.prompt_loader import load_prompt

    same = [
        "author_sys", "scout_head", "excluded_head", "human_scale_clause",
        "scale_clause", "room_capacity_clause", "signage_clause",
    ]
    v9 = resolve_variants_pack_version("9")
    v10 = resolve_variants_pack_version("10")
    for name in same:
        kw = {"count_word": "THREE"} if name == "author_sys" else {}
        assert load_prompt(
            "seed_prompt_variants", name, version=v9, **kw
        ) == load_prompt(
            "seed_prompt_variants", name, version=v10, **kw
        ), name


def test_v9_stems_unchanged_by_v10():
    """v9 잠금 — v10 신규 문구는 v9 에 없음(발행본 불변)."""
    from app.modules.pipeline.seed_prompt_variants import (
        load_variant_judge_texts,
    )
    from app.modules.prompt_loader import load_prompt

    t9 = load_variant_judge_texts(3, "9")
    assert "never wins a structural tie" not in t9["judge_sys"]
    assert "reads bare, unused or freshly emptied" not in t9["critique_sys"]
    pr9 = load_prompt(
        "seed_prompt_variants", "plate_rules",
        version=resolve_variants_pack_version("9"))
    assert "are NOT staging" not in pr9


def test_v7_stems_unchanged_by_v8():
    from app.modules.pipeline.seed_prompt_variants import (
        load_author_sys,
        load_variant_judge_texts,
    )

    s7 = load_author_sys(3, "7")
    assert "ROOM INVENTORY FROM THE SCENARIO TEXT" not in s7
    j7 = load_variant_judge_texts(3, "7")["judge_sys"]
    assert "REQUIRED, never a violation" not in j7


_V9_TEXTS = {3: "여기 원문 A 있음.", 5: "여기 원문 B 있음.",
             7: "여기 원문 C 있음."}


def _v9_author(**over):
    d = {
        "location_line_en": "A SAMPLE place line.",
        "conformance_brief_en": "SAMPLE brief.",
        "room_inventory_applicable": True,
        "room_inventory": [
            {"label_en": "SAMPLE room A", "scene_index": 3,
             "evidence_ko": "원문 A"},
            {"label_en": "SAMPLE room B", "scene_index": 5,
             "evidence_ko": "원문 B"},
        ],
        "lettering_text": "",
        "variants": [
            {"variant_id": f"v{i+1}", "approach_ko": "접근",
             "evidence_ko": ["근거"], "prompt_en": f"BODY {i}"}
            for i in range(3)
        ],
    }
    d.update(over)
    return d


def test_v9_schema_and_validator_inventory_contract():
    """Codex BLOCKING-1 + v8b ③④: 구조화 evidence-bound fail-closed."""
    from app.modules.pipeline.seed_prompt_variants import (
        build_author_schema,
        validate_variant_output,
    )

    base = build_author_schema(3)
    assert "room_inventory" not in base["properties"]
    inv = build_author_schema(3, include_inventory=True)
    assert inv["properties"]["room_inventory"]["items"]["required"] == [
        "label_en", "scene_index", "evidence_ko"]
    assert set(inv["required"]) == {
        "location_line_en", "conformance_brief_en",
        "room_inventory_applicable", "room_inventory",
        "lettering_text", "variants"}

    author = _v9_author
    assert validate_variant_output(
        author(), 3, require_inventory=True, scene_texts=_V9_TEXTS) == []
    # 구팩 모드는 신규 필드 무검증 (byte-호환)
    assert validate_variant_output(
        {k: v for k, v in author().items()
         if k not in ("room_inventory", "room_inventory_applicable",
                      "lettering_text")}, 3) == []
    v = validate_variant_output(
        author(room_inventory="no"), 3, require_inventory=True,
        scene_texts=_V9_TEXTS)
    assert any("비배열" in x for x in v)
    v = validate_variant_output(
        author(room_inventory=[
            {"label_en": "R", "scene_index": 3, "evidence_ko": ""}]),
        3, require_inventory=True, scene_texts=_V9_TEXTS)
    assert any("evidence_ko" in x for x in v)
    v = validate_variant_output(
        author(room_inventory=[
            {"label_en": "Same", "scene_index": 3, "evidence_ko": "가"},
            {"label_en": "same", "scene_index": 5, "evidence_ko": "나"},
        ]), 3, require_inventory=True, scene_texts=_V9_TEXTS)
    assert any("중복" in x for x in v)
    v = validate_variant_output(
        author(lettering_text=None), 3, require_inventory=True,
        scene_texts=_V9_TEXTS)
    assert any("lettering_text" in x for x in v)
    # scene_index 비정수 (bool 포함)
    v = validate_variant_output(
        author(room_inventory=[
            {"label_en": "R", "scene_index": "3", "evidence_ko": "가"}]),
        3, require_inventory=True, scene_texts=_V9_TEXTS)
    assert any("scene_index 비정수" in x for x in v)


def test_v9_validator_applicability_lock():
    """v8b ③: 적용 대상인데 빈 배열 저작 차단 (판정 주체=LLM 저작)."""
    from app.modules.pipeline.seed_prompt_variants import (
        validate_variant_output,
    )

    v = validate_variant_output(
        _v9_author(room_inventory=[]), 3, require_inventory=True,
        scene_texts=_V9_TEXTS)
    assert any("빈 배열" in x for x in v)
    v = validate_variant_output(
        _v9_author(room_inventory_applicable=False), 3,
        require_inventory=True, scene_texts=_V9_TEXTS)
    assert any("모순" in x for x in v)
    assert validate_variant_output(
        _v9_author(room_inventory_applicable=False, room_inventory=[]),
        3, require_inventory=True, scene_texts=_V9_TEXTS) == []
    v = validate_variant_output(
        _v9_author(room_inventory_applicable="yes"), 3,
        require_inventory=True, scene_texts=_V9_TEXTS)
    assert any("room_inventory_applicable 비불리언" in x for x in v)


def test_v9_validator_quote_provenance_scene_bound():
    """v8b ②③: 인용 provenance 를 validator 가 소유 — scene_index 단위
    whitespace-normalized exact substring (cache 재검증 경로 커버)."""
    from app.modules.pipeline.seed_prompt_variants import (
        validate_variant_output,
    )

    texts = {3: "긴 원문\nA 가 있는 씬.", 5: "원문 B 씬."}
    # 개행을 가로지르는 인용 = normalized substring 통과
    ok = _v9_author(room_inventory=[
        {"label_en": "SAMPLE room A", "scene_index": 3,
         "evidence_ko": "원문 A 가"},
        {"label_en": "SAMPLE room B", "scene_index": 5,
         "evidence_ko": "원문 B"},
    ])
    assert validate_variant_output(
        ok, 3, require_inventory=True, scene_texts=texts) == []
    # 공급 밖 scene_index
    v = validate_variant_output(
        _v9_author(room_inventory=[
            {"label_en": "R", "scene_index": 9, "evidence_ko": "원문 A"}]),
        3, require_inventory=True, scene_texts=texts)
    assert any("공급된 씬이 아님" in x for x in v)
    # 해당 씬 원문에 없는 인용 (다른 씬에 있어도 위반 — 씬 단위 대조)
    v = validate_variant_output(
        _v9_author(room_inventory=[
            {"label_en": "R", "scene_index": 3, "evidence_ko": "원문 B"}]),
        3, require_inventory=True, scene_texts=texts)
    assert any("원문에 없음" in x for x in v)
    # scene_texts 미공급 = fail-closed 위반 (Codex v9 NARROW-1:
    # 호출자 인자 누락으로 evidence-bound 보장이 묵시 해제되는 경로 차단)
    v = validate_variant_output(ok, 3, require_inventory=True)
    assert any("scene_texts 미공급" in x for x in v)
    # 구팩 모드(require_inventory=False)는 여전히 scene_texts 불요
    assert validate_variant_output(ok, 3) == []


def test_v14_validator_lettering_text_format_lock():
    """활자 문안은 한 줄 — 개행·제어문자·쌍따옴표 금지.

    v14 부터 "간판이 필요한 업종인가"를 판단시키던 칸이 사라졌다. 활자 유무는
    빈 문자열로 표현하므로 정합 검사가 아니라 형식 잠금만 남는다.
    """
    from app.modules.pipeline.seed_prompt_variants import (
        validate_variant_output,
    )

    assert validate_variant_output(
        _v9_author(lettering_text=""), 3, require_inventory=True,
        scene_texts=_V9_TEXTS) == []
    assert validate_variant_output(
        _v9_author(lettering_text="SAMPLE 상호"), 3, require_inventory=True,
        scene_texts=_V9_TEXTS) == []
    v = validate_variant_output(
        _v9_author(lettering_text="줄1\n줄2"), 3, require_inventory=True,
        scene_texts=_V9_TEXTS)
    assert any("제어문자" in x for x in v)
    v = validate_variant_output(
        _v9_author(lettering_text='상호 "별관"'), 3, require_inventory=True,
        scene_texts=_V9_TEXTS)
    assert any("쌍따옴표" in x for x in v)


def test_v9_assembly_appends_capacity_and_lettering_clauses():
    from app.modules.pipeline.seed_prompt_variants import (
        assemble_variant_roll_prompts,
        build_conformance_prompt,
    )

    data = _v9_author(
        room_inventory=[
            {"label_en": "SAMPLE room A", "scene_index": 3,
             "evidence_ko": "원문 A"},
            {"label_en": "SAMPLE room B", "scene_index": 5,
             "evidence_ko": "원문 B"},
            {"label_en": "SAMPLE room C", "scene_index": 7,
             "evidence_ko": "원문 C"},
        ],
        lettering_text="SAMPLE 상호",
    )
    rolls = assemble_variant_roll_prompts(
        data=data, labels=["A", "B", "C"], world_anchor="",
        excluded_transient_en="", seed_pack_version="5",
        variants_pack_version="14", interior_note_en="노트")
    for p in rolls.values():
        assert "ROOM CAPACITY" in p and "3" in p
        assert '"SAMPLE 상호"' in p
        # 방 이름(개별 정체성)은 하류 미노출
        assert "SAMPLE room A" not in p
    brief = build_conformance_prompt(
        data=data, excluded_transient_en="",
        variants_pack_version="14", interior_note_en="노트")
    assert "ROOM CAPACITY" in brief and '"SAMPLE 상호"' in brief
    # 빈 인벤토리·빈 활자 문안 = 절 미부착
    data2 = {**data, "room_inventory": [], "lettering_text": ""}
    brief2 = build_conformance_prompt(
        data=data2, excluded_transient_en="",
        variants_pack_version="14", interior_note_en="노트")
    assert "ROOM CAPACITY" not in brief2
    assert "IN-WORLD SIGNAGE" not in brief2
    # v7/v8(복원) = 신규 절 미부착 (구팩 불변 — v8 은 스템 파일 자체가
    # 없어 inventory 조립 시 fail-closed 였을 경로)
    for old in ("7", "8"):
        brief_old = build_conformance_prompt(
            data=data, excluded_transient_en="",
            variants_pack_version=old, interior_note_en="노트")
        assert "ROOM CAPACITY" not in brief_old


def test_v9_author_user_content_interior_scenes_block():
    """v8b ③ 배선: 실내 씬 원문 별도 블록 병기(방 전수·규모 증거 전용),
    중복 씬 제외, 실내 씬 원문 결손=fail-closed."""
    texts = {3: "야외 씬 원문.", 5: "실내 안방 원문.", 7: "실내 자기방 원문."}
    heads = {3: "EXT. SAMPLE", 5: "INT. SAMPLE 안방", 7: "INT. SAMPLE 방"}
    out = build_author_user_content(
        scene_indices=[3], scene_texts=texts, scene_headings=heads,
        structure_desc="SAMPLE structure", interior_note_en="",
        exterior_note_en="", world_anchor="", excluded_transient_en="",
        count=3, interior_scene_indices=[5, 7, 3])
    assert "INTERIOR SCENES" in out
    assert "실내 안방 원문." in out and "실내 자기방 원문." in out
    # 중복(3)은 SCENARIO SCENES 블록에만 — 1회 출현
    assert out.count("야외 씬 원문.") == 1
    # 실내 블록은 본문 뒤(방 전수 증거 위치)
    assert out.index("SCENARIO SCENES") < out.index("INTERIOR SCENES")
    # 빈 시퀀스=기존 조립 byte-identical
    base = build_author_user_content(
        scene_indices=[3], scene_texts=texts, scene_headings=heads,
        structure_desc="SAMPLE structure", interior_note_en="",
        exterior_note_en="", world_anchor="", excluded_transient_en="",
        count=3)
    assert "INTERIOR SCENES" not in base
    with pytest.raises(ValueError, match="원문 결손"):
        build_author_user_content(
            scene_indices=[3], scene_texts=texts, scene_headings=heads,
            structure_desc="SAMPLE structure", interior_note_en="",
            exterior_note_en="", world_anchor="", excluded_transient_en="",
            count=3, interior_scene_indices=[9])
