"""entity_protection — _collect_required_entity_ids manifest cascade integration.

manifest 4 종류 (scene_director / shot_validator / shot_director / scene_detail)
를 임시 디렉토리에 박고 union 정확성 검증. settings.projects_dir override.

DB EntityEpisodeLink source 는 Codex iter2 BLOCKING 으로 제거 (low_freq_skip
정책 자체와 충돌) — 본 모듈 시그니처에서도 db 인자 없음.
"""
from __future__ import annotations

import json
from pathlib import Path

import pytest

from app.core.entity_protection import _collect_required_entity_ids


@pytest.fixture
def tmp_pid_eid():
    return "test-pid-g4-6", "test-eid-g4-6"


def _write_manifest(base: Path, step: str, payload: dict) -> None:
    step_dir = base / step
    step_dir.mkdir(parents=True, exist_ok=True)
    (step_dir / "manifest.json").write_text(json.dumps(payload), encoding="utf-8")


@pytest.fixture
def manifest_dir(tmp_path, tmp_pid_eid, monkeypatch):
    """tmp 에 5 manifest 박고 settings.projects_dir override."""
    pid, eid = tmp_pid_eid
    base = tmp_path / "projects" / pid / "checkpoints" / "episodes" / eid

    _write_manifest(base, "scene_director", {
        "data": {"scenes": [
            {"scene_index": 1, "present_entity_ids": ["C07", "C15", "L01"]},
            {"scene_index": 2, "present_entity_ids": ["C07O09"]},  # composite → C07
        ]}
    })
    _write_manifest(base, "shot_validator", {
        "data": {"scenes": [
            {"scene_index": 2, "shots": [
                {"shot_index": 4, "character_ids": ["C08", "C09"]},
            ]}
        ]}
    })
    _write_manifest(base, "shot_director", {
        "data": {"scenes": [
            {"scene_index": 3, "shots": [
                {"shot_index": 1, "visible_entity_ids": ["C13", "C04", "L02"]},
                {"shot_index": 2, "visible_entity_ids": ["C13O09"]},  # composite → C13
            ]},
        ]}
    })
    _write_manifest(base, "scene_detail", {
        "data": {"scenes": [
            {
                "scene_index": 5, "_shot_index": 1,
                "visible_entities": ["C02", "L05"],
                "render_prompt_card": {
                    "asset_requirements": {
                        "required_refs": [{"id": "C03"}, {"id": "C03O09"}],
                    }
                },
            }
        ]}
    })

    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path / "projects"))
    return base


def test_collect_required_unions_all_sources(manifest_dir, tmp_pid_eid):
    """4 manifest source 전부 union (scene_director + shot_validator +
    shot_director + scene_detail rescue)."""
    pid, eid = tmp_pid_eid
    ids = _collect_required_entity_ids(pid, eid)
    expected = {
        # scene_director.present_entity_ids
        "C07", "C15", "L01",
        # shot_validator.character_ids
        "C08", "C09",
        # shot_director.data.scenes[].shots[].visible_entity_ids
        "C13", "C04", "L02",
        # scene_detail rescue
        "C02", "L05", "C03",
    }
    assert expected <= ids


def test_collect_required_missing_manifest_returns_empty(tmp_path, tmp_pid_eid, monkeypatch):
    """manifest 부재 시 fail X — 빈 set 반환."""
    pid, eid = tmp_pid_eid
    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path / "projects"))
    ids = _collect_required_entity_ids(pid, eid)
    assert ids == set()


def test_collect_required_corrupt_manifest_skipped(manifest_dir, tmp_pid_eid, monkeypatch):
    """깨진 manifest.json 은 fail X — 해당 source skip + warning log + 다른 source 진행."""
    pid, eid = tmp_pid_eid
    base = manifest_dir
    (base / "scene_director" / "manifest.json").write_text("not json {", encoding="utf-8")

    ids = _collect_required_entity_ids(pid, eid)
    # source 1 (scene_director) 는 corrupt 로 skip — 나머지 source 들 정상
    assert "C07" not in ids or "C15" not in ids or "L01" not in ids
    # 나머지 manifest source 는 살아있어야 함
    assert {"C08", "C09", "C13", "C04", "C02"} <= ids


def test_collect_required_shot_director_production_shape(tmp_path, tmp_pid_eid, monkeypatch):
    """Codex iter1 B2 — shot_director production schema 는 data.scenes[].shots[].
    visible_entity_ids 임. legacy shape (data.shots[].character_angles[]) 가
    아닌 live shape 에서 ID 추출 검증."""
    pid, eid = tmp_pid_eid
    base = tmp_path / "projects" / pid / "checkpoints" / "episodes" / eid
    _write_manifest(base, "shot_director", {
        "data": {"scenes": [
            {"scene_index": 1, "shots": [
                {"shot_index": 4, "visible_entity_ids": ["C42", "L99"]},
                {"shot_index": 5, "visible_entity_ids": ["C42O11"]},  # composite → C42
            ]}
        ]}
    })
    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path / "projects"))
    ids = _collect_required_entity_ids(pid, eid)
    assert ids == {"C42", "L99"}


def test_collect_required_short_id_base_normalization(tmp_path, tmp_pid_eid, monkeypatch):
    """C##O## composite → C## base normalization (RO-21 carry — outlook 별도 path)."""
    pid, eid = tmp_pid_eid
    base = tmp_path / "projects" / pid / "checkpoints" / "episodes" / eid
    _write_manifest(base, "scene_director", {
        "data": {"scenes": [{"scene_index": 1,
                              "present_entity_ids": ["C08O10", "C09O11", "C15"]}]}
    })
    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path / "projects"))
    ids = _collect_required_entity_ids(pid, eid)
    assert ids == {"C08", "C09", "C15"}
