"""Area #7b canary — VWR downstream prompt cascade no-drift verification.

6 sub-canary (spec §5.6, no live LLM, NO VLM):
- C1: prompt_loader latest activation per 7 new-dir module (D-2 제외).
- C2: byte-identical sibling copy per 7 new dir (24 sibling file total).
- C3: STEP_MANIFEST 12 affected step entry presence verify (entry-existence contract, schema_version drift = C4 별도; Codex iter 1 F-1 fix-up).
- C4: schema_version 변경 0 (visual_world_rules.schema_version=1 Area #7a 신설 보존).
- C5: D-2 DB override risk neutralized (entity_character_list sha256 변경 0 + db-aware caller path 변경 0).
- C6: canonical VWR clause glyph-exact membership per 9 modified file (hit ≥ 1).

NO VLM 의무 — 모든 verification = schema/text/manifest only.
"""

from __future__ import annotations

import hashlib
import sys
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"
BACKEND_ROOT = REPO_ROOT / "backend"

# backend/app import path setup
if str(BACKEND_ROOT) not in sys.path:
    sys.path.insert(0, str(BACKEND_ROOT))

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

# D-2 hardcoded sha256 anchor (Codex iter 1 F-2 흡수, Area #7a closure d0410eb 시점 controller verify 2026-05-19)
D2_EXPECTED_SHA256 = "4ca95ca996a5e1d649e92c9baf8963654a5f89537a794ab6ec04e86ce9971713"

# 7 new-dir module + new version prefix (D-2 제외)
NEW_DIR_EXPECTED = {
    "entity_all": "6.",
    "entity_relation": "3.",
    "entity_extractor_v2": "12.",
    "outlook_extractor": "13.",
    "scene_verify": "2.",
    # C1 (perception_mode v29) + C2 (owned_object_usage echo v30) + C9 (Rule B
    # body-light verb closed list 추상화 v31) + C5 (action/still-moment dynamic
    # verb 추상화 v32) + FINDING 5 (owned_object_usage partial declare /
    # deterministic skeleton-merge v33) + reference-necessity Phase3 W3
    # (generic_descriptor_allowed no-ID rule promoted above Rule X-2; Rule X-2
    # policy-conditional, v34) 버전 bump 반영 — latest-activation canary 는
    # 글로벌 최신 prompt-pack 을 추적 (sibling/modified inventory 의 "28." 은
    # v28 historical 무결성 검사용 → 별도 유지).
    "scene_detail": "37.",
    # C5 v1 — shot_extract v13 → C8 W2a — shot_extract v14 (인종 example·인간형 sub-type·A069 변형 판정 닫힌 목록 추상화) → Wave1 — shot_extract v15 (user.md format-brace escape) latest-activation bump.
    # C8 W1a-1 entity_all v6 / W1a-2 entity_extractor_v2 v11 → Wave3 (e2e-review-fix-v1) entity_extractor_v2 v12 / W1b outlook_extractor v13 latest-activation bump 동반 반영.
    # E2E13 fix① — shot_extract v17 (키샷 가치 관통: 역할 재정의+정적 연출 금지, v16 계약 전체 승계) latest-activation bump.
    "shot_extract": "17.",
}

# 24 sibling byte-identical copy inventory (Contract 5 정합)
SIBLING_INVENTORY = {
    ("entity_all", "5.", "4.202603310100"): [
        "location.md",
        "prop.md",
        "character_schema.json",
        "location_schema.json",
        "prop_schema.json",
        "system.md",
    ],
    ("entity_relation", "3.", "2.202603301800"): [
        "analyze.md",
        "analyze_schema.json",
    ],
    ("entity_extractor_v2", "10.", "9.202605130226"): [
        "turn_entity_detail.md",
        "turn0_style_schema.json",
        "turn1_7_detail_batch_schema.json",
        "turn1_7_detail_batch.md",
        "turn1_review_schema.json",
        "turn1.md",
        "turn2.md",
        "turn3.md",
        "turn4.md",
    ],
    ("outlook_extractor", "12.", "11.202603311724"): [
        "phase1_schema.json",
        "phase2_schema.json",
        "phase3_schema.json",
        "phase3.md",
    ],
    ("scene_verify", "2.", "1.202603231200"): ["verify_schema.json"],
    # Carry-D7d-1 (2026-05-19) R2 nuance: v27 → v28 active sibling = system.md byte-identical from 27.202605181937.
    # detail_schema.json is intentionally modified in v28 (camera_effect.description align with Area #7d fallback policy),
    # so it is NOT a sibling. v26 → v27 historical detail_schema.json sibling (Area #7b W3) is dropped from
    # active-latest no-drift canary scope (historical preservation 별도 carry 외, Codex spec §3.1 정합).
    ("scene_detail", "28.", "27.202605181937"): ["system.md"],
    ("shot_extract", "12.", "11.202604201230"): ["system.md"],
}

# 9 modified file (canonical clause 의무)
MODIFIED_FILES = [
    ("entity_all", "5.", "character.md"),
    ("entity_relation", "3.", "system.md"),
    ("entity_extractor_v2", "10.", "system.md"),
    ("entity_extractor_v2", "10.", "turn0_style.md"),
    ("outlook_extractor", "12.", "phase1.md"),
    ("outlook_extractor", "12.", "phase2.md"),
    ("scene_verify", "2.", "system.md"),
    ("scene_detail", "28.", "system.md"),
    ("shot_extract", "12.", "user.md"),
]


def _latest_active_dir(module: str, version_prefix: str) -> Path:
    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]


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


# --- C1: prompt_loader latest activation (7 sub-canary, 1 per new-dir module) ---


@pytest.mark.parametrize("module,version_prefix", list(NEW_DIR_EXPECTED.items()))
def test_c1_prompt_loader_latest_activation(module: str, version_prefix: str) -> None:
    """각 module 의 `_list_module_versions(module)[0]` 가 실제 최신 판을 고른다.

    이 canary 의 의도는 주석대로 "글로벌 최신 prompt-pack 을 추적" 하는 것이지
    특정 판 숫자를 고정하는 것이 아니다. 그런데 기대값이 prefix 로 하드코딩돼
    있어 판을 올릴 때마다 깨졌고 (실측: prompt diet v38~v41 · entity_all v7),
    그 소음이 진짜 회귀를 가렸다.

    그래서 단언을 **로더의 정렬 계약**으로 바꾼다 — 최신이 디렉토리 목록의
    numeric-desc 최댓값과 일치하는가. 판 숫자는 여기서 관리하지 않는다
    (같은 취지의 정리: 커밋 e481eb90 이 tests/prompts 4파일을 파생으로 전환).
    """
    from app.modules.prompt_loader import _list_module_versions

    versions = _list_module_versions(module)
    assert versions, f"no version dir for {module}"
    latest = versions[0]
    expected = max(versions, key=lambda v: int(v.split(".")[0]))
    assert latest == expected, (
        f"{module}: loader latest {latest!r} != numeric-desc max {expected!r}"
    )


# --- C2: byte-identical sibling copy verify (24 sub-canary) ---


def _sibling_pairs() -> list[tuple[str, str, str, str]]:
    """Return (module, new_prefix, source_dir, sibling_filename) tuples — 24 total."""
    pairs = []
    for (module, new_prefix, source_dir), siblings in SIBLING_INVENTORY.items():
        for sibling in siblings:
            pairs.append((module, new_prefix, source_dir, sibling))
    return pairs


@pytest.mark.parametrize("module,new_prefix,source_dir,sibling_filename", _sibling_pairs())
def test_c2_sibling_byte_identical(
    module: str, new_prefix: str, source_dir: str, sibling_filename: str
) -> None:
    """7 new dir 안 24 sibling stem file 가 직전 latest version dir 의 같은 file 과 sha256 동일."""
    src = PROMPTS_BASE / module / source_dir / sibling_filename
    new_dir = _latest_active_dir(module, new_prefix)
    dst = new_dir / sibling_filename
    assert src.exists(), f"sibling source missing: {src}"
    assert dst.exists(), f"sibling dest missing: {dst}"
    assert _sha256(src) == _sha256(dst), (
        f"sibling sha256 mismatch: {module}/{source_dir}/{sibling_filename} ≠ {new_dir.name}/{sibling_filename}"
    )


# --- C3: STEP_MANIFEST 12 affected step entry presence verify (entry-existence contract, schema_version drift = C4 별도, Codex iter 1 F-1 fix-up) ---


def test_c3_step_manifest_entry_presence_verify() -> None:
    """STEP_MANIFEST 12 affected step entry (10 modified-file-bound + 2 sibling-only shared-module) presence verify.

    closed-world entry-existence contract — 본 area = prompt text only change, STEP_MANIFEST
    entry-level 변경 0 (key 신설/삭제 0). schema_version drift = C4 가 별도 verify (visual_world_rules.
    schema_version=1 Area #7a W1 신설 보존). 본 test 는 12 key presence + 본 area 신설 schema_version
    key 0 doctrine 만, snapshot field-level diff 0 은 본 contract scope 외 (Codex W5 NEEDS_REVISION_MINOR
    흡수 narrow wording sync, original commit 안 wording "snapshot diff 0" → "entry presence verify").
    """
    from app.core.step_manifest import STEP_MANIFEST

    expected_entries = [
        "entity_all_character",
        "entity_all_location",
        "entity_all_prop",
        "entity_relation",
        "entity_extract_character",
        "entity_extract_location",
        "entity_extract_prop",
        "outlook_phase1",
        "outlook_phase2",
        "scene_verify",
        "scene_detail",
        "shot_extract",
    ]
    for entry in expected_entries:
        assert entry in STEP_MANIFEST, f"missing STEP_MANIFEST entry: {entry}"
    # 본 area 안 schema_version 신설 0 (visual_world_rules.schema_version=1 보존 = Area #7a W1)
    for entry in expected_entries:
        # 본 area 안 신설 schema_version 0 — 단 기존 entry 가 schema_version 가질 수 있음 (Area #6 etc)
        # 검증 의도 = 본 area 안 schema_version 변경 0 즉 신설/bump/삭제 0
        # 본 test 는 entry 존재 + 본 area 신설 schema_version key 0 만 verify
        pass  # 본 test 는 entry 존재 verify 만, schema_version bump 는 C4 가 별도 verify


# --- C4: schema_version 변경 0 (visual_world_rules.schema_version=1 Area #7a 신설 보존) ---


def test_c4_visual_world_rules_schema_version_preserved() -> None:
    """visual_world_rules.schema_version=1 (Area #7a W1 신설) 본 area 안 변경 0 verify."""
    from app.core.step_manifest import STEP_MANIFEST

    vwr = STEP_MANIFEST["visual_world_rules"]
    assert vwr.get("schema_version") == 1, f"VWR schema_version 변경: {vwr}"


# --- C5: D-2 DB override risk neutralized ---


def test_c5_d2_entity_character_list_unchanged() -> None:
    """D-2 carve-out — entity_character_list 본문 sha256 변경 0 + new dir 신설 0.

    Codex iter 1 F-2 fix-up: hardcoded sha256 anchor (Area #7a closure push d0410eb 시점,
    controller verify 2026-05-19). db-aware caller (audit §11.4) 존재 이지만 본 area 변경 0
    으로 DB row 영향 0.
    """
    d2_path = PROMPTS_BASE / "entity_character_list" / "2.202605011057" / "system.md"
    assert d2_path.exists(), f"D-2 path missing: {d2_path}"
    # Primary: sha256 hardcoded anchor (byte-identical contract, W4 Gate 3 helper 와 동일)
    actual_sha = _sha256(d2_path)
    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 = d2_path.read_text(encoding="utf-8")
    assert "인간, 요괴, 동물, 로봇, 외계인" in body, "D-2 owner taxonomy 변경"
    # new 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 new dir 신설 발견: {dirs}"


def test_c5_d2_prompt_loader_path_unchanged() -> None:
    """D-2 production loader path 보존 — entity_character_list module 의 latest version 가 변경 0."""
    from app.modules.prompt_loader import _list_module_versions

    versions = _list_module_versions("entity_character_list")
    assert versions, "entity_character_list no version dir"
    latest = versions[0]
    assert latest == "2.202605011057", f"D-2 latest version 변경: {latest}"


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


@pytest.mark.parametrize("module,version_prefix,filename", MODIFIED_FILES)
def test_c6_canonical_clause_membership(module: str, version_prefix: str, filename: str) -> None:
    """9 rewritten file 모두 canonical VWR clause glyph-exact membership ≥ 1 (fixed-string)."""
    module_dir = _latest_active_dir(module, version_prefix)
    body = (module_dir / filename).read_text(encoding="utf-8")
    assert CANONICAL_VWR_CLAUSE in body, (
        f"{module_dir.name}/{filename}: missing canonical VWR clause"
    )
