"""D6 T4-fix3 회귀 — fp ↔ bg link cross-validation.

배경: D6 R3 시점 floor_plans[] schema 에 covering location 정보 (loc_id, space_key_hint)
부재 → 다른 sub-room 의 fp 가 bg.depends_on_fp 에 silent inject 가능. SemanticKeyError
는 *같은 sem_key 가 다른 fp 두 번* 인 경우만 catch — *다른 sem_key + wrong fp* 는 silent
pass → 잘못된 floor plan 위에 background render.

T4-fix3 (raw validator):
- floor_plans[] 에 loc_id + space_key_hint required 추가.
- bg.depends_on_fp[*] 가 가리키는 fp 의 (loc_id, normalized_space_key) 가
  bg 의 (loc_id, normalized_space_key) 와 일치 의무.
- FpLinkMismatchError(SemanticKeyError) raise → LLM retry path.

본 테스트는 production canary (PID 34dc0431, EID 3453b2ab) 에서 발견된 결함
(L10|main|morning|quiet 가 fp_sales_floor + fp_exterior_entrance 둘 다 참조) 을
회귀 차단. 추가로 sem_key 가드 만으로 못 잡는 cross-sem_key 결함도 차단.
"""
from __future__ import annotations

import pytest


_LOCATION_PROFILES_MART = {
    "L09": {"kind": "single_space", "allowed_space_keys": ["main"]},
    "L10": {"kind": "single_space", "allowed_space_keys": ["main"]},
    "L14": {"kind": "single_space", "allowed_space_keys": ["main"]},
}


def _base_plan_template():
    """공통 base — 마트 group (L09 외부, L10 매장, L14 사무실)."""
    return {
        "group_id": "bg_large_mart",
        "rationale_summary": "L09 외부, L10 매장, L14 사무실 — 별도 fp 3개",
        "floor_plans": [
            {
                "fp_id": "fp_sales_floor",
                "loc_id": "L10",
                "space_key_hint": "main",
                "sub_location": "sales_floor",
                "scope": "Indoor mart sales floor with shelves",
                "depends_on_fp": [],
            },
            {
                "fp_id": "fp_exterior_entrance",
                "loc_id": "L09",
                "space_key_hint": "main",
                "sub_location": "exterior_entrance",
                "scope": "Mart building exterior entrance",
                "depends_on_fp": [],
            },
            {
                "fp_id": "fp_office",
                "loc_id": "L14",
                "space_key_hint": "main",
                "sub_location": "office",
                "scope": "Mart back office and security room",
                "depends_on_fp": [],
            },
        ],
        "backgrounds": [],
    }


def _bg(loc_id, time_phase, state_class, deps_fp, applies=("S08_Shot1",)):
    return {
        "loc_id": loc_id,
        "space_key_hint": "main",
        "time_phase": time_phase,
        "state_class": state_class,
        "surface_role": "interior_room",
        "applies_to_shots": list(applies),
        "sub_location_label": "",
        "state_label_raw": "",
        "depends_on_fp": list(deps_fp),
        "depends_on_bg": [],
    }


# ── Case A — 같은 sem_key + 다른 fp (production canary 결함) ──


def test_fp_link_mismatch_same_sem_key_different_fp():
    """L10|main|morning|quiet 가 fp_sales_floor + fp_exterior_entrance 둘 다 참조.

    같은 raw intent 안 두 bg 가 같은 (loc, space, time, state) 인데 fp 다름. raw
    validator 가 fp link cross-check 시 fp_exterior_entrance 의 (loc=L09, space=main)
    이 bg 의 (L10, main) 와 다름 → FpLinkMismatchError.
    """
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )
    from app.core.bg_catalog import FpLinkMismatchError

    plan = _base_plan_template()
    plan["backgrounds"] = [
        _bg("L10", "morning", "quiet", ["fp_sales_floor"], applies=["S08_Shot1"]),
        _bg("L10", "morning", "quiet", ["fp_exterior_entrance"], applies=["S08_Shot2"]),
    ]
    with pytest.raises(FpLinkMismatchError) as exc:
        validate_master_plan_raw_intent(
            plan,
            expected_group_id="bg_large_mart",
            group_loc_ids={"L09", "L10", "L14"},
            group_shot_ids={"S08_Shot1", "S08_Shot2"},
            location_profiles=_LOCATION_PROFILES_MART,
        )
    msg = str(exc.value)
    assert "L10" in msg and "fp_exterior_entrance" in msg
    assert "L09" in msg  # fp 가 cover 하는 loc 도 노출


# ── Case B — 다른 sem_key + wrong fp (sem_key 가드만으로 못 잡는 case) ──


def test_fp_link_mismatch_different_sem_key_wrong_fp():
    """L10|main|day|quiet 가 fp_exterior_entrance (L09 의 fp) 참조.

    sem_key 가 다른 raw intent 들 사이라 catalog 단계 sem_key 가드는 silent pass.
    raw validator 가 fp link cross-check 가 없으면 잘못된 fp 위에 render → 시각
    misalignment. T4-fix3 가 raw validator 에서 catch.
    """
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )
    from app.core.bg_catalog import FpLinkMismatchError

    plan = _base_plan_template()
    plan["backgrounds"] = [
        # L10|main|morning|quiet → fp_sales_floor (정상)
        _bg("L10", "morning", "quiet", ["fp_sales_floor"], applies=["S08_Shot1"]),
        # L10|main|day|quiet → fp_exterior_entrance (L09 의 fp — 잘못)
        _bg("L10", "day", "quiet", ["fp_exterior_entrance"], applies=["S08_Shot2"]),
    ]
    with pytest.raises(FpLinkMismatchError) as exc:
        validate_master_plan_raw_intent(
            plan,
            expected_group_id="bg_large_mart",
            group_loc_ids={"L09", "L10", "L14"},
            group_shot_ids={"S08_Shot1", "S08_Shot2"},
            location_profiles=_LOCATION_PROFILES_MART,
        )
    assert "L10" in str(exc.value) and "fp_exterior_entrance" in str(exc.value)


# ── Case C — pass (정상 link) ──


def test_fp_link_pass_with_correct_fp_per_location():
    """각 bg 가 자기 location 의 fp 만 참조 → 정상 통과.

    L09 → fp_exterior_entrance, L10 → fp_sales_floor, L14 → fp_office.
    """
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )

    plan = _base_plan_template()
    plan["backgrounds"] = [
        _bg("L10", "morning", "quiet", ["fp_sales_floor"], applies=["S08_Shot1"]),
        _bg("L09", "day", "quiet", ["fp_exterior_entrance"], applies=["S08_Shot2"]),
        _bg("L14", "day", "normal", ["fp_office"], applies=["S08_Shot3"]),
    ]
    # raise 없이 통과
    validate_master_plan_raw_intent(
        plan,
        expected_group_id="bg_large_mart",
        group_loc_ids={"L09", "L10", "L14"},
        group_shot_ids={"S08_Shot1", "S08_Shot2", "S08_Shot3"},
        location_profiles=_LOCATION_PROFILES_MART,
    )


# ── Case D — fp 의 loc_id 가 group 밖 (invariant F2) ──


def test_floor_plan_loc_id_must_be_in_group():
    """floor_plans[].loc_id 가 group_loc_ids 안에 없으면 reject."""
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )

    plan = _base_plan_template()
    plan["floor_plans"][0]["loc_id"] = "L99"  # group 밖
    plan["backgrounds"] = [
        _bg("L10", "morning", "quiet", ["fp_sales_floor"], applies=["S08_Shot1"]),
    ]
    with pytest.raises(ValueError) as exc:
        validate_master_plan_raw_intent(
            plan,
            expected_group_id="bg_large_mart",
            group_loc_ids={"L09", "L10", "L14"},
            group_shot_ids={"S08_Shot1"},
            location_profiles=_LOCATION_PROFILES_MART,
        )
    assert "L99" in str(exc.value)


# ── Case E — fp 의 missing space_key_hint (T4-fix3 schema) ──


def test_floor_plan_missing_space_key_hint():
    """floor_plans[].space_key_hint 부재 시 reject (T4-fix3 schema)."""
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )

    plan = _base_plan_template()
    plan["floor_plans"][0].pop("space_key_hint")
    plan["backgrounds"] = [
        _bg("L10", "morning", "quiet", ["fp_sales_floor"], applies=["S08_Shot1"]),
    ]
    with pytest.raises(ValueError) as exc:
        validate_master_plan_raw_intent(
            plan,
            expected_group_id="bg_large_mart",
            group_loc_ids={"L09", "L10", "L14"},
            group_shot_ids={"S08_Shot1"},
            location_profiles=_LOCATION_PROFILES_MART,
        )
    assert "space_key_hint" in str(exc.value)


# ── Case F — multi_space fp + bg space_key 일치 ──


def test_fp_link_pass_multi_space_aligned():
    """multi_space location 의 fp + bg 가 같은 hint 참조 → 정상 통과."""
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )

    profiles = {
        "L05": {
            "kind": "multi_space",
            "allowed_space_keys": ["main", "kitchen", "rooftop"],
            "default_space_key": "main",
        },
    }
    plan = {
        "group_id": "bg_house",
        "rationale_summary": "다층 거주 공간",
        "floor_plans": [
            {
                "fp_id": "fp_living_main",
                "loc_id": "L05",
                "space_key_hint": "main",
                "sub_location": "living_main",
                "scope": "Living room and entrance",
                "depends_on_fp": [],
            },
            {
                "fp_id": "fp_kitchen",
                "loc_id": "L05",
                "space_key_hint": "kitchen",
                "sub_location": "kitchen",
                "scope": "Kitchen with sink and stove",
                "depends_on_fp": [],
            },
        ],
        "backgrounds": [
            {
                "loc_id": "L05",
                "space_key_hint": "main",
                "time_phase": "day",
                "state_class": "normal",
                "surface_role": "interior_room",
                "applies_to_shots": ["S04_Shot1"],
                "sub_location_label": "",
                "state_label_raw": "",
                "depends_on_fp": ["fp_living_main"],
                "depends_on_bg": [],
            },
            {
                "loc_id": "L05",
                "space_key_hint": "kitchen",
                "time_phase": "day",
                "state_class": "normal",
                "surface_role": "interior_room",
                "applies_to_shots": ["S04_Shot2"],
                "sub_location_label": "",
                "state_label_raw": "",
                "depends_on_fp": ["fp_kitchen"],
                "depends_on_bg": [],
            },
        ],
    }
    validate_master_plan_raw_intent(
        plan,
        expected_group_id="bg_house",
        group_loc_ids={"L05"},
        group_shot_ids={"S04_Shot1", "S04_Shot2"},
        location_profiles=profiles,
    )


def test_fp_link_mismatch_multi_space_wrong_hint():
    """multi_space — bg 가 main 인데 kitchen fp 참조 → FpLinkMismatchError."""
    from app.modules.pipeline.background_master_plan import (
        validate_master_plan_raw_intent,
    )
    from app.core.bg_catalog import FpLinkMismatchError

    profiles = {
        "L05": {
            "kind": "multi_space",
            "allowed_space_keys": ["main", "kitchen"],
            "default_space_key": "main",
        },
    }
    plan = {
        "group_id": "bg_house",
        "rationale_summary": "multi-space mismatch",
        "floor_plans": [
            {
                "fp_id": "fp_living_main",
                "loc_id": "L05",
                "space_key_hint": "main",
                "sub_location": "living_main",
                "scope": "Living room",
                "depends_on_fp": [],
            },
            {
                "fp_id": "fp_kitchen",
                "loc_id": "L05",
                "space_key_hint": "kitchen",
                "sub_location": "kitchen",
                "scope": "Kitchen",
                "depends_on_fp": [],
            },
        ],
        "backgrounds": [
            {
                "loc_id": "L05",
                "space_key_hint": "main",
                "time_phase": "day",
                "state_class": "normal",
                "surface_role": "interior_room",
                "applies_to_shots": ["S04_Shot1"],
                "sub_location_label": "",
                "state_label_raw": "",
                # main bg 가 kitchen fp 참조 — 잘못
                "depends_on_fp": ["fp_kitchen"],
                "depends_on_bg": [],
            },
        ],
    }
    with pytest.raises(FpLinkMismatchError) as exc:
        validate_master_plan_raw_intent(
            plan,
            expected_group_id="bg_house",
            group_loc_ids={"L05"},
            group_shot_ids={"S04_Shot1"},
            location_profiles=profiles,
        )
    msg = str(exc.value)
    assert "main" in msg and "kitchen" in msg


# ── Case G — FpLinkMismatchError 가 SemanticKeyError subclass ──


def test_fp_link_mismatch_is_semantic_key_error_subclass():
    """FpLinkMismatchError 가 SemanticKeyError subclass — 기존 caller 에서 catch 가능."""
    from app.core.bg_catalog import FpLinkMismatchError, SemanticKeyError
    assert issubclass(FpLinkMismatchError, SemanticKeyError)
    err = FpLinkMismatchError("test")
    assert isinstance(err, SemanticKeyError)
    assert isinstance(err, ValueError)
