"""Area #4 W4 closure canary fixture replay tests.

3 시나리오 deterministic (Codex plan iter 1 I3 fix):
- canary_1: full + close overlap → _detect_framing_conflicts returns non-empty
- canary_2: separated shots (no overlap) → returns empty
- canary_3: old v6 cp (schema_version 2 + element_scope missing) → manifest schema_version 3
  mismatch detected, stale cp rejected, v7 path invoked via call_structured.

Fixture files: backend/tests/_audit_outputs/area_4_w4/canary_*.jsonl
"""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any, Dict
from unittest.mock import patch

import pytest


CANARY_DIR = Path(__file__).resolve().parent.parent.parent.parent / "tests" / "_audit_outputs" / "area_4_w4"


def _load_canary(name: str) -> Dict[str, Any]:
    return json.loads((CANARY_DIR / name).read_text())


def _write_cp(tmp_path: Path, project_id: str, episode_id: str, step_id: str, manifest: dict) -> None:
    cp_dir = tmp_path / project_id / "checkpoints" / "episodes" / episode_id / step_id
    cp_dir.mkdir(parents=True, exist_ok=True)
    (cp_dir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False), encoding="utf-8")


# ─────────────────────────────────────────────
# Canary 1: full + close overlap → conflict detected
# ─────────────────────────────────────────────

def test_canary_1_full_close_overlap_detected():
    """canary_1: 같은 character_name + applies_to_shots 겹침 + 다른 element_scope → violation."""
    from app.core.steps.scene_consistency_step import _detect_framing_conflicts

    fixture = _load_canary("canary_1_full_close_overlap.jsonl")
    scene = {"fixed_elements": fixture["fixed_elements"]}
    violations = _detect_framing_conflicts(scene)
    expected_shots = sorted(fixture["expected_violation_shots"])
    actual_shots = sorted(v["shot_index"] for v in violations)
    assert actual_shots == expected_shots
    for v in violations:
        assert v["character_name"] == "Victim"
        assert sorted(v["framings"]) == ["close", "full"]


# ─────────────────────────────────────────────
# Canary 2: separated shots → no conflict
# ─────────────────────────────────────────────

def test_canary_2_separated_shots_no_conflict():
    """canary_2: full + close 가 다른 shot 집합 → conflict 없음."""
    from app.core.steps.scene_consistency_step import _detect_framing_conflicts

    fixture = _load_canary("canary_2_separated_shots.jsonl")
    scene = {"fixed_elements": fixture["fixed_elements"]}
    violations = _detect_framing_conflicts(scene)
    assert violations == fixture["expected_violation_shots"] == []


# ─────────────────────────────────────────────
# Canary 3: old v6 cp force rerun (deterministic — Codex plan iter 1 I3 fix)
# ─────────────────────────────────────────────

def test_canary_3_old_v6_cp_force_rerun(tmp_path, monkeypatch):
    """canary_3: old v6 cp (schema_version=2 + element_scope missing) + current manifest=3
    → mismatch detected via StepRunner._check_cp_mismatch helper, v7 path invoked via
    call_structured mock in force mode.

    Codex range review I1 fix — actual cp_mismatch helper invocation 입증.
    No production DB write — tmp_path isolated fixture.
    """
    from app.core.steps.scene_consistency_step import (
        SCENE_CONSISTENCY_SCHEMA_VERSION,
        SceneConsistencyStep,
    )

    # Current manifest schema_version assertion (W1 fix)
    assert SCENE_CONSISTENCY_SCHEMA_VERSION == 3, "W1 schema_version bump 2→3 missing"

    fixture = _load_canary("canary_3_old_v6_cp_force_rerun.jsonl")
    stale_cp = fixture["cp_payload"]
    assert stale_cp["schema_version"] == 2, "canary fixture stale cp must be schema_version=2"
    assert SCENE_CONSISTENCY_SCHEMA_VERSION != stale_cp["schema_version"], (
        "current manifest schema_version must NOT equal stale cp schema_version "
        "(cp_mismatch trigger condition)"
    )

    # Verify stale cp's fixed_elements indeed lack element_scope (v6 shape).
    stale_fe = stale_cp["data"]["scenes"][0]["fixed_elements"][0]
    assert "element_scope" not in stale_fe, "v6 cp must lack element_scope field"

    # ── Part A (Codex range review I1 fix): StepRunner._check_cp_mismatch helper
    # 직접 호출로 schema_version mismatch deterministic verify.
    mismatch_step = SceneConsistencyStep.__new__(SceneConsistencyStep)
    mismatch_step.manifest = {"schema_version": SCENE_CONSISTENCY_SCHEMA_VERSION}
    mismatch_reason = mismatch_step._check_cp_mismatch(stale_cp)
    assert mismatch_reason is not None, "stale v6 cp must trigger cp_mismatch (None = match)"
    assert "schema_version mismatch" in mismatch_reason
    assert "체크포인트=2" in mismatch_reason
    assert "현재=3" in mismatch_reason

    # ── Part B: force-mode execution path verifies v7 invocation in tmp_path isolation.
    # (force mode 는 resume cp_mismatch 경로와 별개 — production code 의 v7 rerun
    # path 가 정상 동작함을 입증. Part A 가 stale cp rejection 자체를 deterministic
    # 검증, Part B 가 v7 producer 호출 가능성을 검증.)
    _write_cp(tmp_path, "P1", "E1", "scene_consistency", stale_cp)

    # Minimal deps setup (mode='force' bypasses resume reuse anyway, ensures clean v7 invocation).
    _write_cp(tmp_path, "P1", "E1", "scene_save",
              {"data": {"segments": [{"scene_index": 1, "text": "scene"}]}})
    _write_cp(tmp_path, "P1", "E1", "shot_validator",
              {"data": {"scenes": [{"scene_index": 1, "shots": [
                  {"shot_index": 1, "description": "x"},
                  {"shot_index": 2, "description": "y"},
              ]}]}})
    _write_cp(tmp_path, "P1", "E1", "shot_selection",
              {"data": {"scenes": [{"scene_index": 1, "selected_shot_indices": [1, 2]}]}})

    step = SceneConsistencyStep.__new__(SceneConsistencyStep)
    step.project_id = "P1"
    step.episode_id = "E1"
    step.step_id = "scene_consistency"
    step.project_config = {}
    step.opik_context = {}
    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path))

    # v7 path expected response (element_scope emit).
    # source_facts non-empty + confidence='high' — contract validator
    # (assert_fresh_llm_evidence) 통과 의무.
    v7_resp = {
        "scene_index": 1, "analysis_summary": "v7 clean",
        "fixed_elements": [
            {"element_id": "victim_a", "element_type": "character_state",
             "character_name": "Victim", "description": "lying face-down",
             "applies_to_shots": [1, 2], "element_scope": "full",
             "source_facts": ["scene narration: lying motionless"],
             "visual_inferences": ["left-side recumbent pose"],
             "creative_decisions": ["element_scope=full anchor"],
             "confidence": "high"},
        ],
    }
    with patch("app.core.steps.scene_consistency_step.call_structured",
               return_value=v7_resp) as mock_call:
        # mode='force' → existing_ok reuse 우회, 강제 v7 path 실행.
        result = step._execute(mode="force")
        # v7 path invoked (cp_mismatch retry simulated by force mode).
        mock_call.assert_called_once()

    by_idx = {s["scene_index"]: s for s in result["data"]["scenes"]}
    # v7 결과 적용됨 — element_scope 있는 fixed_element 보유.
    assert by_idx[1]["status"] == "ok"
    assert "element_scope" in by_idx[1]["fixed_elements"][0]
    assert by_idx[1]["fixed_elements"][0]["element_scope"] == "full"
