"""C9 regression — scene_detail v31 Rule B body-light verb closed list 추상화 가드.

audit 03 L-7 (fix-critical-1 Tier β #6) — `prompts/_base/scene_detail/<latest>/
system.md` 의 `## 조명·색조가 신체를 변형하지 않게 (Rule B)` 섹션에서 빛/색이
신체에 작용하는 closed verb list (`cut/slice/split/carve/bisect`) + forbidden/
recommended 예문 목록 + recommended verb list 4개 닫힌 enumeration 을 단일
generic semantic principle 로 추상화 (Track B Prompt Closed-List Ban,
feedback_llm_based_judgment).

Gate G1-G7 (spec §7):
  G1 — closed verb list 닫힌-form literal 부재 (Rule B 섹션 path-scoped)
  G2 — forbidden/recommended 예문 + recommended verb list marker 부재
  G3 — generic semantic principle marker 존재
  G4 — Rule B header + WHY 문단 보존
  G5 — v31 detail_schema.json == v30 byte-identical (sha256)
  G6 — 4-point version sync (v31 lockstep)
  G7 — section boundary 보존 (실루엣 cutout / windshield slice / 다음 섹션)

설계: 파일 read + 문자열 검사 + sha256 only — no live LLM, NO VLM
(feedback_no_vlm_dependency). residue needle 은 split-string 상수로 구성해
test 파일 자체가 다른 audit residue gate 에 self-hit 하지 않게 한다 (방어적).
residue gate 는 Rule B 섹션 path-scoped slice 에만 적용 — bare `cut`/`slice`
토큰 global grep 은 하지 않는다 (섹션 밖 평면 cutout / windshield slice 무관
줄 false-hit 방지, spec §3.3 / §8 R1).

spec: docs/superpowers/specs/2026-05-20-c9-scene-detail-body-light-verb-v1-design.md
plan: docs/superpowers/plans/2026-05-20-c9-scene-detail-body-light-verb-v1-implementation.md
"""
from __future__ import annotations

import hashlib
from pathlib import Path

from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION
from app.core.version_registry import MODULE_VERSIONS, _MODULE_INFO

_REPO_ROOT = Path(__file__).resolve().parents[3]
_SCENE_DETAIL_BASE = _REPO_ROOT / "prompts" / "_base" / "scene_detail"
_V30_DIR = "30.202605200303"
# C9 W1 직전 active prompt-pack — detail_schema.json byte-identical 기준 (frozen).
# C9 W1 산출물 prompt-pack — FINDING 5 v33 bump 후 _latest 가 drift + v33 가
# detail_schema.json description 을 수정 → C9 regression 의 byte-identical gate 는
# C9 의 실제 산출물 v31 에 고정.
_V31_DIR = "31.202605200805"
_RULE_B_HEADER = "## 조명·색조가 신체를 변형하지 않게 (Rule B)"


def _v31_system_md() -> str:
    path = _SCENE_DETAIL_BASE / SCENE_DETAIL_PROMPT_VERSION / "system.md"
    return path.read_text(encoding="utf-8")


def _rule_b_section(system_md_text: str) -> str:
    """Rule B 섹션만 slice — header 부터 다음 `## ` (line-start) 직전까지.

    path-scoped: residue gate 는 이 slice 에만 적용 → 실루엣 평면 cutout /
    windshield slice 등 섹션 밖 무관 줄을 스캔하지 않는다 (spec §3.3).
    """
    assert _RULE_B_HEADER in system_md_text, "Rule B header 부재 — v31 system.md 손상"
    start = system_md_text.index(_RULE_B_HEADER)
    nxt = system_md_text.index("\n## ", start + len(_RULE_B_HEADER))
    return system_md_text[start:nxt]


def test_g1_no_closed_verb_list_literal():
    """G1 — Rule B 섹션에 body-light closed verb list 닫힌-form literal 0.

    quoted-comma form + slash form 둘 다 검사. needle 은 split-string 상수 —
    test 파일에 닫힌-form literal 이 contiguous 로 존재하지 않게 한다.
    """
    rule_b = _rule_b_section(_v31_system_md())
    quoted_comma_form = '"cut"' + ', "slice"'
    slash_form = "cut / slice" + " / split / carve / bisect"
    assert quoted_comma_form not in rule_b, (
        "Rule B 섹션에 closed verb list (quoted-comma form) 잔존 — C9 추상화 미적용"
    )
    assert slash_form not in rule_b, (
        "Rule B 섹션에 closed verb list (slash form) 잔존 — C9 추상화 미적용"
    )


def test_g2_no_example_phrase_markers():
    """G2 — forbidden/recommended 예문 + recommended verb list marker 0.

    v30 의 영문 예문/positive verb list 가 v31 Rule B 섹션에 잔존하지 않는지
    안정 substring marker 로 검사. needle 은 split-string 상수.
    """
    rule_b = _rule_b_section(_v31_system_md())
    markers = [
        "cuts across" + " her eyes",
        "slicing " + "through his face",
        "splits her" + " cheek",
        "carving " + "across the mouth",
        "reflects on" + " her cheek",
        "pools" + " on",
        "glows " + "across",
    ]
    for marker in markers:
        assert marker not in rule_b, (
            f"Rule B 섹션에 예문/verb-list marker {marker!r} 잔존 — "
            f"C9 예시 전폐 미적용"
        )


def test_g3_generic_principle_present():
    """G3 — Rule B 섹션에 generic semantic principle marker 존재."""
    rule_b = _rule_b_section(_v31_system_md())
    assert "눈에 보이는 표면·재질·영역" in rule_b, (
        "Rule B 섹션에 generic principle marker (표면 추상 서술) 부재"
    )
    assert "신체 변형으로 해석된다" in rule_b, (
        "Rule B 섹션에 generic principle marker (신체 변형 해석) 부재"
    )


def test_g4_header_and_why_preserved():
    """G4 — Rule B header + WHY 문단 보존."""
    system_md = _v31_system_md()
    assert _RULE_B_HEADER in system_md, "Rule B header 손실"
    rule_b = _rule_b_section(system_md)
    assert "물리적 절단" in rule_b, "WHY 문단 (T2I 물리적 절단 오해석 rationale) 손실"


def test_g5_schema_byte_identical_to_v30():
    """G5 — v31 detail_schema.json == v30 byte-identical (sha256).

    C9 = prompt text only — schema 내용 변경 0.

    FINDING 5 / e2e-bughunt-v1 W3 (2026-05-21): scene_detail v33 bump 이
    detail_schema.json description 을 수정 → _latest 가 더이상 v30 과
    byte-identical 아님. C9 의 "prompt-only" 주장은 C9 의 실제 산출물 v31 에
    대한 것이므로 본 gate 를 v31 frozen dir 에 고정한다.
    """
    v30_schema = _SCENE_DETAIL_BASE / _V30_DIR / "detail_schema.json"
    v31_schema = _SCENE_DETAIL_BASE / _V31_DIR / "detail_schema.json"
    assert v30_schema.is_file(), f"v30 detail_schema.json 부재: {v30_schema}"
    assert v31_schema.is_file(), f"v31 detail_schema.json 부재: {v31_schema}"
    v30_hash = hashlib.sha256(v30_schema.read_bytes()).hexdigest()
    v31_hash = hashlib.sha256(v31_schema.read_bytes()).hexdigest()
    assert v30_hash == v31_hash, (
        f"v31 detail_schema.json sha256 {v31_hash} != v30 {v30_hash} — "
        f"C9 는 prompt-only, schema 변경 0 이어야 한다"
    )


def test_g6_four_point_version_sync():
    """G6 — 4-point version sync: 네 곳이 **서로** 같은 판을 가리키는가.

    ★판 숫자를 박지 않는다. 예전에는 "37." 처럼 그 시점 값을 박아 두어 판을
    올릴 때마다 이 테스트가 깨졌고, 그래서 v38·v39 에서는 registry 쪽 갱신이
    통째로 누락된 채 남았다(MODULE_VERSIONS 1.37.0 / prompt_dependency v37).
    지켜야 할 계약은 "지금이 몇 판인가"가 아니라 "네 곳이 일치하는가"이다.
    """
    from app.modules.prompt_loader import _list_module_versions

    latest = _list_module_versions("scene_detail")[0]
    major = latest.split(".")[0]
    assert SCENE_DETAIL_PROMPT_VERSION == latest, (
        f"SCENE_DETAIL_PROMPT_VERSION={SCENE_DETAIL_PROMPT_VERSION!r} != "
        f"최신 판 {latest!r}"
    )
    assert f".{major}." in MODULE_VERSIONS["scene_detail_composer"], (
        f"MODULE_VERSIONS['scene_detail_composer']="
        f"{MODULE_VERSIONS['scene_detail_composer']!r} 의 minor 가 "
        f"prompt v{major} 와 정렬 안 됨"
    )
    pdep = _MODULE_INFO["scene_detail_composer"]["prompt_dependency"]
    assert pdep == f"scene_detail/v{major}", (
        f"_MODULE_INFO['scene_detail_composer']['prompt_dependency']="
        f"{pdep!r} != 'scene_detail/v{major}'"
    )
    latest_dir = _SCENE_DETAIL_BASE / SCENE_DETAIL_PROMPT_VERSION
    assert latest_dir.is_dir(), f"scene_detail prompt-pack 디렉토리 부재: {latest_dir}"


def test_g7_section_boundary_preserved():
    """G7 — Rule B 인접/무관 섹션 boundary 보존.

    실루엣 평면 cutout / windshield slice 는 'cut'/'slice' 부분 문자열을 갖지만
    Rule B 와 무관 — C9 추상화가 이 줄을 오삭제하지 않았는지 확인.
    """
    system_md = _v31_system_md()
    assert "## 실루엣 묘사 규칙 — 평면 cutout 금지" in system_md, (
        "boundary 손실 — 실루엣 평면 cutout 섹션 (무관 context)"
    )
    assert "a slice of the windshield visible" in system_md, (
        "boundary 손실 — windshield slice framing 예시 (무관 context)"
    )
    assert "## 크기·치수" in system_md, (
        "boundary 손실 — Rule B 직후 ## 크기·치수 섹션"
    )
