"""G3.2 scene_context_loader._load_chain_bg_owned_by_shot tests.

Phase 4 / Task 12 of G3.2 plan. Verifies the consumer-side loader that maps
``background_prompt`` cp ``objects_owned_by_background`` into a per-shot dict.

Round 4 BLOCKING 1: bg-on + bp_cp is None → fail-fast raise (NOT silent {}).
Round 5 IMPORTANT 2: docstring states "bg-on + cp 부재 → AppError fail-fast".
"""
from __future__ import annotations

from typing import Any, Dict

import pytest

from app.core.errors import AppError
from app.core.steps.scene_context_loader import SceneContextLoader


class _FakeRunner:
    project_id = "p"
    episode_id = "e"
    db = None
    project_config = None

    def __init__(self, cps: Dict[str, Any]) -> None:
        self._cps = cps

    def _load_prev_checkpoint(self, step_id: str):
        return self._cps.get(step_id)

    def build_opik_metadata(self):
        return {}


def _make_loader(cps: Dict[str, Any]) -> SceneContextLoader:
    return SceneContextLoader(_FakeRunner(cps))


@pytest.fixture
def fake_settings_bg_on(monkeypatch):
    from app.core.config import settings
    monkeypatch.setattr(settings, "background_mode", "on", raising=False)


@pytest.fixture
def fake_settings_bg_off(monkeypatch):
    from app.core.config import settings
    monkeypatch.setattr(settings, "background_mode", "off", raising=False)


class TestLoadChainBgOwnedByShot:
    def test_bg_off_returns_empty(self, fake_settings_bg_off) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {"bg1": {"status": "ok",
                "objects_owned_by_background": ["door"]}}},
        }})
        assert loader._load_chain_bg_owned_by_shot() == {}

    def test_bg_off_no_cp_returns_empty(self, fake_settings_bg_off) -> None:
        # bg off + cp 없음 → 통과 ({} 리턴, no raise).
        loader = _make_loader({})
        assert loader._load_chain_bg_owned_by_shot() == {}

    def test_bg_on_no_cp_raises(self, fake_settings_bg_on) -> None:
        # round 4 BLOCKING 1: bg-on AND cp 부재 = silent {} 차단 → fail-fast.
        loader = _make_loader({})
        with pytest.raises(AppError) as exc_info:
            loader._load_chain_bg_owned_by_shot()
        assert exc_info.value.code == "step.contract_violation"

    def test_bg_on_old_v4_cp_raises(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 1,
            "data": {"backgrounds": {"bg1": {"status": "ok"}}},
        }})
        with pytest.raises(AppError) as exc_info:
            loader._load_chain_bg_owned_by_shot()
        assert exc_info.value.code == "step.contract_violation"

    def test_bg_on_partial_owned_raises(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {"status": "ok", "objects_owned_by_background": ["door"]},
                "bg2": {"status": "ok"},
            }},
        }})
        with pytest.raises(AppError):
            loader._load_chain_bg_owned_by_shot()

    def test_bg_on_v5_cp_maps_via_applies_to_shots(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["window", "door"],
                    "spec": {"applies_to_shots": ["S1_Shot1", "S1_Shot2"]},
                },
            }},
        }})
        result = loader._load_chain_bg_owned_by_shot()
        assert result == {(1, 1): ["door", "window"], (1, 2): ["door", "window"]}

    def test_bg_on_v5_cp_falls_back_to_shot_guides(self, fake_settings_bg_on) -> None:
        # spec.applies_to_shots 부재 → shot_guides[].shot_id fallback (round 3 minor).
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["TV"],
                    "shot_guides": [{"shot_id": "S2_Shot3", "guide_text": "g"}],
                },
            }},
        }})
        result = loader._load_chain_bg_owned_by_shot()
        assert result == {(2, 3): ["TV"]}

    def test_skipped_failed_status(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg_ok": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
                "bg_failed": {"status": "failed"},
            }},
        }})
        result = loader._load_chain_bg_owned_by_shot()
        assert result == {(1, 1): ["door"]}

    def test_multi_bg_same_shot_union_sorted(self, fake_settings_bg_on) -> None:
        # 두 bg 가 같은 shot 에 적용 → owned 합집합, sorted.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
                "bg2": {
                    "status": "ok",
                    "objects_owned_by_background": ["window", "TV"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
            }},
        }})
        result = loader._load_chain_bg_owned_by_shot()
        assert result == {(1, 1): ["TV", "door", "window"]}

    def test_invalid_shot_id_format_skipped(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["bad_id", "S1_Shot1"]},
                },
            }},
        }})
        result = loader._load_chain_bg_owned_by_shot()
        assert result == {(1, 1): ["door"]}


class TestLoadChainBgIdByShot:
    """N3 cleanup (G4.1 follow-up): `_load_chain_bg_id_by_shot` multi-bg
    deterministic alpha-sort 회귀 차단.

    docstring (`scene_context_loader.py::_load_chain_bg_id_by_shot`)에
    "Multi-bg 가 같은 shot 에 적용되면 첫 번째 ok bid (alpha-sorted —
    deterministic) 만 매핑" invariant 가 명시되어 있으므로 이를 단위 test 로
    고정. dict insertion 순서가 alpha-역순이어도 winner 는 alpha-첫 번째.
    """

    def test_bg_off_returns_empty(self, fake_settings_bg_off) -> None:
        # bg-off 면 bp_cp 가 ok 라도 매핑 X.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
            }},
        }})
        assert loader._load_chain_bg_id_by_shot() == {}

    def test_single_bg_maps_via_applies_to_shots(self, fake_settings_bg_on) -> None:
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
            }},
        }})
        assert loader._load_chain_bg_id_by_shot() == {(1, 1): "bg1"}

    def test_single_bg_falls_back_to_shot_guides(self, fake_settings_bg_on) -> None:
        # spec.applies_to_shots 부재 → shot_guides[].shot_id fallback.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg1": {
                    "status": "ok",
                    "objects_owned_by_background": ["TV"],
                    "shot_guides": [{"shot_id": "S2_Shot3", "guide_text": "g"}],
                },
            }},
        }})
        assert loader._load_chain_bg_id_by_shot() == {(2, 3): "bg1"}

    def test_multi_bg_same_shot_alpha_sorted_first_wins(
        self, fake_settings_bg_on,
    ) -> None:
        # N3 핵심: 두 bg 가 같은 shot 가리킬 때 alpha-sorted bid 첫 번째
        # ('bg_a') 가 매핑 차지. dict insertion 순서는 'bg_b' 먼저여도 sort 가
        # winner 결정 — 다중 bg 매핑의 deterministic invariant.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg_b": {  # insertion 먼저
                    "status": "ok",
                    "objects_owned_by_background": ["window"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
                "bg_a": {  # alpha 첫 번째 — winner
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
            }},
        }})
        result = loader._load_chain_bg_id_by_shot()
        assert result == {(1, 1): "bg_a"}, (
            f"alpha-sorted first bid 'bg_a' expected, got {result!r}"
        )

    def test_multi_bg_distinct_shots_each_maps_own(
        self, fake_settings_bg_on,
    ) -> None:
        # 같은 shot 충돌 없으면 두 bg 모두 자신의 shot 에 매핑.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg_a": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
                "bg_b": {
                    "status": "ok",
                    "objects_owned_by_background": ["window"],
                    "spec": {"applies_to_shots": ["S1_Shot2"]},
                },
            }},
        }})
        result = loader._load_chain_bg_id_by_shot()
        assert result == {(1, 1): "bg_a", (1, 2): "bg_b"}

    def test_failed_bg_skipped(self, fake_settings_bg_on) -> None:
        # status='failed' bid 는 매핑 대상 X — 같은 shot 의 ok bid 가 winner.
        loader = _make_loader({"background_prompt": {
            "schema_version": 2,
            "data": {"backgrounds": {
                "bg_a_failed": {"status": "failed"},
                "bg_b_ok": {
                    "status": "ok",
                    "objects_owned_by_background": ["door"],
                    "spec": {"applies_to_shots": ["S1_Shot1"]},
                },
            }},
        }})
        result = loader._load_chain_bg_id_by_shot()
        assert result == {(1, 1): "bg_b_ok"}
