"""entity_protection — G4.6 Phase 2 (RC-C).

`should_skip_low_freq` (deterministic protection cascade) +
`compute_variant_pole_ids` (RO-1 narrowed) + helper utilities 단위 검증.
"""
from __future__ import annotations

import pytest

from app.core.entity_protection import (
    _parse_traits,
    _short_id_base,
    compute_variant_pole_ids,
    should_skip_low_freq,
)


# ── _parse_traits — JSON 문자열 / list / 빈값 양쪽 ───────────────


@pytest.mark.parametrize("raw,expected", [
    (None, []),
    ("", []),
    ("{}", []),  # default JSON empty dict
    ("[]", []),
    ("[\"trait one\", \"trait two\"]", ["trait one", "trait two"]),
    (["already", "list"], ["already", "list"]),
    ("not json string", ["not json string"]),  # parse fail → 단일 원소 리스트
])
def test_parse_traits(raw, expected):
    assert _parse_traits(raw) == expected


# ── _short_id_base ───────────────────────────────────────────────


@pytest.mark.parametrize("sid,base", [
    ("C08O10", "C08"),
    ("C15", "C15"),
    ("L01", "L01"),
    ("P03", "P03"),
    ("O10", ""),  # bare 'O##' starts with O — split("O")[0] = ""
    ("", ""),
    (None, ""),
])
def test_short_id_base(sid, base):
    assert _short_id_base(sid) == base


# ── should_skip_low_freq — deterministic cascade (시나리오 의존 0건) ────


def test_should_skip_low_freq_protects_variant_self():
    """RO-1 — variant 자체 보호 (relation 기반)."""
    e = {"entity_type": "character", "name": "any", "stable_traits": "[]"}
    assert not should_skip_low_freq(
        e, count=1, is_base_for_variant=False,
        is_variant_self=True, required_by_pipeline=False,
    )


def test_should_skip_low_freq_protects_required_by_pipeline():
    """manifest 4-source cascade union 안이면 보호."""
    e = {"entity_type": "character", "name": "any", "stable_traits": "[]"}
    assert not should_skip_low_freq(
        e, count=1, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=True,
    )


def test_should_skip_low_freq_protects_base_for_variant():
    """variant pole 의 base 측은 항상 보호."""
    e = {"entity_type": "character", "name": "base", "stable_traits": "[]"}
    assert not should_skip_low_freq(
        e, count=1, is_base_for_variant=True,
        is_variant_self=False, required_by_pipeline=False,
    )


def test_should_skip_low_freq_protects_count_above_threshold():
    e = {"entity_type": "character", "name": "Recurring", "stable_traits": "[]"}
    assert not should_skip_low_freq(
        e, count=5, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=False,
    )


def test_should_skip_low_freq_unprotected_returns_true():
    """count <= 1 + 보호 조건 모두 미적용 (variant 없음, base 아님, manifest
    cascade 미참조) → skip 정상 (cost 절약)."""
    e = {"entity_type": "character", "name": "extra", "stable_traits": "[]"}
    assert should_skip_low_freq(
        e, count=1, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=False,
    )


def test_should_skip_low_freq_count_zero_unprotected_returns_true():
    e = {"entity_type": "character", "name": "Unused", "stable_traits": "[]"}
    assert should_skip_low_freq(
        e, count=0, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=False,
    )


@pytest.mark.parametrize("etype", ["location", "outlook"])
def test_should_skip_low_freq_etype_returns_false(etype):
    """RO-12 — location/outlook 은 helper 진입 시 즉시 False (caller 가 미리 skip)."""
    e = {"entity_type": etype, "name": "any"}
    assert not should_skip_low_freq(
        e, count=0, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=False,
    )


# ── compute_variant_pole_ids — RO-1 narrowed (character-character +
#    identity/transformation 만) ───────────────────────────────────


def test_compute_variant_pole_ids_transformation_character_character():
    """transformation character→character 의 second participant 만 variant pole."""
    entities = [
        {"id": "base-uuid", "entity_type": "character"},
        {"id": "variant-uuid", "entity_type": "character"},
    ]
    relations = [{"id": "rel-1", "relation_family": "transformation"}]
    participants = [
        {"relation_id": "rel-1", "canon_id": "base-uuid"},     # primary (base)
        {"relation_id": "rel-1", "canon_id": "variant-uuid"},  # variant
    ]
    poles = compute_variant_pole_ids(entities, relations, participants)
    assert poles == {"variant-uuid"}


def test_compute_variant_pole_ids_identity_character_character():
    """identity 관계도 RO-1 보호 대상."""
    entities = [
        {"id": "primary-uuid", "entity_type": "character"},
        {"id": "alias-uuid", "entity_type": "character"},
    ]
    relations = [{"id": "rel-1", "relation_family": "identity"}]
    participants = [
        {"relation_id": "rel-1", "canon_id": "primary-uuid"},
        {"relation_id": "rel-1", "canon_id": "alias-uuid"},
    ]
    poles = compute_variant_pole_ids(entities, relations, participants)
    assert poles == {"alias-uuid"}


def test_compute_variant_pole_ids_excludes_possession_prop():
    """Codex iter2 IMPORTANT — possession (character→prop) 은 variant_self 미적용.

    이전 _is_variant_pole 구현은 generic deps 그래프 의존으로 prop 도 variant
    로 분류했음. 좁힘 후 prop 은 미포함."""
    entities = [
        {"id": "char-uuid", "entity_type": "character"},
        {"id": "prop-uuid", "entity_type": "prop"},
    ]
    relations = [{"id": "rel-1", "relation_family": "possession"}]
    participants = [
        {"relation_id": "rel-1", "canon_id": "char-uuid"},
        {"relation_id": "rel-1", "canon_id": "prop-uuid"},
    ]
    assert compute_variant_pole_ids(entities, relations, participants) == set()


def test_compute_variant_pole_ids_excludes_location_to_location():
    """location-location identity/transformation 은 RO-1 character pole 좁힘
    대상 외 (location 은 별도 path)."""
    entities = [
        {"id": "loc1", "entity_type": "location"},
        {"id": "loc2", "entity_type": "location"},
    ]
    relations = [{"id": "rel-1", "relation_family": "transformation"}]
    participants = [
        {"relation_id": "rel-1", "canon_id": "loc1"},
        {"relation_id": "rel-1", "canon_id": "loc2"},
    ]
    assert compute_variant_pole_ids(entities, relations, participants) == set()


def test_compute_variant_pole_ids_excludes_unrelated_family():
    """transformation/identity 외 relation_family 무시."""
    entities = [
        {"id": "c1", "entity_type": "character"},
        {"id": "c2", "entity_type": "character"},
    ]
    relations = [{"id": "rel-1", "relation_family": "antagonism"}]  # non-target
    participants = [
        {"relation_id": "rel-1", "canon_id": "c1"},
        {"relation_id": "rel-1", "canon_id": "c2"},
    ]
    assert compute_variant_pole_ids(entities, relations, participants) == set()


def test_compute_variant_pole_ids_with_real_dep_graph_alignment():
    """build_visual_dependency_graph 와 동일 결과 (character-character의
    transformation 만) — 두 함수의 character-pole 부분이 일관되게 동작."""
    from app.modules.entity_dependency import build_visual_dependency_graph

    entities = [
        {"id": "base", "entity_type": "character"},
        {"id": "variant", "entity_type": "character"},
        {"id": "prop", "entity_type": "prop"},
    ]
    relations = [
        {"id": "rel-1", "relation_family": "transformation"},
        {"id": "rel-2", "relation_family": "possession"},
    ]
    participants = [
        {"relation_id": "rel-1", "canon_id": "base"},
        {"relation_id": "rel-1", "canon_id": "variant"},
        {"relation_id": "rel-2", "canon_id": "base"},
        {"relation_id": "rel-2", "canon_id": "prop"},
    ]
    deps = build_visual_dependency_graph(entities, relations, participants)
    poles = compute_variant_pole_ids(entities, relations, participants)

    # generic deps 는 possession 도 dep 생성 — prop deps non-empty
    assert deps["prop"] != set()
    # variant pole 좁힘 — character variant 만 (prop 제외)
    assert poles == {"variant"}


# ── source 4 (DB) 제거 회귀 — count=1 일반 entity 가 skip 되는지 ─────


def test_should_skip_low_freq_count_one_unprotected_still_skipped_after_db_source_removed():
    """Codex iter2 BLOCKING 1 — DB EntityEpisodeLink.t2i_appearance_count >= 1
    source 제거 후 count=1 일반 entity (kind/variant/required 모두 미적용) 는
    여전히 skip. low_freq_skip 정책 보존."""
    e = {"entity_type": "character", "name": "단역", "stable_traits": "[]"}
    assert should_skip_low_freq(
        e, count=1, is_base_for_variant=False,
        is_variant_self=False, required_by_pipeline=False,
    ) is True


# ── Phase 1 — _collect_reference_required_ids narrow 집합 검증 ──────


import json  # noqa: E402 — appended block
from app.core import entity_protection
from app.core.config import settings


def _write_cp(tmp_path, monkeypatch, pid, eid, step_id, data):
    monkeypatch.setattr(settings, "projects_dir", str(tmp_path))
    d = tmp_path / pid / "checkpoints" / "episodes" / eid / step_id
    d.mkdir(parents=True, exist_ok=True)
    (d / "manifest.json").write_text(json.dumps({"data": data}), encoding="utf-8")


def test_narrow_set_is_only_scene_detail_required_refs(tmp_path, monkeypatch):
    """narrow 집합 = scene_detail.required_refs[].id 만. visible_entities 제외,
    scene_director / shot_director / shot_validator 전부 제외."""
    _write_cp(tmp_path, monkeypatch, "p", "e", "scene_director",
              {"scenes": [{"present_entity_ids": ["C04"]}]})
    _write_cp(tmp_path, monkeypatch, "p", "e", "shot_director",
              {"scenes": [{"shots": [{"visible_entity_ids": ["C05"]}]}]})
    _write_cp(tmp_path, monkeypatch, "p", "e", "scene_detail",
              {"scenes": [{
                  "visible_entities": ["C06"],
                  "render_prompt_card": {"asset_requirements": {"required_refs": [
                      {"kind": "character", "id": "C01"},
                      {"kind": "character_outlook", "id": "C02O03"},
                  ]}}}]})
    got = entity_protection._collect_reference_required_ids("p", "e")
    assert got == {"C01", "C02"}  # composite → base. C04/C05/C06 미포함


def test_narrow_set_empty_when_no_scene_detail(tmp_path, monkeypatch):
    monkeypatch.setattr(settings, "projects_dir", str(tmp_path))
    assert entity_protection._collect_reference_required_ids("p", "e") == set()
