"""야외 구조물 파이프 정책 SOT — 조합 유효성 계약 (2026-07-31).

설계 §5.1: 조합 유효성을 config_hash 나 manifest applicability 에 맡기지
않는다. hash 는 무효화·감사 장치이지 validity gate 가 아니다. 불가능한
조합은 실행·claim **이전에** 중앙 validator 가 거부한다.
"""
from __future__ import annotations

from types import SimpleNamespace

import pytest

from app.core.errors import AppError
from app.core.structure_pipeline_policy import (
    CRITIQUE_OFF,
    CRITIQUE_STRUCTURE_PRESERVING_V1,
    PLACE_MARK_OFF,
    PLACE_MARK_V1,
    POLICY_VERSION,
    SEED_RECIPE_LEGACY,
    SEED_RECIPE_SKELETON_LOOK_V1,
    resolve_structure_pipeline_policy,
)


def _settings(**over):
    base = dict(
        structure_seed_recipe=SEED_RECIPE_LEGACY,
        place_mark_mode=PLACE_MARK_OFF,
        seed_critique_mode=CRITIQUE_OFF,
        structure_seed_variants_enabled=False,
        outdoor_lane_pipe_enabled=False,
        outdoor_lane_plan_enabled=False,
    )
    base.update(over)
    return SimpleNamespace(**base)


def _full_on(**over):
    base = dict(
        structure_seed_recipe=SEED_RECIPE_SKELETON_LOOK_V1,
        place_mark_mode=PLACE_MARK_OFF,
        seed_critique_mode=CRITIQUE_OFF,
        structure_seed_variants_enabled=True,
        outdoor_lane_pipe_enabled=True,
        outdoor_lane_plan_enabled=True,
    )
    base.update(over)
    return SimpleNamespace(**base)


def test_default_is_legacy_and_everything_off():
    """기본값 = 오늘의 경로. 새 스텝은 아무것도 켜지지 않는다."""
    p = resolve_structure_pipeline_policy(_settings())
    assert p.seed_recipe == SEED_RECIPE_LEGACY
    assert p.skeleton_look_on is False
    assert p.place_mark_on is False
    assert p.structure_critique_on is False


def test_skeleton_look_requires_variant_authoring():
    """변형 저작이 꺼져 있으면 skeleton_look 은 성립하지 않는다.

    스케치는 저작된 conformance brief 에서 나온다 — 저작이 없으면 입력
    자체가 없다.
    """
    with pytest.raises(AppError) as exc:
        resolve_structure_pipeline_policy(
            _full_on(structure_seed_variants_enabled=False))
    assert "structure_seed_variants_enabled" in str(exc.value.message)


def test_skeleton_look_requires_outdoor_lane_pipe():
    """야외 파이프가 꺼져 있으면 레시피가 조용히 죽는다 — 거부한다."""
    with pytest.raises(AppError):
        resolve_structure_pipeline_policy(
            _full_on(outdoor_lane_pipe_enabled=False))


def test_place_mark_requires_skeleton_look_recipe():
    """표시면 재작화는 seed_plan 의 목표 문안을 소비한다 — legacy 에는 없다."""
    with pytest.raises(AppError) as exc:
        resolve_structure_pipeline_policy(
            _settings(place_mark_mode=PLACE_MARK_V1))
    assert "place_mark_mode" in str(exc.value.message)


def test_structure_critique_requires_skeleton_look_recipe():
    with pytest.raises(AppError):
        resolve_structure_pipeline_policy(
            _settings(seed_critique_mode=CRITIQUE_STRUCTURE_PRESERVING_V1))


def test_unknown_value_is_rejected_not_coerced():
    """오타를 기본값으로 흡수하면 사용자가 켠 줄 알고 있는데 꺼져 있다."""
    with pytest.raises(AppError) as exc:
        resolve_structure_pipeline_policy(
            _settings(structure_seed_recipe="skeleton_look"))
    assert "structure_seed_recipe" in str(exc.value.message)


def test_full_stack_on_is_accepted():
    p = resolve_structure_pipeline_policy(
        _full_on(place_mark_mode=PLACE_MARK_V1,
                 seed_critique_mode=CRITIQUE_STRUCTURE_PRESERVING_V1))
    assert p.skeleton_look_on is True
    assert p.place_mark_on is True
    assert p.structure_critique_on is True


def test_hash_payload_carries_policy_version_and_all_three():
    """각 스텝 config_hash 는 resolved policy 를 **기록**만 한다."""
    p = resolve_structure_pipeline_policy(_full_on())
    payload = p.as_hash_payload()
    assert payload["policy_version"] == POLICY_VERSION
    assert payload["seed_recipe"] == SEED_RECIPE_SKELETON_LOOK_V1
    assert payload["place_mark_mode"] == PLACE_MARK_OFF
    assert payload["seed_critique_mode"] == CRITIQUE_OFF


def test_policy_is_frozen():
    """정책 객체가 실행 중에 바뀌면 hash 와 동작이 어긋난다."""
    import dataclasses

    p = resolve_structure_pipeline_policy(_settings())
    with pytest.raises(dataclasses.FrozenInstanceError):
        p.seed_recipe = SEED_RECIPE_SKELETON_LOOK_V1  # type: ignore[misc]
