"""scene_verify (Step 17) 단위 테스트.

검증 항목:
- check_applicability: 다중 캐릭터 씬이 있을 때만 True
- 앞 2씬 컨텍스트가 user_prompt에 포함
- 단일 엔티티 씬은 검증 건너뜀
"""

import json
from unittest.mock import MagicMock, patch

import pytest


# -- Fixtures --

@pytest.fixture()
def fake_db():
    db = MagicMock()
    db.execute.return_value.fetchone.return_value = None
    return db


# -- Sample Data --

SAMPLE_SEGMENTS = [
    {"scene_index": 1, "heading": "씬1", "start_char": 0, "end_char": 50, "length": 50},
    {"scene_index": 2, "heading": "씬2", "start_char": 50, "end_char": 100, "length": 50},
    {"scene_index": 3, "heading": "씬3", "start_char": 100, "end_char": 150, "length": 50},
    {"scene_index": 4, "heading": "씬4", "start_char": 150, "end_char": 200, "length": 50},
]

SAMPLE_DETAIL_SCENES_MULTI = [
    {"scene_index": 1, "visible_entities": ["C01"], "beat_title": "t1"},
    {"scene_index": 2, "visible_entities": ["C01", "C02"], "beat_title": "t2"},
    {"scene_index": 3, "visible_entities": ["C01", "C02", "C03"], "beat_title": "t3"},
    {"scene_index": 4, "visible_entities": ["C04"], "beat_title": "t4"},
]

SAMPLE_DETAIL_SCENES_SINGLE = [
    {"scene_index": 1, "visible_entities": ["C01"], "beat_title": "t1"},
    {"scene_index": 2, "visible_entities": ["C02"], "beat_title": "t2"},
]

FULLTEXT = "A" * 50 + "B" * 50 + "C" * 50 + "D" * 50


def _make_step(fake_db, checkpoints=None, applicability="if_multi_char_scenes"):
    """SceneVerifyStep 인스턴스를 생성하는 헬퍼."""
    with patch("app.core.steps.detail_steps.StepRunner.__init__", return_value=None):
        from app.core.steps.detail_steps import SceneVerifyStep

        step = SceneVerifyStep.__new__(SceneVerifyStep)
        step.step_id = "scene_verify"
        step.project_id = "proj1"
        step.episode_id = "ep1"
        step.db = fake_db
        step.project_config = {}
        step.manifest = {"applicability": applicability}
        step.opik_context = {}

        if checkpoints:
            step._load_prev_checkpoint = lambda sid: checkpoints.get(sid)
        else:
            step._load_prev_checkpoint = lambda sid: None

        step._load_cleaned_text = lambda: FULLTEXT

        return step


class TestSceneVerifyApplicability:

    def test_true_when_multi_char_scenes_exist(self, fake_db):
        """다중 캐릭터 씬(visible_entities >= 2)이 있으면 True."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": SAMPLE_DETAIL_SCENES_MULTI}},
        }
        step = _make_step(fake_db, checkpoints)
        assert step.check_applicability() is True

    def test_false_when_only_single_char_scenes(self, fake_db):
        """모든 씬이 단일 엔티티면 False."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": SAMPLE_DETAIL_SCENES_SINGLE}},
        }
        step = _make_step(fake_db, checkpoints)
        assert step.check_applicability() is False

    def test_false_when_no_detail_checkpoint(self, fake_db):
        """scene_detail 체크포인트가 없으면 False."""
        step = _make_step(fake_db, checkpoints={})
        assert step.check_applicability() is False

    def test_true_when_applicability_always(self, fake_db):
        """applicability가 'always'면 항상 True."""
        step = _make_step(fake_db, checkpoints={}, applicability="always")
        assert step.check_applicability() is True

    def test_false_when_scenes_empty(self, fake_db):
        """scenes가 빈 배열이면 False."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": []}},
        }
        step = _make_step(fake_db, checkpoints)
        assert step.check_applicability() is False


class TestSceneVerifyExecution:

    @pytest.mark.skip(reason="W3-3 cluster B drift — v3/v2 계약이 현재(v4/shot-more) 구현과 어긋남. docs/review-codex-1/11-fix-plan.md §8.2 참조. 복원/재작성은 Wave 5 이후 재평가.")
    def test_prev_2_scenes_context_in_prompt(self, fake_db):
        """앞 2개 씬 텍스트가 user_prompt에 포함."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": SAMPLE_DETAIL_SCENES_MULTI}},
            "scene_save": {"data": {"segments": SAMPLE_SEGMENTS}},
        }
        step = _make_step(fake_db, checkpoints)

        captured_prompts = {}

        def _capture_call(**kwargs):
            prompt = kwargs.get("user_prompt", "")
            # Extract scene index from schema_name
            sname = kwargs.get("schema_name", "")
            si = int(sname.split("_")[-1]) if sname else 0
            captured_prompts[si] = prompt
            return {"verified_entities": []}

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", side_effect=_capture_call):
            step._execute()

        # Scene 3 (index 3) should have scenes 1 and 2 as context
        assert 3 in captured_prompts
        prompt3 = captured_prompts[3]
        # Scene 1 text is "A"*50, Scene 2 text is "B"*50
        assert "A" * 50 in prompt3
        assert "B" * 50 in prompt3

    def test_single_entity_scenes_skipped(self, fake_db):
        """단일 엔티티 씬은 LLM 호출 없이 그대로 반환."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": SAMPLE_DETAIL_SCENES_MULTI}},
            "scene_save": {"data": {"segments": SAMPLE_SEGMENTS}},
        }
        step = _make_step(fake_db, checkpoints)

        call_count = 0

        def _counting_call(**kwargs):
            nonlocal call_count
            call_count += 1
            return {"verified_entities": []}

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", side_effect=_counting_call):
            result = step._execute()

        # Scenes 1 and 4 have single entities -> skipped (no LLM call)
        # Scenes 2 and 3 have multi entities -> LLM called
        assert call_count == 2

        # All 4 scenes should be in results
        assert result["completed_count"] == 4

    def test_verification_added_to_scene(self, fake_db):
        """검증 결과가 scene에 verification 키로 추가."""
        scenes = [
            {"scene_index": 1, "visible_entities": ["C01", "C02"], "beat_title": "t1"},
        ]
        segments = [
            {"scene_index": 1, "start_char": 0, "end_char": 50},
        ]
        checkpoints = {
            "scene_detail": {"data": {"scenes": scenes}},
            "scene_save": {"data": {"segments": segments}},
        }
        step = _make_step(fake_db, checkpoints)

        verify_response = {
            "verified_entities": [
                {"entity_id": "C01", "physically_visible": True, "reason": "씬에 등장"},
                {"entity_id": "C02", "physically_visible": False, "reason": "언급만"},
            ]
        }

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", return_value=verify_response):
            result = step._execute()

        scene = result["data"]["scenes"][0]
        assert "verification" in scene
        assert len(scene["verification"]["verified_entities"]) == 2
        assert scene["verification"]["verified_entities"][0]["entity_id"] == "C01"

    def test_results_sorted_by_scene_index(self, fake_db):
        """결과가 scene_index 순서로 정렬."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": SAMPLE_DETAIL_SCENES_MULTI}},
            "scene_save": {"data": {"segments": SAMPLE_SEGMENTS}},
        }
        step = _make_step(fake_db, checkpoints)

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", return_value={"verified_entities": []}):
            result = step._execute()

        indices = [s["scene_index"] for s in result["data"]["scenes"]]
        assert indices == [1, 2, 3, 4]

    def test_verify_failure_does_not_crash(self, fake_db):
        """검증 LLM 호출 실패 시 scene은 그대로 반환 (verification 없이)."""
        scenes = [
            {"scene_index": 1, "visible_entities": ["C01", "C02"], "beat_title": "t1"},
        ]
        segments = [
            {"scene_index": 1, "start_char": 0, "end_char": 50},
        ]
        checkpoints = {
            "scene_detail": {"data": {"scenes": scenes}},
            "scene_save": {"data": {"segments": segments}},
        }
        step = _make_step(fake_db, checkpoints)

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", side_effect=RuntimeError("fail")):
            result = step._execute()

        # Scene still returned, just without verification
        assert result["completed_count"] == 1
        scene = result["data"]["scenes"][0]
        assert "verification" not in scene

    def test_empty_scenes_returns_empty(self, fake_db):
        """씬이 없으면 빈 결과."""
        checkpoints = {
            "scene_detail": {"data": {"scenes": []}},
            "scene_save": {"data": {"segments": []}},
        }
        step = _make_step(fake_db, checkpoints)

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}):
            result = step._execute()

        assert result["completed_count"] == 0
        assert result["applicable_count"] == 0
        assert result["data"]["scenes"] == []

    def test_scene_1_has_no_prev_context(self, fake_db):
        """씬 1은 이전 씬이 없으므로 prev_texts가 비어있음."""
        scenes = [
            {"scene_index": 1, "visible_entities": ["C01", "C02"], "beat_title": "t1"},
        ]
        segments = [
            {"scene_index": 1, "start_char": 0, "end_char": 50},
        ]
        checkpoints = {
            "scene_detail": {"data": {"scenes": scenes}},
            "scene_save": {"data": {"segments": segments}},
        }
        step = _make_step(fake_db, checkpoints)

        captured_prompts = []

        def _capture_call(**kwargs):
            captured_prompts.append(kwargs.get("user_prompt", ""))
            return {"verified_entities": []}

        with patch("app.core.steps.detail_steps.load_prompt", return_value="sys"), \
             patch("app.core.steps.detail_steps.load_schema", return_value={}), \
             patch("app.core.steps.detail_steps.call_structured", side_effect=_capture_call):
            step._execute()

        assert len(captured_prompts) == 1
        # "앞 씬 컨텍스트:" followed by empty prev_texts joined
        prompt = captured_prompts[0]
        assert "앞 씬 컨텍스트:\n\n" in prompt
