"""background_share_plan — 에피소드 전체 배경 공유·참조 계획 (재설계 B-2).

결정론 검증만: 커버리지·첫 샷 background·prev 앵커 위상·인용 실재.
LLM 판단 품질은 여기서 주장하지 않는다.
"""
from __future__ import annotations

import pytest

from app.modules.pipeline.background_share_plan import (
    build_share_schema,
    resolve_prompt_version,
    validate_share_plan,
)


def _shots():
    return ["S2sh1", "S2sh3", "S5sh1", "S9sh2"]


def _texts():
    return {2: "골목에서 남자가 고개를 든다", 5: "옥탑에 앉아 있다",
            9: "다시 골목으로 돌아온다"}


def _plan():
    return {
        "share_groups": [
            {"group_key": "alley", "bg_authority": "seed",
             "shot_tags": ["S2sh1", "S2sh3", "S9sh2"],
             "evidence": [{"scene_index": 2,
                           "quote_ko": "골목에서 남자가 고개를 든다"},
                          {"scene_index": 9,
                           "quote_ko": "다시 골목으로 돌아온다"}]},
            {"group_key": "rooftop", "bg_authority": "interior",
             "shot_tags": ["S5sh1"],
             "evidence": [{"scene_index": 5,
                           "quote_ko": "옥탑에 앉아 있다"}]},
        ],
        "shot_plans": {
            "S2sh1": {"ref_plan": "background"},
            "S2sh3": {"ref_plan": "prev", "prev_anchor_tag": "S2sh1"},
            "S5sh1": {"ref_plan": "background"},
            "S9sh2": {"ref_plan": "prev", "prev_anchor_tag": "S2sh3"},
        },
    }


def test_schema_locks_enums():
    schema = build_share_schema()
    g = schema["properties"]["share_groups"]["items"]["properties"]
    assert set(g["bg_authority"]["enum"]) == {"seed", "plate", "interior"}


def test_pack_v1_resolves_and_neutral():
    from pathlib import Path

    resolved = resolve_prompt_version("1")
    assert resolved.startswith("1.")
    root = Path(__file__).resolve().parents[3]
    text = (root / "prompts" / "_base" / "background_share_plan" / resolved
            / "system.md").read_text(encoding="utf-8")
    for banned in ("금월", "수리영", "옥탑방 건물"):
        assert banned not in text


def test_validate_passes_clean():
    assert validate_share_plan(_plan(), _shots(), _texts()) == []


def test_validate_rejects_uncovered_or_duplicate_shot():
    p = _plan()
    p["share_groups"][0]["shot_tags"].remove("S9sh2")
    del p["shot_plans"]["S9sh2"]
    assert any("커버" in v for v in
               validate_share_plan(p, _shots(), _texts()))
    p2 = _plan()
    p2["share_groups"][1]["shot_tags"].append("S2sh1")  # 두 그룹 중복
    assert any("중복" in v for v in
               validate_share_plan(p2, _shots(), _texts()))


def test_validate_first_shot_must_be_background():
    p = _plan()
    p["shot_plans"]["S2sh1"] = {"ref_plan": "prev",
                                "prev_anchor_tag": "S2sh3"}
    assert any("첫 샷" in v for v in
               validate_share_plan(p, _shots(), _texts()))


def test_validate_prev_anchor_must_be_earlier_same_group():
    p = _plan()
    # 다른 그룹 앵커
    p["shot_plans"]["S2sh3"]["prev_anchor_tag"] = "S5sh1"
    assert any("앵커" in v for v in
               validate_share_plan(p, _shots(), _texts()))
    p2 = _plan()
    # 뒤쪽 샷 앵커 (스토리 역방향)
    p2["shot_plans"]["S2sh3"]["prev_anchor_tag"] = "S9sh2"
    assert any("앵커" in v for v in
               validate_share_plan(p2, _shots(), _texts()))
    p3 = _plan()
    # prev 인데 앵커 결손
    p3["shot_plans"]["S9sh2"] = {"ref_plan": "prev"}
    assert any("앵커" in v for v in
               validate_share_plan(p3, _shots(), _texts()))


def test_validate_evidence_quote_must_exist():
    p = _plan()
    p["share_groups"][0]["evidence"][0]["quote_ko"] = "존재하지 않는 문장"
    assert any("인용" in v for v in
               validate_share_plan(p, _shots(), _texts()))


def test_validate_rejects_duplicate_group_key():
    """v2 (E2E10 Codex 재리뷰): group_key=소비부 실질 PK — 중복 fail-closed.

    서로 다른 두 그룹이 같은 키를 가지면 groupbg/sidecar 병합·first_of_group
    overwrite·prev 앵커 그룹 오인이 생긴다.
    """
    plan = _plan()
    plan["share_groups"][1]["group_key"] = "alley"  # rooftop 그룹 키 충돌
    violations = validate_share_plan(plan, _shots(), _texts())
    assert any("group_key" in v and "중복" in v for v in violations)


def test_run_retries_until_group_key_unique():
    """중복 키 응답 → 위반 힌트 재시도 → 유일 키 응답으로 교정."""
    from app.modules.pipeline.background_share_plan import (
        run_background_share_plan,
    )

    dup = _plan()
    dup["share_groups"][1]["group_key"] = "alley"
    good = _plan()
    responses = [dup, good]
    calls = []

    def fake_structured(module, system, parts, schema, **kw):
        calls.append(parts)
        return responses.pop(0)

    out = run_background_share_plan(
        shots_block="-", scenes_block="-",
        shot_tags=_shots(), scene_texts=_texts(),
        prompt_version="1", call_structured_fn=fake_structured,
    )
    assert out["attempts"] == 2
    keys = [g["group_key"] for g in out["plan"]["share_groups"]]
    assert len(keys) == len(set(keys))
    # 재시도 힌트에 위반이 실렸는지 (fail-closed 재시도 계약 —
    # 이 러너는 content 문자열에 위반 목록을 병기한다)
    assert "group_key" in calls[1]


def test_validation_version_stamped_in_step_hash(monkeypatch):
    """검증 계약 전환(v2)이 기존 완료 CP 를 stale 로 감지해야 한다."""
    import app.modules.pipeline.background_share_plan as bsp
    from app.core.steps.background_share_plan_step import (
        BackgroundSharePlanStep,
    )

    step = BackgroundSharePlanStep.__new__(BackgroundSharePlanStep)
    base = step._config_hash()
    monkeypatch.setattr(bsp, "VALIDATION_VERSION", 99)
    assert step._config_hash() != base
