"""W22 W4b 야외 스택 우회·plate skip 결정론 테스트."""

import json

from unittest.mock import MagicMock, patch

from app.core.applicability import (
    _if_outdoor_direct_compose,
    _if_outdoor_site_layout_enabled,
)
from app.core.steps.background_render_step import (
    BackgroundRenderStep,
    _outdoor_loc_ids_from_classify,
    _split_outdoor_direct_skip,
)


def _classify_cp():
    return {"data": {"building_groups": [
        {"group_id": "g1", "members": [
            {"loc_id": "L01", "is_indoor": False},
            {"loc_id": "L02", "is_indoor": True},
        ]},
        {"group_id": "g2", "members": [
            {"loc_id": "L05", "is_indoor": False},
        ]},
    ]}}


# ── ① 야외 loc 추출 (plate skip 조인 소스) ──────────────────────────


def test_outdoor_loc_ids_from_classify():
    assert _outdoor_loc_ids_from_classify(_classify_cp()) == {"L01", "L05"}


def test_outdoor_loc_ids_conservative_on_missing():
    assert _outdoor_loc_ids_from_classify(None) == set()
    assert _outdoor_loc_ids_from_classify({"data": {}}) == set()


# ── ① plate skip: 혼합/전량 + D6 hash 보존 (Codex NARROW_2) ─────────


def test_split_skips_outdoor_keeps_indoor():
    """혼합 케이스 — 실외 bg 만 렌더 대상에서 제외, 실내 bg 유지."""
    bg_specs = {
        "L01B01": {"loc_id": "L01", "sub_location": "야외 마당",
                   "applies_to_shots": ["S3_Shot1"]},
        "L02B01": {"loc_id": "L02", "sub_location": "실내 거실",
                   "applies_to_shots": ["S4_Shot1"]},
    }
    renderable, skipped = _split_outdoor_direct_skip(
        {"L01B01", "L02B01"}, bg_specs, {"L01", "L05"},
    )
    assert renderable == {"L02B01"}
    assert set(skipped) == {"L01B01"}
    entry = skipped["L01B01"]
    assert entry["status"] == "skipped_outdoor_direct"
    assert entry["location_id"] == "L01"
    assert entry["shot_ids"] == ["S3_Shot1"]


def _render_step():
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    step.project_id = "SAMPLE_P"
    step.episode_id = "SAMPLE_E"
    step.project_config = {}
    step.db = MagicMock()
    return step


def _all_outdoor_cps():
    return {
        "background_master_plan": {"data": {
            "plans": {"g1": {"status": "ok", "plan": {"backgrounds": [
                {"bg_id": "L01B01", "loc_id": "L01",
                 "sub_location": "야외 마당", "depends_on_bg": [],
                 "applies_to_shots": ["S3_Shot1"]},
            ]}}},
            "bg_catalog_hash": "HASH-CAT",
            "shot_binding_hash": "HASH-BIND",
        }},
        "background_prompt": {"data": {"backgrounds": {
            "L01B01": {"status": "ok", "t2i_prompt": "SAMPLE bg prompt"},
        }}},
        "background_classify": _classify_cp(),
    }


def test_execute_all_outdoor_skips_and_preserves_d6_hashes():
    """전량 실외 케이스 — renderable 0 조기 경로에서도 skip 마커 보존 +
    consumed_bg_catalog_hash/shot_binding_hash 그대로 (D6 불변식,
    all-skipped 도 completed=1 empty-path 관례)."""
    step = _render_step()
    cps = _all_outdoor_cps()
    step._load_prev_checkpoint = lambda sid: cps.get(sid)

    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.outdoor_direct_compose_enabled",
               True, create=True):
        result = step._execute()

    assert result["completed_count"] == 1 and result["failed_count"] == 0
    data = result["data"]
    # D6 불변식: 카탈로그 hash 스탬프 무손상 → StaleUpstreamError 0
    assert data["consumed_bg_catalog_hash"] == "HASH-CAT"
    assert data["consumed_shot_binding_hash"] == "HASH-BIND"
    # skip 마커 보존 (가시성) — 렌더 산출 없음
    assert data["groups"]["L01B01"]["status"] == "skipped_outdoor_direct"
    assert "png_path" not in data["groups"]["L01B01"]


def test_execute_flag_off_no_skip_markers():
    """OFF — skip 분기 미발화: 야외 bg 도 렌더 대상 유지(마커 0).

    ※classify cp 자체는 aerial/W-J 등 기존 경로가 flag 무관 로드할 수 있음 —
    검증 대상은 skip 분기의 발화 여부다."""
    step = _render_step()
    cps = _all_outdoor_cps()
    step._load_prev_checkpoint = lambda sid: cps.get(sid)
    step._register_image_assets = MagicMock()
    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.outdoor_direct_compose_enabled",
               False, create=True), \
         patch("app.core.steps.background_render_step._resolve_openai_client",
               MagicMock()), \
         patch("app.modules.pipeline.background_render.render_one_background",
               MagicMock(return_value={"status": "failed", "error": "stub"})):
        result = step._execute()
    statuses = {
        entry.get("status")
        for entry in result["data"]["groups"].values()
    }
    assert "skipped_outdoor_direct" not in statuses
    # 야외 bg 가 렌더 시도 대상이었음(stub failed 로 기록) = skip 미발화 증거
    assert result["data"]["groups"].get("L01B01") is not None


def test_skipped_status_not_ingested_by_bg_map(tmp_path):
    """하류 loader — skipped_outdoor_direct 는 scene ref 로 안 들어감."""
    from app.services.scene_checkpoint_loaders import (
        load_background_chain_bg_map,
    )
    png = tmp_path / "ok.png"
    png.write_bytes(b"PNG")
    cp_dir = (tmp_path / "SAMPLE_P" / "checkpoints" / "episodes"
              / "SAMPLE_E" / "background_render")
    cp_dir.mkdir(parents=True)
    (cp_dir / "manifest.json").write_text(json.dumps({"data": {"groups": {
        "L01B01": {"status": "skipped_outdoor_direct",
                   "shot_ids": ["S3_Shot1"], "location_id": "L01"},
        "L02B01": {"status": "ok", "png_path": str(png),
                   "shot_ids": ["S4_Shot1"], "location_id": "L02"},
    }}}), encoding="utf-8")
    with patch("app.core.config.settings.background_chain_enabled",
               True, create=True):
        bg_map = load_background_chain_bg_map(
            str(tmp_path), "SAMPLE_P", "SAMPLE_E",
        )
    assert "3_1" not in bg_map   # skipped 는 미주입
    assert "4_1" in bg_map       # ok 는 기존대로


# ── ② site_layout applicability 우회 ────────────────────────────────


def _runner():
    r = MagicMock()
    return r


def test_site_layout_applicable_when_direct_off():
    with patch("app.core.config.settings.outdoor_site_layout_enabled",
               True, create=True), \
         patch("app.core.config.settings.outdoor_direct_compose_enabled",
               False, create=True):
        assert _if_outdoor_site_layout_enabled(_runner()) is True


def test_site_layout_bypassed_when_direct_on():
    """직행 ON → site_layout not_applicable (설계 §4 우회)."""
    with patch("app.core.config.settings.outdoor_site_layout_enabled",
               True, create=True), \
         patch("app.core.config.settings.outdoor_direct_compose_enabled",
               True, create=True):
        assert _if_outdoor_site_layout_enabled(_runner()) is False


def test_direct_compose_resolver():
    with patch("app.core.config.settings.outdoor_direct_compose_enabled",
               True, create=True):
        assert _if_outdoor_direct_compose(_runner()) is True
    with patch("app.core.config.settings.outdoor_direct_compose_enabled",
               False, create=True):
        assert _if_outdoor_direct_compose(_runner()) is False


# ── ③ aerial 컨텍스트 우회 ──────────────────────────────────────────


def _aerial_step():
    """classify cp 를 실제로 반환하는 인스턴스 — 내부 예외→{} 경로가 아니라
    direct guard 자체를 검증 (Codex NARROW_1 false-positive 정정)."""
    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    step._load_prev_checkpoint = lambda sid: (
        _classify_cp() if sid == "background_classify" else None
    )
    return step


def test_aerial_context_empty_when_direct_on():
    """직행 ON → aerial flag ON + classify cp 실재여도 context {}."""
    with patch("app.core.config.settings.outdoor_direct_compose_enabled",
               True, create=True), \
         patch("app.core.config.settings.outdoor_aerial_reference_enabled",
               True, create=True):
        assert _aerial_step()._load_aerial_loc_context() == {}


def test_aerial_context_nonempty_when_direct_off():
    """보조 검증 — 같은 fixture 에서 direct OFF + aerial ON 이면 non-empty
    (guard 가 아니라 fixture 문제로 {} 가 되는 false-positive 차단)."""
    with patch("app.core.config.settings.outdoor_direct_compose_enabled",
               False, create=True), \
         patch("app.core.config.settings.outdoor_aerial_reference_enabled",
               True, create=True):
        ctx = _aerial_step()._load_aerial_loc_context()
    assert ctx, "fixture 기준 aerial 컨텍스트가 비면 테스트 자체가 무효"
    assert any(c.get("is_indoor") is False for c in ctx.values())
