"""D6 T-pre-1 (T4 atomic 흡수) — `_SAFE_BG_RE` 4 site → marker-gated `_select_bg_id_filter`.

기존 `_SAFE_BG_RE = ^[a-z0-9][a-z0-9_]*$` (4 site) 이 D6 새 `L09B01` 형식 reject.
하지만 단독 commit 으로 BG_ID_RE 강제 시 master_plan 가 여전히 legacy `bg_*` 출력
하던 transitional 상태에서 silent skip → empty completed (silent no-op).

해결: D6 marker-gated filter (`_select_bg_id_filter(plans_cp)`).
- D6 cp (sibling field `background_catalog` / `bg_catalog_hash` 보유) → BG_ID_RE strict.
- legacy cp (no sibling) → `_LEGACY_SAFE_BG_RE` (transitional compat).

spec: docs/superpowers/specs/2026-05-09-deterministic-bg-id-and-catalog-lineage.md
plan: T-pre-1 (R2 B2) + review iter4 marker-gated transition note
"""
from __future__ import annotations

import re


# ──────────────────────────────────────────────────────────────────────
# _LEGACY_SAFE_BG_RE — transitional 보호 (legacy bg_* 통과 보장)
# ──────────────────────────────────────────────────────────────────────


def test_legacy_safe_bg_re_unchanged_pattern():
    """기존 lowercase freeform 정규식 보존 (transitional cp 호환)."""
    from app.core.steps.background_render_step import _LEGACY_SAFE_BG_RE
    assert _LEGACY_SAFE_BG_RE.match("cb_living_day")
    assert _LEGACY_SAFE_BG_RE.match("bg_store_sales_floor")
    assert _LEGACY_SAFE_BG_RE.match("a")
    # freeform path traversal / 대문자 / 공백 등은 여전히 reject
    assert not _LEGACY_SAFE_BG_RE.match("L09B01")  # uppercase: legacy regex reject
    assert not _LEGACY_SAFE_BG_RE.match("../etc")
    assert not _LEGACY_SAFE_BG_RE.match("Cb_Mixed")


# ──────────────────────────────────────────────────────────────────────
# _select_bg_id_filter — marker-gated
# ──────────────────────────────────────────────────────────────────────


def test_filter_uses_legacy_when_no_d6_marker():
    """plans_cp 가 D6 marker 없으면 legacy regex (lowercase freeform 통과)."""
    from app.core.steps.background_render_step import (
        _select_bg_id_filter, _LEGACY_SAFE_BG_RE,
    )
    legacy_cp = {"data": {"plans": {"g1": {"status": "ok"}}}}  # no D6 marker
    rx = _select_bg_id_filter(legacy_cp)
    assert rx is _LEGACY_SAFE_BG_RE
    assert rx.match("cb_living_day")
    assert not rx.match("L09B01")


def test_filter_uses_bg_id_re_when_d6_catalog_marker_present():
    """plans_cp.data.background_catalog 있으면 BG_ID_RE strict."""
    from app.core.steps.background_render_step import _select_bg_id_filter
    from app.core.bg_state_vocab import BG_ID_RE
    d6_cp = {"data": {
        "plans": {"g1": {"status": "ok"}},
        "background_catalog": {"L09B01": {"bg_id": "L09B01"}},
    }}
    rx = _select_bg_id_filter(d6_cp)
    assert rx is BG_ID_RE
    assert rx.match("L09B01")
    assert rx.match("L113B07")
    assert not rx.match("cb_living_day")  # legacy reject in D6 mode


def test_filter_uses_bg_id_re_when_d6_hash_marker_present():
    """plans_cp.data.bg_catalog_hash 만 있어도 D6 marker 로 인정."""
    from app.core.steps.background_render_step import _select_bg_id_filter
    from app.core.bg_state_vocab import BG_ID_RE
    d6_cp = {"data": {
        "plans": {"g1": {"status": "ok"}},
        "bg_catalog_hash": "abc123",
    }}
    rx = _select_bg_id_filter(d6_cp)
    assert rx is BG_ID_RE


def test_filter_handles_none_plans_cp():
    """plans_cp 가 None → legacy fallback (transitional)."""
    from app.core.steps.background_render_step import (
        _select_bg_id_filter, _LEGACY_SAFE_BG_RE,
    )
    assert _select_bg_id_filter(None) is _LEGACY_SAFE_BG_RE
    assert _select_bg_id_filter({}) is _LEGACY_SAFE_BG_RE
    assert _select_bg_id_filter({"data": {}}) is _LEGACY_SAFE_BG_RE


# ──────────────────────────────────────────────────────────────────────
# 4 sites use marker-gated filter (no module-level _SAFE_BG_RE remaining)
# ──────────────────────────────────────────────────────────────────────


def test_no_module_level_legacy_only_filter():
    """`_SAFE_BG_RE` (legacy only) 모듈 attr 가 없어야 — 모든 site 가 marker-gated."""
    from app.core.steps import background_render_step as mod
    assert not hasattr(mod, "_SAFE_BG_RE"), (
        "background_render_step._SAFE_BG_RE 가 여전히 module-level — D6 marker-gated "
        "transition 미완료. `_LEGACY_SAFE_BG_RE` + `_select_bg_id_filter(plans_cp)` 패턴 사용."
    )
    # 새 marker-gated path 의 helpers 는 export 되어 있어야
    assert hasattr(mod, "_LEGACY_SAFE_BG_RE")
    assert hasattr(mod, "_select_bg_id_filter")


# ──────────────────────────────────────────────────────────────────────
# T4-fix B4 — D6 marker 모드 invalid bg_id fail-fast
# ──────────────────────────────────────────────────────────────────────


def test_d6_marker_invalid_bg_id_raises_in_execute(tmp_path, monkeypatch):
    """D6 marker cp 에서 invalid bg_id 등장 시 silent skip 안 함, raise.

    이전: invalid bg_id 면 logger.error + skip → 전부 skip 시 renderable=[] 로
    silent 성공 처리. SOT 결손 가시화 안 됨.
    이제: D6 marker 모드 면 invalid bg_id (BG_ID_RE 미스) → fail-fast raise.
    """
    from unittest.mock import MagicMock
    from app.core.steps.background_render_step import BackgroundRenderStep

    monkeypatch.setattr("app.core.config.settings.background_mode", "on")
    monkeypatch.setattr("app.core.config.settings.projects_dir", str(tmp_path / "p"))

    step = BackgroundRenderStep.__new__(BackgroundRenderStep)
    step.project_id = "proj-d6-fail-fast"
    step.episode_id = "ep1"
    step.db = MagicMock()
    step.project_config = {}

    # D6 marker 보유 cp + invalid bg_id (lowercase legacy 형식)
    d6_plans_cp = {
        "data": {
            "plans": {
                "g1": {
                    "status": "ok",
                    "plan": {
                        "backgrounds": [
                            {"bg_id": "cb_legacy_invalid",  # BG_ID_RE 미스
                             "depends_on_bg": []},
                        ],
                    },
                },
            },
            "background_catalog": {"L09B01": {}},  # D6 marker
            "bg_catalog_hash": "h",
        }
    }

    def _load_cp(name):
        if name == "background_master_plan":
            return d6_plans_cp
        return None

    step._load_prev_checkpoint = _load_cp

    import pytest as _pytest
    with _pytest.raises(Exception, match="(?i)invalid|D6|fail.?fast|cb_legacy_invalid"):
        step._execute()
