"""W19B-2: BackgroundPromptStep selector / config_hash / overlay fail-closed.

production-side, focused. LLM / image / VLM API call 0, DB / ImageAsset 0.
기존 ``tests/core/test_background_prompt_step.py`` 패턴 재사용 (StepRunner.__new__
+ patch).
"""
from __future__ import annotations

import hashlib
import json as _json
from unittest.mock import MagicMock, patch

from app.core.steps.background_prompt_step import (
    PROMPT_VERSION,
    SCHEMA_VERSION,
    BackgroundPromptStep,
    _resolved_prompt_version,
)
from app.modules.pipeline.background_prompt import PROMPT_VERSION_MAP

V6_PACK = PROMPT_VERSION_MAP["6"]  # "6.202605091200"
V7_PACK = PROMPT_VERSION_MAP["7"]  # "7.202605262330"


def _expected_hash(*, background_mode: str, prompt_version: str) -> str:
    payload = {
        "background_mode": background_mode,
        "schema_version": SCHEMA_VERSION,
        "prompt_version": prompt_version,
    }
    return hashlib.sha256(
        _json.dumps(payload, sort_keys=True).encode("utf-8")
    ).hexdigest()[:16]


def _new_step() -> BackgroundPromptStep:
    return BackgroundPromptStep.__new__(BackgroundPromptStep)


def test_default_v6_hash_matches_legacy_prompt_version():
    """default selector "6" 의 _config_hash 는 기존 PROMPT_VERSION constant 기반
    hash 와 byte-identical. v6 caller checkpoint 보존 contract.
    """
    assert PROMPT_VERSION == V6_PACK
    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.background_prompt_version", "6"):
        got = _new_step()._config_hash()
        assert _resolved_prompt_version() == V6_PACK
    assert got == _expected_hash(background_mode="on", prompt_version=V6_PACK)


def test_v7_selector_hash_differs_from_v6_default():
    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.background_prompt_version", "6"):
        h6 = _new_step()._config_hash()
    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.background_prompt_version", "7"):
        h7 = _new_step()._config_hash()
        assert _resolved_prompt_version() == V7_PACK
    assert h6 != h7
    assert h7 == _expected_hash(background_mode="on", prompt_version=V7_PACK)


def test_v7_selector_missing_overlay_cp_marks_bg_failed_without_llm_call():
    """selector "7" + floor_plan_overlay_payload cp 없음 → bg 가 fail-closed.

    build_bg_user_prompt 가 BackgroundPromptOverlayError 를 raise → step 이 흡수해
    bg-level failed 로 기록. run_background_prompt (LLM 진입) 는 호출되지 않음.
    """
    plans_data = {
        "data": {
            "plans": {
                "g_a": {
                    "status": "ok",
                    "plan": {
                        "group_id": "g_a",
                        "backgrounds": [
                            {
                                "bg_id": "L01B01",
                                "loc_id": "L01",
                                "sub_location": "primary_unit",
                                "state_label_raw": "day_norm",
                                "depends_on_fp": ["fp_a"],
                                "depends_on_bg": [],
                                "applies_to_shots": ["S1_Shot1"],
                            }
                        ],
                    },
                }
            }
        }
    }
    # floor_plan_render cp + floor_plan_prompt cp 도 정상 모양으로 — overlay cp만 누락.
    fp_render_cp = {
        "data": {
            "floor_plans": {
                "fp_a": {"status": "ok", "png_path": "/p/fp.png"}
            }
        }
    }
    fp_prompt_cp = {"data": {"floor_plans": {"fp_a": {"numbered_elements": [], "camera_recommendations": []}}}}
    cp_map = {
        "background_master_plan": plans_data,
        "floor_plan_render": fp_render_cp,
        "floor_plan_prompt": fp_prompt_cp,
        "floor_plan_overlay_payload": None,  # missing — fail-closed path.
        "scene_save": {"data": {"segments": [{"scene_index": 1, "heading": "h", "text": "t"}]}},
        "shot_validator": {"data": {"scenes": [{"scene_index": 1, "shots": [{"shot_index": 1, "description": "d", "location_id": "L01"}]}]}},
        "shot_selection": {"data": {"scenes": [{"scene_index": 1, "selected_shot_indices": [1]}]}},
        "visual_world_rules": {"data": {"rules_text": ""}},
        "world_guide": None,
    }

    with patch("app.core.config.settings.background_mode", "on"), \
         patch("app.core.config.settings.background_prompt_version", "7"), \
         patch(
             "app.core.steps.background_prompt_step.run_background_prompt"
         ) as mock_run:
        step = _new_step()
        step.project_id = "p"
        step.episode_id = "e"
        step.project_config = {}
        step.build_opik_metadata = MagicMock(return_value={})
        step._load_prev_checkpoint = MagicMock(
            side_effect=lambda sid: cp_map.get(sid)
        )
        result = step._execute()
        # overlay 누락으로 LLM 진입 전 fail-closed — run_background_prompt 미호출.
        mock_run.assert_not_called()

    bg_entry = result["data"]["backgrounds"]["L01B01"]
    assert bg_entry["status"] == "failed"
    assert "overlay" in bg_entry["error"].lower() or "received None" in bg_entry["error"]
    assert result["failed_count"] == 1
