"""C7 — shot_dependency_t2i keep_elements[].subject_kind structured signal.

fix-critical-1 Tier β #4. roadmap §5.12 (Area #12). producer
(shot_dependency_t2i) 가 LLM 으로 emit 하는 keep_elements[] entry 에
subject_kind enum (non_human_visual_element / human_or_character_reference)
신설. code 는 enum 값만 소비 — human_or_character_reference 는 fail-fast
(routing layer 안내). prompt 의 비-enforced 11-lexicon ban + false fail-fast
claim 폐기.

no live LLM, NO VLM — helper unit + v8 schema/version source canary +
scene_context_loader envelope behavioral test.

G1 valid 비인물 entry (environment + static_prop) 통과
G2 subject_kind=human_or_character_reference → AppError + routing layer 안내
G3 subject_kind 누락 → AppError "missing required keys"
G4 invalid subject_kind 값 → AppError "subject_kind must be one of"
G5 boundary preserve — invalid kind 여전히 fail-fast + KEEP_ELEMENT_KINDS 불변
G6 v8 schema.json keep_elements item strict (subject_kind enum / required / kind enum)
G7 version cascade — schema_version 4 / MODULE_VERSIONS 1.5.0 / v8 / allowlist 불변
G8 v8 system.md / schema.json residue canary
G9 scene_context_loader._load_dependencies() envelope — v8 통과 / v7 거부
"""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any, Dict

import pytest

from app.core.errors import AppError
from app.core.keep_elements import (
    KEEP_ELEMENT_KINDS,
    KEEP_ELEMENT_SUBJECT_KINDS,
    validate_keep_elements_entry,
)


_ERROR_CODE = "step.shot_dependency_t2i.keep_elements_shape_invalid"

# pytest 기본 gate 는 CWD=backend 로 실행되므로 prompts/_base 상대경로는 깨진다.
# repo root 를 __file__ 에서 resolve (sibling test_c1/test_c2 와 동일 패턴).
_REPO_ROOT = Path(__file__).resolve().parents[3]
_SHOT_DEP_T2I_PROMPT_ROOT = _REPO_ROOT / "prompts" / "_base" / "shot_dependency_t2i"


def _entry(label="wooden bench against the wall", kind="environment",
           subject_kind="non_human_visual_element"):
    return {"label": label, "kind": kind, "subject_kind": subject_kind}


def _v8_schema() -> dict:
    dirs = sorted(_SHOT_DEP_T2I_PROMPT_ROOT.glob("8.*"))
    assert dirs, "v8 prompt 디렉토리 없음 — C7 미완"
    return json.loads((dirs[-1] / "schema.json").read_text(encoding="utf-8"))


# ── G1 ──────────────────────────────────────────────────────────────────

def test_g1_valid_non_human_entry_passes():
    """G1 — valid 비인물 entry (environment + static_prop) 통과."""
    validate_keep_elements_entry(
        _entry(kind="environment"),
        label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
    )
    validate_keep_elements_entry(
        _entry(label="broken vase on the floor", kind="static_prop"),
        label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
    )


# ── G2 ──────────────────────────────────────────────────────────────────

def test_g2_human_or_character_reference_fails_fast():
    """G2 — subject_kind=human_or_character_reference entry 는 fail-fast,
    message 에 routing layer 명시."""
    with pytest.raises(AppError) as excinfo:
        validate_keep_elements_entry(
            _entry(label="the standing detective", kind="environment",
                   subject_kind="human_or_character_reference"),
            label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
        )
    assert excinfo.value.code == _ERROR_CODE
    msg = str(excinfo.value.message)
    assert "scene_consistency.character_state" in msg
    assert "character_state_variant" in msg
    assert "semantic_contract_router" in msg


# ── G3 ──────────────────────────────────────────────────────────────────

def test_g3_missing_subject_kind_fails_fast():
    """G3 — subject_kind 누락 → presence check fail-fast."""
    with pytest.raises(AppError) as excinfo:
        validate_keep_elements_entry(
            {"label": "wooden bench", "kind": "environment"},
            label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
        )
    assert excinfo.value.code == _ERROR_CODE
    assert "missing required keys" in str(excinfo.value.message)


# ── G4 ──────────────────────────────────────────────────────────────────

def test_g4_invalid_subject_kind_fails_fast():
    """G4 — invalid subject_kind enum 값 → fail-fast."""
    with pytest.raises(AppError) as excinfo:
        validate_keep_elements_entry(
            _entry(subject_kind="unknown_subject"),
            label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
        )
    assert excinfo.value.code == _ERROR_CODE
    assert "subject_kind must be one of" in str(excinfo.value.message)


# ── G5 ──────────────────────────────────────────────────────────────────

def test_g5_boundary_preserve_kind_enum():
    """G5 — boundary preserve: invalid kind 여전히 fail-fast (subject_kind 추가가
    kind enum check 본체를 바꾸지 않음) + KEEP_ELEMENT_KINDS 2종 불변."""
    with pytest.raises(AppError) as excinfo:
        validate_keep_elements_entry(
            _entry(label="x", kind="immobilized_character"),
            label_source="S1_Shot2 loc_ref[0]", error_code=_ERROR_CODE,
        )
    assert excinfo.value.code == _ERROR_CODE
    assert "must be one of" in str(excinfo.value.message)
    assert KEEP_ELEMENT_KINDS == frozenset({"environment", "static_prop"})


# ── G6 ──────────────────────────────────────────────────────────────────

def test_g6_v8_schema_keep_elements_item_strict():
    """G6 — v8 schema.json keep_elements item strict."""
    schema = _v8_schema()
    keep_items = (
        schema["properties"]["dependencies"]["items"]["properties"]
        ["location_refs"]["items"]["properties"]["keep_elements"]["items"]
    )
    assert set(keep_items["required"]) == {"label", "kind", "subject_kind"}
    assert keep_items["additionalProperties"] is False
    assert set(keep_items["properties"]["kind"]["enum"]) == set(KEEP_ELEMENT_KINDS)
    assert (
        set(keep_items["properties"]["subject_kind"]["enum"])
        == set(KEEP_ELEMENT_SUBJECT_KINDS)
    )


# ── G7 ──────────────────────────────────────────────────────────────────

def test_g7_version_cascade():
    """G7 — version cascade: schema_version 4 / MODULE_VERSIONS 1.5.0 / v8 /
    _LEGACY_SCHEMA_BUMP_ALLOWLIST 불변."""
    from app.core.step_manifest import STEP_MANIFEST, _LEGACY_SCHEMA_BUMP_ALLOWLIST
    from app.core.version_registry import MODULE_VERSIONS, get_module_info

    assert STEP_MANIFEST["shot_dependency_t2i"]["schema_version"] == 4
    assert MODULE_VERSIONS["shot_dependency_t2i"] == "1.5.0"
    assert (
        get_module_info("shot_dependency_t2i")["prompt_dependency"]
        == "shot_dependency_t2i/v8"
    )
    assert sorted(_SHOT_DEP_T2I_PROMPT_ROOT.glob("8.*")), "v8 dir 부재"
    assert _LEGACY_SCHEMA_BUMP_ALLOWLIST == frozenset({"entity_t2i"})


# ── G8 ──────────────────────────────────────────────────────────────────

def test_g8_v8_prompt_residue_canary():
    """G8 — v8 system.md / schema.json residue canary."""
    dirs = sorted(_SHOT_DEP_T2I_PROMPT_ROOT.glob("8.*"))
    assert dirs, "v8 prompt 디렉토리 없음"
    sys_md = (dirs[-1] / "system.md").read_text(encoding="utf-8")
    # false claim (label 패턴이 enum 위반으로 거부) 부재 — code 는 label 내용 검사 X
    assert "enum 위반으로 fail-fast" not in sys_md
    # 'fail-fast 거부' wording 은 반드시 subject_kind 기반 정정 문맥과 동반
    for line in sys_md.splitlines():
        if "fail-fast 거부" in line:
            assert "subject_kind" in line, (
                f"fail-fast 거부 wording 이 subject_kind 동반 없이 등장: {line!r}"
            )
    # 11-lexicon ban list 부재 — "person silhouette" 는 11-lexicon 나열에만 존재
    assert "person silhouette" not in sys_md
    # subject_kind emit instruction 존재
    assert "subject_kind" in sys_md
    assert "non_human_visual_element" in sys_md
    # v8 schema.json ignore_elements description 의 conditional closed-list 열거 부재
    schema = _v8_schema()
    ignore_desc = (
        schema["properties"]["dependencies"]["items"]["properties"]
        ["location_refs"]["items"]["properties"]["ignore_elements"]["description"]
    )
    assert "only if" not in ignore_desc


# ── G9 ──────────────────────────────────────────────────────────────────

class _FakeRunner:
    project_id = "p"
    episode_id = "e"
    db = None
    project_config = None

    def __init__(self, cps: Dict[str, Any]) -> None:
        self._cps = cps

    def _load_prev_checkpoint(self, step_id: str):
        return self._cps.get(step_id)

    def _get_step_run(self, step_id: str):
        return None

    def build_opik_metadata(self):
        return {}


def _loader(cps: Dict[str, Any]):
    from app.core.steps.scene_context_loader import SceneContextLoader
    return SceneContextLoader(_FakeRunner(cps))


def test_g9_scene_context_loader_envelope_v8_passes_v7_rejected():
    """G9 — scene_context_loader._load_dependencies() envelope (Codex spec
    review BLOCKING 1 — consumer-break 방지): v8 cp (schema_version 4) 통과 /
    v7 cp (schema_version 3) → legacy AppError."""
    v8_cp = {
        "shot_dependency_t2i": {
            "schema_version": 4,
            "data": {"dependencies": [{
                "scene_index": 1, "shot_index": 2,
                "location_refs": [{
                    "ref_usage": "exact_background",
                    "ignore_elements": "",
                    "keep_elements": [
                        {"label": "wooden bench", "kind": "environment",
                         "subject_kind": "non_human_visual_element"},
                    ],
                }],
            }]},
        },
    }
    deps = _loader(v8_cp)._load_dependencies()
    assert len(deps) == 1

    v7_cp = {
        "shot_dependency_t2i": {
            "schema_version": 3,
            "data": {"dependencies": [{
                "scene_index": 1, "shot_index": 2,
                "location_refs": [{"keep_elements": []}],
            }]},
        },
    }
    with pytest.raises(AppError) as excinfo:
        _loader(v7_cp)._load_dependencies()
    assert excinfo.value.code == "step.scene_context_loader.legacy_keep_elements_cp"
