"""G3.1 consumer wiring sentinel tests.

각 consumer 가 _evidence_helpers 의 normalize 를 실제로 호출하는지 검증.
G2.2 sham 회귀 (call site 검증 누락) 방지.

monkeypatch 로 normalize 를 sentinel tracker 로 교체 → consumer 진입점 호출
→ tracker called + where marker 포함 확인.

각 wiring 별 1 class. 5 consumer = 5 class (t2i_review 만 2 호출 지점).
"""
from __future__ import annotations

import json
from typing import Any, Dict, List

import pytest


# ──────────────────────────────────────────────────────────────────────────
# Task 17: scene_context_loader._load_fixed_elements
# ──────────────────────────────────────────────────────────────────────────


class TestSceneContextLoaderWiring:
    """SceneContextLoader._load_fixed_elements 가
    _normalize_scene_consistency_result 를 호출하는지."""

    def test_load_fixed_elements_calls_normalize(self, monkeypatch):
        from app.core.steps import scene_context_loader as scl

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(scl, "_normalize_scene_consistency_result", sentinel)

        # 옛 cp = 4 evidence 필드 부재. 두 scene = 두 호출 검증.
        old_cp = {
            "data": {
                "scenes": [
                    {
                        "scene_index": 1,
                        "fixed_elements": [
                            {"element_type": "character_state", "description": "x"},
                        ],
                    },
                    {
                        "scene_index": 2,
                        "fixed_elements": [],
                    },
                ]
            }
        }

        class _Runner:
            project_id = "p"
            episode_id = "e"
            db = None

            def _load_prev_checkpoint(self, step_id: str):
                return old_cp if step_id == "scene_consistency" else None

        loader = scl.SceneContextLoader(_Runner())
        result = loader._load_fixed_elements()

        assert called, "normalize 미호출"
        assert all("scene_context_loader" in w for w in called), called
        # 각 scene 마다 1회 호출.
        assert len(called) == 2
        # downstream return shape 보존.
        assert isinstance(result, dict)


# ──────────────────────────────────────────────────────────────────────────
# Task 19: t2i_review (Area #6 v1 후 — 1 place: _review_scene_detail @ line 264).
# 옛 두 번째 지점 (_apply_scene_fixes) 은 mutation 폐기로 함수 자체 삭제됨.
# ──────────────────────────────────────────────────────────────────────────


class TestT2iReviewWiring:
    """t2i_review 의 scene_detail iter 지점 (_review_scene_detail items 수집)
    이 _normalize_scene_detail_result 호출. Area #6 v1 (2026-05-18+): mutation
    폐기 + _apply_scene_fixes 함수 삭제로 두 번째 진입 지점은 사라짐. 본 wiring
    test 는 `_review_scene_detail` normalize call 보존만 검증."""

    def test_review_scene_detail_calls_normalize(self, monkeypatch):
        from app.modules.pipeline import t2i_review

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(t2i_review, "_normalize_scene_detail_result", sentinel)
        monkeypatch.setattr(t2i_review, "load_prompt", lambda *a, **k: "system {t2i_context} {char_names}")
        monkeypatch.setattr(t2i_review, "load_schema", lambda *a, **k: {})

        scene_detail_data = {
            "scenes": [
                {"scene_index": 1, "_shot_index": 1, "t2i_variations": []},
                {"scene_index": 2, "_shot_index": 1, "t2i_variations": []},
            ]
        }
        # t2i_variations = [] → items=[], LLM call skipped. normalize 만 호출.
        t2i_review._review_scene_detail(
            scene_detail_data,
            shot_map={},
            staging_map={},
            t2i_context="ctx",
            char_names_str="",
            opik_metadata={},
        )
        assert called, "_review_scene_detail 에서 normalize 미호출"
        assert all("t2i_review" in w for w in called)
        assert len(called) == 2  # scene 2개.


# ──────────────────────────────────────────────────────────────────────────
# Task 20: scene_still_normalizer
# ──────────────────────────────────────────────────────────────────────────


class TestSceneStillNormalizerWiring:
    """SceneStillNormalizer._plan_scene 에서
    _normalize_scene_detail_result 호출."""

    def test_plan_scene_calls_normalize(self, monkeypatch):
        from app.services.checkpoint_sync import scene_still_normalizer
        from app.services.checkpoint_sync._scene_still_contracts import (
            CheckpointBundle,
            EntityMaps,
        )

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(
            scene_still_normalizer, "_normalize_scene_detail_result", sentinel,
        )

        # legacy path (no selected_shots) 가 가장 단순. 하나의 scene 만 normalize.
        bundle = CheckpointBundle(
            sd_completed=True,
            scenes=[
                {
                    "scene_index": 1,
                    "t2i_variations": [],
                    "heading": "",
                    "beat_title": "",
                    "representative_moment": "",
                    "visible_entities": [],
                    "t2i_prompt": "",
                    "scene_type": "normal",
                },
            ],
            shot_info_by_scene={},
            selected_flag_by_scene={},
            shot_director_ve_map={},
            scene_director_ve={},
            scene_director_audio={},
            scene_director_hall={},
        )
        maps = EntityMaps(
            short_to_id={}, short_to_name={}, name_to_id={}, name_to_short={},
        )
        normalizer = scene_still_normalizer.SceneStillNormalizer(maps)
        normalizer.normalize(bundle)

        assert called, "normalize 미호출"
        assert any("scene_still_normalizer" in w for w in called)


# ──────────────────────────────────────────────────────────────────────────
# Task 21: scene_checkpoint_loaders.load_shot_t2i_variations
# ──────────────────────────────────────────────────────────────────────────


class TestSceneCheckpointLoadersWiring:
    """load_shot_t2i_variations 의 두 path (camera_json / fallback) 모두
    _normalize_evidence_fields 호출."""

    def test_camera_json_path_normalizes_each_var(self, monkeypatch, tmp_path):
        from app.services import scene_checkpoint_loaders as scl

        called: List[str] = []

        def sentinel(item: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return item

        monkeypatch.setattr(scl, "_normalize_evidence_fields", sentinel)

        camera_json = json.dumps({
            "t2i_variations": [
                {"t2i_prompt": "a"},
                {"t2i_prompt": "b"},
            ]
        })
        result = scl.load_shot_t2i_variations(
            str(tmp_path), "p", "e",
            camera_json=camera_json,
            scene_index=1, still_index=1, shot_index=1,
        )
        assert len(result) == 2
        # var 마다 1 호출 = 2 호출.
        assert len(called) == 2
        assert all("scene_checkpoint_loaders" in w for w in called)

    def test_scene_detail_fallback_path_normalizes_each_var(self, monkeypatch, tmp_path):
        from app.services import scene_checkpoint_loaders as scl

        called: List[str] = []

        def sentinel(item: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return item

        monkeypatch.setattr(scl, "_normalize_evidence_fields", sentinel)

        # scene_detail cp file 생성.
        ep_dir = tmp_path / "p" / "checkpoints" / "episodes" / "e" / "scene_detail"
        ep_dir.mkdir(parents=True)
        (ep_dir / "manifest.json").write_text(json.dumps({
            "data": {
                "scenes": [
                    {
                        "scene_index": 1,
                        "_shot_index": 1,
                        "t2i_variations": [
                            {"t2i_prompt": "x"},
                        ],
                    }
                ]
            }
        }), encoding="utf-8")

        result = scl.load_shot_t2i_variations(
            str(tmp_path), "p", "e",
            camera_json=None,  # fallback path 강제
            scene_index=1, still_index=1, shot_index=1,
        )
        assert len(result) == 1
        assert called, "fallback path 에서 normalize 미호출"
        assert all("scene_checkpoint_loaders" in w for w in called)


# ──────────────────────────────────────────────────────────────────────────
# Cl-I5: verify_completion sentinel — scene_consistency / scene_detail
# G2.2 sham 회귀 (drift) 차단. plan §7.1a 명시.
# ──────────────────────────────────────────────────────────────────────────


class TestSceneConsistencyVerifyCompletionWiring:
    """SceneConsistencyStep.verify_completion 가
    _normalize_scene_consistency_result 를 호출하는지."""

    def test_verify_completion_calls_normalize(self, monkeypatch):
        from app.core.steps import scene_consistency_step as scs

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(scs, "_normalize_scene_consistency_result", sentinel)

        step = scs.SceneConsistencyStep.__new__(scs.SceneConsistencyStep)
        step._last_execute_result = {
            "data": {
                "scenes": [
                    {"scene_index": 1, "fixed_elements": []},
                    {"scene_index": 2, "fixed_elements": []},
                ]
            }
        }
        step.verify_completion()

        assert called, "verify_completion 에서 normalize 미호출"
        assert all("scene_consistency.verify_completion" in w for w in called)
        assert len(called) == 2  # 두 scene.


class TestSceneDetailVerifyCompletionWiring:
    """SceneDetailStep.verify_completion 가
    _normalize_scene_detail_result 를 호출하는지."""

    def test_verify_completion_calls_normalize(self, monkeypatch, tmp_path):
        from app.core.steps import detail_steps as ds

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(ds, "_normalize_scene_detail_result", sentinel)
        # Wave 6 cascade: FIX 1 이 blanket except 제거 → loader 호출 시
        # project_id 가 필요. bg-off path 도 안전한 {} 반환하지만 cp 경로 조회용
        # project_id/episode_id 가 attribute 로 존재해야 함.
        monkeypatch.setattr(
            "app.core.config.settings.projects_dir", str(tmp_path), raising=False,
        )
        monkeypatch.setattr(
            "app.core.config.settings.background_mode", "off", raising=False,
        )

        step = ds.SceneDetailStep.__new__(ds.SceneDetailStep)
        step.project_id = "P_test"
        step.episode_id = "E_test"
        # verify_completion 은 _last_execute_result 가 있으면 cp 무시.
        step._last_execute_result = {
            "data": {
                "scenes": [
                    {"scene_index": 1, "_shot_index": 1, "t2i_variations": [{"t2i_prompt": "a"}]},
                    {"scene_index": 2, "_shot_index": 1, "t2i_variations": [{"t2i_prompt": "b"}]},
                ]
            }
        }
        step.verify_completion()

        assert called, "verify_completion 에서 normalize 미호출"
        assert all("scene_detail.verify_completion" in w for w in called)
        assert len(called) == 2  # 두 scene.
