"""Area #7b residue gate — Visual World Rules downstream prompt cascade.

closed-world exact residue phrase contract (사용자 격분 흡수 2026-05-19, [[feedback_llm_based_judgment]] 정합):
- pure str membership (str.__contains__ / `in` operator) only — regex 0 for phrase gates.
- inventory split: audit-verbatim (audit §11.2 line 240-254 본문 정확) + current-active-promoted (controller verify 2026-05-19, audit §11.2 미포함).
- source path + line + audit-or-current-active classification anchored.

4-Gate Boundary Doctrine (semantic 판단 절대 금지):
- Gate (1) Semantic Regex Ban — open-world semantic 판단 regex 0.
- Gate (2) Prompt Closed-List Ban — prompt 안 LLM-instructed closed-list domain term enumeration 0.
- Gate (3) Structured SOT Required — open-world semantic 판단 = LLM-produced structured emit.
- Gate (4) No Silent Fallback — invalid input = explicit reject + typed error.

Gate Coverage:
- Gate 1.1-1.9 exact residue phrase membership per modified file (9 sub-gate, 15 phrase total).
- Gate 2 canonical VWR clause glyph-exact membership per 9 modified file (hit ≥ 1).
- Gate 3 D-2 entity_character_list byte-identical (sha256 diff 0).
- Gate 4 archive directory exclusion (path glob, structural).

NO VLM hard constraint inline.
"""

from __future__ import annotations

import hashlib
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[3]  # backend/tests/integration/<this> → repo root
PROMPTS_BASE = REPO_ROOT / "prompts" / "_base"

# --- Canonical clause (audit §11.3 verbatim, 9 rewritten site 모두 글자-단위 동일 의무) ---

CANONICAL_VWR_CLAUSE = (
    "본 prompt 의 변신/빙의/원격접속 판단 기준은 visual_world_rules.rules[] 의 "
    "rule_type ∈ {possession, transformation, ghost, projection, superpower, "
    "body_deformation} 을 따른다. 작품에 해당 rule_type rule 이 정의되어 있지 "
    "않으면 일반 인물로 처리."
)

# --- D-2 carve-out path + hardcoded sha256 (변경 0 boundary preserve, Codex iter 1 F-2 흡수) ---

D2_ENTITY_CHARACTER_LIST = PROMPTS_BASE / "entity_character_list" / "2.202605011057" / "system.md"
# D-2 sha256 anchor = Area #7a closure push (d0410eb) 시점 controller verify 2026-05-19
D2_EXPECTED_SHA256 = "4ca95ca996a5e1d649e92c9baf8963654a5f89537a794ab6ec04e86ce9971713"


def _sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _latest_active_dir(module: str, version_prefix: str) -> Path:
    """Return the lexicographically latest dir matching version prefix (e.g. '5.', '12.')."""
    base = PROMPTS_BASE / module
    candidates = sorted(p for p in base.iterdir() if p.is_dir() and p.name.startswith(version_prefix))
    assert candidates, f"no {version_prefix}* dir under {base}"
    return candidates[-1]


@pytest.fixture(scope="module")
def entity_all_v5_dir() -> Path:
    return _latest_active_dir("entity_all", "5.")


@pytest.fixture(scope="module")
def entity_relation_v3_dir() -> Path:
    return _latest_active_dir("entity_relation", "3.")


@pytest.fixture(scope="module")
def entity_extractor_v2_v10_dir() -> Path:
    return _latest_active_dir("entity_extractor_v2", "10.")


@pytest.fixture(scope="module")
def outlook_extractor_v12_dir() -> Path:
    return _latest_active_dir("outlook_extractor", "12.")


@pytest.fixture(scope="module")
def scene_verify_v2_dir() -> Path:
    return _latest_active_dir("scene_verify", "2.")


@pytest.fixture(scope="module")
def scene_detail_v28_dir() -> Path:
    return _latest_active_dir("scene_detail", "28.")


@pytest.fixture(scope="module")
def shot_extract_v12_dir() -> Path:
    return _latest_active_dir("shot_extract", "12.")


# --- Gate 1.1: entity_all 5.*/character.md (audit §11.2 site 1 line 244 verbatim) ---


def test_gate_1_1_entity_all_character_md_residue(entity_all_v5_dir: Path) -> None:
    """Site 1 audit-verbatim 8 phrase hit 0 (fixed-string membership only).

    빙의/합체 entry = audit line 29 full bullet form (Generic Korean standalone ban 0,
    Plan W1 inventory line 162 + amend boundary line 439 + spec §3.1 line 239 정합).
    Generic heading `빙의/합체` 단독 substring ban 0 — full legacy phrase 만 forbidden.
    """
    body = (entity_all_v5_dir / "character.md").read_text(encoding="utf-8")
    audit_verbatim_phrases = [
        "인간↔요괴",
        "인간↔괴물",
        "본체↔변신체",
        "빙의/합체: 다른 존재가 몸을 빌리거나 합체하여 외형이 변하는 경우",
        "이무기",
        "구미호",
        "요괴화",
        "뱀파이어",
    ]
    for phrase in audit_verbatim_phrases:
        assert phrase not in body, f"entity_all character.md residue: {phrase!r}"


# --- Gate 1.2: entity_relation 3.*/system.md (audit §11.2 site 3 line 246 + current-active-promoted line 30) ---


def test_gate_1_2_entity_relation_system_md_residue(entity_relation_v3_dir: Path) -> None:
    """Site 3 audit-verbatim 3 phrase + current-active-promoted 1 phrase hit 0."""
    body = (entity_relation_v3_dir / "system.md").read_text(encoding="utf-8")
    audit_verbatim_phrases = [
        "요괴/괴물 형태",
        "유령/망령 형태",
        "갑옷 착용 형태",
    ]
    current_active_promoted = "요괴로 변해도 원래 인물의 흔적이 있음 → true"
    for phrase in audit_verbatim_phrases:
        assert phrase not in body, f"entity_relation system.md audit-verbatim residue: {phrase!r}"
    assert current_active_promoted not in body, (
        f"entity_relation system.md current-active-promoted residue: {current_active_promoted!r}"
    )


# --- Gate 1.3: entity_extractor_v2 10.*/system.md (audit §11.2 site 6 line 249 verbatim) ---


def test_gate_1_3_entity_extractor_v2_system_md_residue(entity_extractor_v2_v10_dir: Path) -> None:
    """Site 6 audit-verbatim 2 phrase hit 0."""
    body = (entity_extractor_v2_v10_dir / "system.md").read_text(encoding="utf-8")
    audit_verbatim_phrases = [
        "평상복 → 전신 갑옷",
        "### 빙의/정신 전환/몸을 빌리는 설정이 있는 경우",
    ]
    for phrase in audit_verbatim_phrases:
        assert phrase not in body, f"entity_extractor_v2 system.md residue: {phrase!r}"


# --- Gate 1.4: entity_extractor_v2 10.*/turn0_style.md (audit §11.2 site 7 line 250 verbatim) ---


def test_gate_1_4_entity_extractor_v2_turn0_style_md_residue(
    entity_extractor_v2_v10_dir: Path,
) -> None:
    """Site 7 audit-verbatim 1 phrase hit 0."""
    body = (entity_extractor_v2_v10_dir / "turn0_style.md").read_text(encoding="utf-8")
    assert "원격 접속/조종/빙의/텔레파시" not in body, (
        "entity_extractor_v2 turn0_style.md residue: '원격 접속/조종/빙의/텔레파시'"
    )


# --- Gate 1.5: outlook_extractor 12.*/phase1.md (audit §11.2 site 4 line 247 + current-active-promoted line 24) ---


def test_gate_1_5_outlook_phase1_residue(outlook_extractor_v12_dir: Path) -> None:
    """Site 4 audit-verbatim 1 phrase + current-active-promoted 1 phrase hit 0."""
    body = (outlook_extractor_v12_dir / "phase1.md").read_text(encoding="utf-8")
    audit_verbatim = "메카, 파워드슈트, 강화복, 갑옷 로봇"
    current_active_promoted = "인간형 요괴/괴물, 뱀파이어, 좀비"
    assert audit_verbatim not in body, f"outlook phase1.md audit-verbatim residue: {audit_verbatim!r}"
    assert current_active_promoted not in body, (
        f"outlook phase1.md current-active-promoted residue: {current_active_promoted!r}"
    )


# --- Gate 1.6: outlook_extractor 12.*/phase2.md (audit §11.2 site 5 line 248 verbatim) ---


def test_gate_1_6_outlook_phase2_residue(outlook_extractor_v12_dir: Path) -> None:
    """Site 5 audit-verbatim 1 phrase hit 0."""
    body = (outlook_extractor_v12_dir / "phase2.md").read_text(encoding="utf-8")
    assert "원격접속/빙의:" not in body, "outlook phase2.md residue: '원격접속/빙의:'"


# --- Gate 1.7: scene_verify 2.*/system.md (audit §11.2 site 9 line 252 verbatim) ---


def test_gate_1_7_scene_verify_system_md_residue(scene_verify_v2_dir: Path) -> None:
    """Site 9 audit-verbatim 1 full sentence hit 0."""
    body = (scene_verify_v2_dir / "system.md").read_text(encoding="utf-8")
    audit_verbatim = (
        "3. **빙의/영혼 상태**: 다른 인물의 몸에 들어간 경우, 원래 몸은 "
        "visible=false, 빙의된 몸은 visible=true. reason에 상태를 명시"
    )
    assert audit_verbatim not in body, f"scene_verify system.md residue: {audit_verbatim!r}"


# --- Gate 1.8: scene_detail 28.*/system.md (Carry-D7d-1 v27 → v28; v28 system.md byte-identical from 27.202605181937 — historical Area #7b W3 v27 residue removal preserved; audit §11.2 site 10 line 253 verbatim, line drift verify) ---


def test_gate_1_8_scene_detail_system_md_residue(scene_detail_v28_dir: Path) -> None:
    """Site 10 audit-verbatim 1 full sentence hit 0 (audit 시점 line 327 → current line 473 drift verify)."""
    body = (scene_detail_v28_dir / "system.md").read_text(encoding="utf-8")
    audit_verbatim = "- 원격 조종/접속 인물 제외 (조종자 몸이 다른 장소에 있음)"
    assert audit_verbatim not in body, f"scene_detail system.md residue: {audit_verbatim!r}"


# --- Gate 1.9: shot_extract 12.*/user.md (audit §11.2 site 11 line 254 verbatim, line drift verify) ---


def test_gate_1_9_shot_extract_user_md_residue(shot_extract_v12_dir: Path) -> None:
    """Site 11 audit-verbatim 1 parenthetical hit 0 (audit 시점 line 33 → current line 49 drift verify)."""
    body = (shot_extract_v12_dir / "user.md").read_text(encoding="utf-8")
    assert "빙의로 얼굴이 바뀜" not in body, "shot_extract user.md residue: '빙의로 얼굴이 바뀜'"


# --- Gate 2: canonical VWR clause glyph-exact membership per 9 modified file (hit ≥ 1) ---


@pytest.mark.parametrize(
    "module_fixture,filename",
    [
        ("entity_all_v5_dir", "character.md"),
        ("entity_relation_v3_dir", "system.md"),
        ("entity_extractor_v2_v10_dir", "system.md"),
        ("entity_extractor_v2_v10_dir", "turn0_style.md"),
        ("outlook_extractor_v12_dir", "phase1.md"),
        ("outlook_extractor_v12_dir", "phase2.md"),
        ("scene_verify_v2_dir", "system.md"),
        ("scene_detail_v28_dir", "system.md"),
        ("shot_extract_v12_dir", "user.md"),
    ],
)
def test_gate_2_canonical_clause_membership(
    module_fixture: str, filename: str, request: pytest.FixtureRequest
) -> None:
    """9 rewritten file 모두 canonical VWR clause glyph-exact membership ≥ 1 (fixed-string)."""
    module_dir = request.getfixturevalue(module_fixture)
    body = (module_dir / filename).read_text(encoding="utf-8")
    assert CANONICAL_VWR_CLAUSE in body, (
        f"{module_dir.name}/{filename}: missing canonical VWR clause (글자-단위 정확 동일 의무)"
    )


# --- Gate 3: D-2 entity_character_list byte-identical (sha256 diff 0, 변경 0 boundary preserve) ---


def test_gate_3_d2_entity_character_list_byte_identical() -> None:
    """D-2 carve-out — entity_character_list/2.202605011057/system.md 본 area 안 변경 0 verify.

    Codex iter 1 F-2 fix-up: hardcoded sha256 anchor (Area #7a closure push d0410eb 시점,
    controller verify 2026-05-19). 변경 시 D-2 boundary 위반.
    """
    assert D2_ENTITY_CHARACTER_LIST.exists(), f"D-2 path missing: {D2_ENTITY_CHARACTER_LIST}"
    # Primary: sha256 hardcoded anchor (byte-identical contract)
    actual_sha = _sha256(D2_ENTITY_CHARACTER_LIST)
    assert actual_sha == D2_EXPECTED_SHA256, (
        f"D-2 sha256 mismatch (boundary preserve 위반): "
        f"expected {D2_EXPECTED_SHA256}, actual {actual_sha}"
    )
    # Sanity: owner taxonomy 5-element 분류 본체 보존 (sha256 PASS 시 자동 보장, but explicit)
    body_text = D2_ENTITY_CHARACTER_LIST.read_text(encoding="utf-8")
    assert "인간, 요괴, 동물, 로봇, 외계인" in body_text, (
        "D-2 owner taxonomy 5-element 분류 본체 변경 — boundary preserve 위반"
    )
    # entity_character_list 디렉토리 안 새 dir 신설 0 verify
    base = PROMPTS_BASE / "entity_character_list"
    dirs = sorted(p.name for p in base.iterdir() if p.is_dir())
    assert dirs == ["1.202604010100", "2.202605011057"], (
        f"D-2 entity_character_list new dir 신설 발견 — boundary 위반: {dirs}"
    )


# --- Gate 4: archive directory exclusion (path glob, structural) ---


def test_gate_4_archive_directory_exclusion() -> None:
    """archive dir (current latest 외) 본문 안 residue phrase 잔존 = 허용 (Gate 1.x scope 외).

    structural verify: 7 module 각각 latest dir (5.* / 3.* / 10.* / 12.* / 2.* / 28.* / 12.*) 가
    Gate 1.x scope 안. archive dir (예: entity_all 1.* / 2.* / 3.* / 4.*) 은 path glob 으로
    scope 외 verify.
    """
    expected = {
        "entity_all": "5.",
        "entity_relation": "3.",
        "entity_extractor_v2": "10.",
        "outlook_extractor": "12.",
        "scene_verify": "2.",
        "scene_detail": "28.",
        "shot_extract": "12.",
    }
    for module, prefix in expected.items():
        latest = _latest_active_dir(module, prefix)
        # latest dir name starts with expected prefix (closed-world path comparison)
        assert latest.name.startswith(prefix), (
            f"{module}: latest dir {latest.name} does not start with {prefix}"
        )


# --- Sanity: 모든 Gate 1.x fixture 가 존재하는 path 인지 verify ---


def test_sanity_all_modified_file_exist(
    entity_all_v5_dir: Path,
    entity_relation_v3_dir: Path,
    entity_extractor_v2_v10_dir: Path,
    outlook_extractor_v12_dir: Path,
    scene_verify_v2_dir: Path,
    scene_detail_v28_dir: Path,
    shot_extract_v12_dir: Path,
) -> None:
    """9 modified file 모두 존재 verify."""
    expected_files = [
        (entity_all_v5_dir, "character.md"),
        (entity_relation_v3_dir, "system.md"),
        (entity_extractor_v2_v10_dir, "system.md"),
        (entity_extractor_v2_v10_dir, "turn0_style.md"),
        (outlook_extractor_v12_dir, "phase1.md"),
        (outlook_extractor_v12_dir, "phase2.md"),
        (scene_verify_v2_dir, "system.md"),
        (scene_detail_v28_dir, "system.md"),
        (shot_extract_v12_dir, "user.md"),
    ]
    for module_dir, filename in expected_files:
        target = module_dir / filename
        assert target.exists(), f"missing modified file: {target}"
