"""Phase 9.1 — chain_bg camera_meta consumer + framing-aware ref skip.

테스트 대상:
  - SceneContextLoader._load_chain_bg_camera_meta_by_shot
  - _build_phase2_prepend_blocks chain_bg_camera_meta 블록
  - scene_generation_coordinator close framing 정규식
"""
from __future__ import annotations

from typing import Any, Dict


# ---------------------------------------------------------------------------
# Loader: data.groups[bg_id].camera_recommendations → (si, shi) → meta
# ---------------------------------------------------------------------------


def test_loader_camera_meta_returns_empty_when_no_checkpoint():
    """Phase 7/5 background_render 체크포인트 둘 다 없으면 빈 dict."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return None

    loader = SceneContextLoader(FakeRunner())
    assert loader._load_chain_bg_camera_meta_by_shot() == {}


def test_loader_camera_meta_parses_phase7_groups_shape():
    """data.groups[bg_id].camera_recommendations + shot_ids → 4 필드 dict."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "bg_living_day": {
                    "status": "ok",
                    "shot_ids": ["S5_Shot1", "S5_Shot3"],
                    "camera_recommendations": {
                        "camera_position": "doorway 1.6m southeast",
                        "camera_height": "eye-level standing 1.6m",
                        "lens_hint": "35mm wide angle",
                        "framing_notes": "doorway foreground, table mid-ground",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_render" else None

    loader = SceneContextLoader(FakeRunner())
    result = loader._load_chain_bg_camera_meta_by_shot()
    assert (5, 1) in result
    assert (5, 3) in result
    meta = result[(5, 1)]
    assert meta["camera_position"] == "doorway 1.6m southeast"
    assert meta["camera_height"] == "eye-level standing 1.6m"
    assert meta["lens_hint"] == "35mm wide angle"
    assert meta["framing_notes"] == "doorway foreground, table mid-ground"
    # 두 shot에 같은 meta가 매핑되어야 함 (group 단위 cr)
    assert result[(5, 1)] == result[(5, 3)]


def test_loader_camera_meta_skips_non_ok_groups():
    """status != 'ok' group은 무시."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "bg_failed": {
                    "status": "failed",
                    "shot_ids": ["S5_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "ignored",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
                "bg_ok": {
                    "status": "ok",
                    "shot_ids": ["S6_Shot2"],
                    "camera_recommendations": {
                        "camera_position": "kept",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_render" else None

    loader = SceneContextLoader(FakeRunner())
    result = loader._load_chain_bg_camera_meta_by_shot()
    assert (5, 1) not in result
    assert (6, 2) in result
    assert result[(6, 2)]["camera_position"] == "kept"


def test_loader_camera_meta_skips_when_all_fields_empty():
    """4 필드 모두 빈 문자열이면 매핑 의미 없음 → skip."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "bg_empty": {
                    "status": "ok",
                    "shot_ids": ["S5_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_render" else None

    loader = SceneContextLoader(FakeRunner())
    result = loader._load_chain_bg_camera_meta_by_shot()
    assert result == {}


def test_loader_camera_meta_phase5_fallback():
    """Phase 7 cp 없고 Phase 5 cp만 있을 때도 동일하게 동작."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "cb_living_day": {
                    "status": "ok",
                    "shot_ids": ["S7_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "from_phase5",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_chain_render" else None

    loader = SceneContextLoader(FakeRunner())
    result = loader._load_chain_bg_camera_meta_by_shot()
    assert (7, 1) in result
    assert result[(7, 1)]["camera_position"] == "from_phase5"


def test_loader_camera_meta_invalid_shot_id_format_skipped(caplog):
    """shot_id 'S{int}_Shot{int}' 미준수 → skip + warning."""
    import logging
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "bg_bad": {
                    "status": "ok",
                    "shot_ids": ["BAD_FORMAT", "S5_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "x",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_render" else None

    loader = SceneContextLoader(FakeRunner())
    with caplog.at_level(logging.WARNING):
        result = loader._load_chain_bg_camera_meta_by_shot()
    assert (5, 1) in result
    # BAD_FORMAT은 skip
    assert len(result) == 1
    # MINOR M1 (Codex): warning contract — invalid shot id 메시지에 BAD_FORMAT 포함
    assert any("BAD_FORMAT" in rec.message for rec in caplog.records), \
        "expected warning containing 'BAD_FORMAT', got: " + repr([r.message for r in caplog.records])


# ---------------------------------------------------------------------------
# DTO field
# ---------------------------------------------------------------------------


def test_scene_analysis_context_has_phase9_1_field():
    """Phase 9.1 chain_bg_camera_meta_by_shot 필드 존재 확인."""
    from app.core.dto.scene_analysis import SceneAnalysisContext
    ctx = SceneAnalysisContext()
    assert hasattr(ctx, "chain_bg_camera_meta_by_shot")
    assert ctx.chain_bg_camera_meta_by_shot == {}


# ---------------------------------------------------------------------------
# _build_phase2_prepend_blocks chain_bg_camera_meta 블록
# ---------------------------------------------------------------------------


def test_build_prepend_blocks_camera_meta_off_default():
    """chain_bg_camera_meta_enabled 인자 없이 호출 — 기본 off, meta 블록 없음 (회귀 가드)."""
    from app.core.steps.detail_steps import _build_phase2_prepend_blocks
    out = _build_phase2_prepend_blocks(
        si=5, shi=1,
        essence_by_shot={},
        chain_bg_guide_by_shot={},
        shot_essence_enabled=False,
        chain_bg_guide_enabled=False,
    )
    # meta 인자 안 줘도 정상 동작 (default off)
    assert out == ""


def test_build_prepend_blocks_camera_meta_block_appended():
    """camera_meta on + 데이터 있음 → meta 블록 포함."""
    from app.core.steps.detail_steps import _build_phase2_prepend_blocks
    out = _build_phase2_prepend_blocks(
        si=5, shi=1,
        essence_by_shot={},
        chain_bg_guide_by_shot={},
        chain_bg_camera_meta_by_shot={
            (5, 1): {
                "camera_position": "doorway 1.6m southeast",
                "camera_height": "eye-level standing",
                "lens_hint": "35mm wide angle",
                "framing_notes": "doorway foreground, table mid-ground",
            },
        },
        shot_essence_enabled=False,
        chain_bg_guide_enabled=False,
        chain_bg_camera_meta_enabled=True,
    )
    assert "[chain_bg reference 카메라 정보" in out
    assert "doorway 1.6m southeast" in out
    assert "35mm wide angle" in out
    assert "framing notes" in out


def test_build_prepend_blocks_camera_meta_empty_meta_no_block():
    """camera_meta on이지만 (5, 1) entry 없음 → 블록 없음."""
    from app.core.steps.detail_steps import _build_phase2_prepend_blocks
    out = _build_phase2_prepend_blocks(
        si=5, shi=1,
        essence_by_shot={},
        chain_bg_guide_by_shot={},
        chain_bg_camera_meta_by_shot={},
        shot_essence_enabled=False,
        chain_bg_guide_enabled=False,
        chain_bg_camera_meta_enabled=True,
    )
    assert out == ""


def test_build_prepend_blocks_camera_meta_partial_fields():
    """meta dict의 일부 필드만 채워져도 그 필드만 표시."""
    from app.core.steps.detail_steps import _build_phase2_prepend_blocks
    out = _build_phase2_prepend_blocks(
        si=5, shi=1,
        essence_by_shot={},
        chain_bg_guide_by_shot={},
        chain_bg_camera_meta_by_shot={
            (5, 1): {
                "camera_position": "doorway",
                "camera_height": "",
                "lens_hint": "",
                "framing_notes": "",
            },
        },
        shot_essence_enabled=False,
        chain_bg_guide_enabled=False,
        chain_bg_camera_meta_enabled=True,
    )
    assert "doorway" in out
    # 빈 필드는 라인에 추가되면 안 됨
    assert "height:" not in out
    assert "lens:" not in out
    assert "framing notes:" not in out


def test_build_prepend_blocks_all_three_blocks_order():
    """essence + chain_bg_guide + camera_meta 모두 on → 순서 essence → guide → meta."""
    from app.core.steps.detail_steps import _build_phase2_prepend_blocks
    out = _build_phase2_prepend_blocks(
        si=5, shi=1,
        essence_by_shot={(5, 1): ["e1"]},
        chain_bg_guide_by_shot={(5, 1): "g1"},
        chain_bg_camera_meta_by_shot={
            (5, 1): {
                "camera_position": "p1",
                "camera_height": "",
                "lens_hint": "",
                "framing_notes": "",
            },
        },
        shot_essence_enabled=True,
        chain_bg_guide_enabled=True,
        chain_bg_camera_meta_enabled=True,
    )
    e_idx = out.find("[샷 핵심")
    g_idx = out.find("[chain_bg reference에 이미 있음")
    c_idx = out.find("[chain_bg reference 카메라 정보")
    assert 0 <= e_idx < g_idx < c_idx


# ---------------------------------------------------------------------------
# IMPORTANT (Claude I1): duplicate-shot first-wins 명시 contract
# ---------------------------------------------------------------------------


def test_loader_camera_meta_duplicate_shot_first_wins():
    """같은 shot이 여러 group에 등장하면 group iteration order 첫 번째 보존."""
    from app.core.steps.scene_context_loader import SceneContextLoader

    cp_data = {
        "data": {
            "groups": {
                "bg_first": {
                    "status": "ok",
                    "shot_ids": ["S5_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "first",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
                "bg_second": {
                    "status": "ok",
                    "shot_ids": ["S5_Shot1"],
                    "camera_recommendations": {
                        "camera_position": "second",
                        "camera_height": "",
                        "lens_hint": "",
                        "framing_notes": "",
                    },
                },
            },
        },
    }

    class FakeRunner:
        def _load_prev_checkpoint(self, step_id):
            return cp_data if step_id == "background_render" else None

    loader = SceneContextLoader(FakeRunner())
    result = loader._load_chain_bg_camera_meta_by_shot()
    assert (5, 1) in result
    # 첫 번째 group이 보존됨 (Python 3.7+ dict iteration = insertion order)
    assert result[(5, 1)]["camera_position"] == "first"


# ---------------------------------------------------------------------------
# IMPORTANT (Codex): scene_generation_coordinator framing skip 통합 동작
# framing_scale enum SOT v1 (2026-05-15): regex 폐기 후 staging.framing_scale
# enum helper read 기반 chain_bg ref 분기 검증.
# ---------------------------------------------------------------------------


def test_coordinator_skip_chain_bg_when_close_framing():
    """staging.framing_scale=close → chain_bg ref가 labeled_refs에 들어가지 않음."""
    from app.core.framing_scale import FRAMING_CLOSE, get_framing_scale_or_raise

    chain_bg_map = {"5_1": {"label": "bg_room_day", "image_bytes": b"PNG"}}
    staging_close = {"framing_scale": "close"}
    staging_medium = {"framing_scale": "medium"}

    def _decide(staging, key):
        bg = chain_bg_map.get(key)
        framing = get_framing_scale_or_raise(staging, where=f"test {key}")
        is_close = framing == FRAMING_CLOSE
        return bg if (bg and not is_close) else None

    # close → chain_bg ref skip
    assert _decide(staging_close, "5_1") is None
    # medium → chain_bg ref 사용
    assert _decide(staging_medium, "5_1") is not None


def test_coordinator_no_chain_bg_skip_when_medium_framing():
    """staging.framing_scale=medium → close detection skip → 기존 동작 유지."""
    from app.core.framing_scale import FRAMING_CLOSE, get_framing_scale_or_raise

    staging_medium = {"framing_scale": "medium"}
    framing = get_framing_scale_or_raise(staging_medium, where="test 5_1")
    is_close = framing == FRAMING_CLOSE
    assert is_close is False  # medium → close 아님 → chain_bg 그대로 사용
