"""D6 T7 — bg_catalog_hash vs shot_binding_hash 두 축 독립.

축 1 (catalog): render-relevant field 변경 → catalog_hash drift.
축 2 (binding): applies_to_shots 변경만 → binding_hash drift, catalog_hash 안정.

T4 의 atom-level isolation (`tests/core/test_bg_catalog.py:373`) 위에 step
composition (`_d6_post_process` 의 4-tuple 결합) 회귀 차단. preflight (T8) 가
catalog/binding 따로 STALE_UPSTREAM 잡으려면 두 축이 step 레벨에서도 독립이어야 함.

unit-style pivot (T6/T7-determinism 와 같은 결정).

spec: docs/superpowers/specs/2026-05-09-deterministic-bg-id-and-catalog-lineage.md §4.6
plan: T7 (unit-style pivot)
"""
from __future__ import annotations

from typing import Any, Dict, List

import pytest


_PROFILES = {"L09": {"kind": "single_space", "allowed_space_keys": ["main"]}}


def _make_step(prev_catalog: Dict[str, Dict[str, Any]] = None):
    from app.core.steps.background_master_plan_step import BackgroundMasterPlanStep
    step = BackgroundMasterPlanStep.__new__(BackgroundMasterPlanStep)
    step.project_id = "p-d6-t7-iso"
    step.episode_id = "ep1"
    step.step_id = "background_master_plan"
    step.project_config = {}
    step._load_location_profiles = lambda: dict(_PROFILES)
    step._load_prev_background_catalog = lambda: dict(prev_catalog or {})
    return step


def _plans(intents: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
    return {"g1": {"status": "ok", "plan": {
        "floor_plans": [], "backgrounds": [dict(i) for i in intents],
    }}}


def _intent(**kw):
    base = {
        "loc_id": "L09", "space_key_hint": "main", "time_phase": "dusk",
        "state_class": "busy_exit", "applies_to_shots": ["S8_Shot4"],
        "sub_location_label": "store sales floor", "state_label_raw": "raw",
        "depends_on_fp": ["fp_supermarket"], "depends_on_bg": [],
        # W21B-w2 에서 semantic key 의 필수 필드가 됐다(bg_catalog fail-fast).
        "surface_role": "interior_main",
    }
    base.update(kw)
    return base


def test_applies_to_shots_change_only_drifts_binding_hash():
    """축 2 — applies_to_shots 변경만 → binding_hash drift, catalog_hash 안정."""
    a = _intent(applies_to_shots=["S8_Shot4"])
    b = _intent(applies_to_shots=["S8_Shot4", "S8_Shot5"])

    step_a = _make_step()
    _, _, ch_a, bh_a = step_a._d6_post_process(_plans([a]))

    step_b = _make_step()
    _, _, ch_b, bh_b = step_b._d6_post_process(_plans([b]))

    assert ch_a == ch_b, (
        f"catalog hash 가 applies_to_shots 변경에 흔들림 — 축 분리 깨짐: "
        f"{ch_a!r} vs {ch_b!r}"
    )
    assert bh_a != bh_b, (
        "binding hash 가 shot 추가에 안 변함 — 축 분리 깨짐 (T8 preflight 가 "
        "binding-only stale 못 잡게 됨)."
    )


def test_state_class_change_drifts_catalog_hash():
    """축 1 — state_class (render-relevant) 변경 시 catalog_hash drift."""
    a = _intent(state_class="busy_exit")
    b = _intent(state_class="ransacked")

    step_a = _make_step()
    _, _, ch_a, _ = step_a._d6_post_process(_plans([a]))

    step_b = _make_step()
    _, _, ch_b, _ = step_b._d6_post_process(_plans([b]))

    assert ch_a != ch_b, (
        f"state_class 변경이 catalog hash 에 안 잡힘 — render-relevant field "
        f"isolation 깨짐: {ch_a!r} vs {ch_b!r}"
    )


def test_depends_on_fp_change_fail_fasts_on_same_sem_key():
    """축 1 — 같은 sem_key 에서 depends_on_fp 가 다르면 fail-fast.

    T4-fix B3 의 silent first-wins 차단: 같은 sem_key 의 두 intent 가 다른
    render-relevant deps 를 갖는 건 SOT 결손 → SemanticKeyError. catalog hash
    가 drift 든 안 하든 fail-fast 가 우선 — 무결성 SOT.
    """
    from app.core.bg_catalog import SemanticKeyError

    a = _intent(depends_on_fp=["fp_supermarket"])
    b = _intent(depends_on_fp=["fp_warehouse"])  # 같은 sem_key, 다른 deps

    step = _make_step()
    with pytest.raises(SemanticKeyError):
        step._d6_post_process(_plans([a, b]))
