"""scene/schema.py の OetfConfig / OetfExposureConfig / EvaluationConfig.oetf 仕様ベーステスト．

仕様出典:
- docs/04_SPEC/SPEC_03_基準設計の物理構成.md（「手法訂正」「既知の差分 item 5」節）
- docs/04_SPEC/SPEC_02_シーン定義仕様.md（evaluation セクション）
- docs/01_GUIDE/GUIDE_08_テスト方針.md（テスト方針）
- src/sm_tias_lightsim/scene/schema.py（OetfConfig, OetfExposureConfig 定義）

テスト方針:
  - Pydantic モデルを直接構築して振る舞いを検証する（YAML パースは不要）
  - ファイル I/O はモックしない (GUIDE_08)
  - 後方互換性（oetf キーなし = OETF 無効）を重点的に検証する
"""

from __future__ import annotations

import copy

import pytest
from pydantic import ValidationError

from sm_tias_lightsim.scene.schema import (
    EvaluationConfig,
    OetfConfig,
    OetfExposureConfig,
)


# ---------------------------------------------------------------------------
# 1. OetfExposureConfig — フィールド検証
# ---------------------------------------------------------------------------


def test_oetf_exposure_config_valid_anchor_mean():
    """mode=anchor_mean + target_mean で正常にパースされること．

    仕様: OetfExposureConfig: mode=anchor_mean（デフォルト）, target_mean が必須（schema.py）
    """
    cfg = OetfExposureConfig(mode="anchor_mean", target_mean=146.7146)
    assert cfg.mode == "anchor_mean"
    assert cfg.target_mean == pytest.approx(146.7146)


def test_oetf_exposure_config_default_mode():
    """mode を省略したとき anchor_mean がデフォルトになること．

    仕様: mode: Literal["anchor_mean"] = Field(default="anchor_mean", ...)（schema.py）
    """
    cfg = OetfExposureConfig(target_mean=100.0)
    assert cfg.mode == "anchor_mean"


def test_oetf_exposure_config_missing_target_mean_raises():
    """target_mean を省略したとき ValidationError が発生すること．

    仕様: target_mean: float = Field(..., ...) — 必須フィールド（schema.py）
    """
    with pytest.raises(ValidationError):
        OetfExposureConfig(mode="anchor_mean")


def test_oetf_exposure_config_invalid_mode_raises():
    """未対応の mode（例: fixed_scale）で ValidationError が発生すること．

    仕様: mode: Literal["anchor_mean"] — anchor_mean のみ（schema.py）
    """
    with pytest.raises(ValidationError):
        OetfExposureConfig(mode="fixed_scale", target_mean=100.0)  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# 2. OetfConfig — フィールド検証
# ---------------------------------------------------------------------------


def test_oetf_config_defaults():
    """OetfConfig のデフォルト値を確認する．

    仕様:
    - enabled: bool = False（省略時は OETF 無効）
    - transfer: Literal["srgb"] = "srgb"
    - exposure: None（省略可）
    - quantize_uint8: bool = True
    （schema.py OetfConfig）
    """
    cfg = OetfConfig()
    assert cfg.enabled is False
    assert cfg.transfer == "srgb"
    assert cfg.exposure is None
    assert cfg.quantize_uint8 is True


def test_oetf_config_enabled_true():
    """enabled=True で OetfConfig が生成されること．"""
    cfg = OetfConfig(enabled=True, exposure=OetfExposureConfig(target_mean=146.0))
    assert cfg.enabled is True


def test_oetf_config_invalid_transfer_raises():
    """transfer に未対応の値（例: bt709）を指定したとき ValidationError が発生すること．

    仕様: transfer: Literal["srgb"] — srgb のみ（schema.py）
    """
    with pytest.raises(ValidationError):
        OetfConfig(transfer="bt709")  # type: ignore[call-arg]


def test_oetf_config_with_full_exposure():
    """exposure を含む完全な OetfConfig が正常にパースされること．

    仕様: eval_baseline.yaml の oetf セクション相当
    """
    exposure = OetfExposureConfig(mode="anchor_mean", target_mean=146.7146)
    cfg = OetfConfig(
        enabled=True,
        transfer="srgb",
        exposure=exposure,
        quantize_uint8=True,
    )
    assert cfg.enabled is True
    assert cfg.exposure is not None
    assert cfg.exposure.target_mean == pytest.approx(146.7146)
    assert cfg.quantize_uint8 is True


def test_oetf_config_quantize_uint8_false():
    """quantize_uint8=False が正常にパースされること．

    仕様: quantize_uint8: bool = True（False も許容）（schema.py）
    """
    cfg = OetfConfig(quantize_uint8=False)
    assert cfg.quantize_uint8 is False


def test_oetf_config_is_frozen():
    """OetfConfig が frozen（イミュータブル）であること．

    仕様: _FrozenBase → frozen=True（schema.py ConfigDict）
    """
    cfg = OetfConfig()
    with pytest.raises(ValidationError):
        cfg.enabled = True  # type: ignore[misc]


# ---------------------------------------------------------------------------
# 3. EvaluationConfig.oetf — 後方互換性
# ---------------------------------------------------------------------------


def _make_minimal_eval_data(**overrides: object) -> dict:
    """最小構成の EvaluationConfig データを返す．"""
    data: dict = {
        "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100},
    }
    data.update(overrides)
    return data


def test_evaluation_config_oetf_none_when_omitted():
    """oetf キーを省略したとき EvaluationConfig.oetf が None になること（後方互換）．

    仕様: oetf: OetfConfig | None = Field(default=None, ...)
         省略時は OETF 無効（linear のまま評価）（schema.py EvaluationConfig）
    SPEC_03: eval_real.yaml には oetf を設定しないこと
    """
    data = _make_minimal_eval_data()
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is None


def test_evaluation_config_oetf_none_means_disabled():
    """oetf=None のとき OETF 適用がされない（enabled=False 相当）を確認すること．

    仕様: 省略時は OETF 無効扱い（linear のまま評価）（schema.py / cli/evaluate.py）
    """
    data = _make_minimal_eval_data(oetf=None)
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is None
    # cli/evaluate.py では `if oetf_cfg is not None and oetf_cfg.enabled:` でスキップ
    # → oetf=None なら OETF は適用されない
    oetf_cfg = cfg.oetf
    is_applied = oetf_cfg is not None and oetf_cfg.enabled
    assert is_applied is False


def test_evaluation_config_oetf_disabled_explicit():
    """oetf.enabled=False を明示したとき OETF が無効であること．

    仕様: enabled: bool = False（省略時デフォルト）（schema.py）
    """
    data = _make_minimal_eval_data(oetf={"enabled": False})
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is False
    # OETF は適用されない
    is_applied = cfg.oetf is not None and cfg.oetf.enabled
    assert is_applied is False


def test_evaluation_config_oetf_enabled_with_exposure():
    """oetf.enabled=True かつ exposure を設定した EvaluationConfig が正常にパースされること．

    仕様: eval_baseline.yaml の oetf セクション相当（SPEC_03「キャリブレーション経過」）
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "transfer": "srgb",
            "exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
            "quantize_uint8": True,
        }
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is True
    assert cfg.oetf.transfer == "srgb"
    assert cfg.oetf.exposure is not None
    assert cfg.oetf.exposure.target_mean == pytest.approx(146.7146)
    assert cfg.oetf.quantize_uint8 is True


def test_evaluation_config_without_oetf_is_backward_compatible():
    """既存の eval_real.yaml 相当（oetf キーなし）が後方互換で動くこと．

    仕様: oetf フィールドを省略した既存 YAML は後方互換（OETF 無効扱い）で動作する
         （schema.py EvaluationConfig docstring）
    eval_real.yaml は実測 PNG 向けで OETF 設定なし（二重適用禁止）
    """
    # eval_real.yaml 相当の最小データ（oetf キーなし）
    data = {
        "roi": {"type": "image_pixel", "x": 24, "y": 37, "width": 464, "height": 450},
        "zones": {"enabled": True, "boundaries": [0.33, 0.66]},
        "metrics": {"cov": True, "std": True, "max_min": True, "radial_profile": True},
    }
    cfg = EvaluationConfig.model_validate(data)
    # oetf は None（後方互換）
    assert cfg.oetf is None
    # OETF は適用されない（実測 PNG への二重適用を防ぐ）
    is_applied = cfg.oetf is not None and cfg.oetf.enabled
    assert is_applied is False


def test_evaluation_config_oetf_invalid_transfer_raises():
    """oetf.transfer に未対応の値を指定したとき ValidationError が発生すること．

    仕様: transfer: Literal["srgb"]（schema.py OetfConfig）
    """
    data = _make_minimal_eval_data(
        oetf={"enabled": True, "transfer": "gamma22"}
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


def test_evaluation_config_oetf_invalid_exposure_mode_raises():
    """oetf.exposure.mode に未対応の値を指定したとき ValidationError が発生すること．

    仕様: mode: Literal["anchor_mean"]（schema.py OetfExposureConfig）
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "exposure": {"mode": "fixed_gain", "target_mean": 100.0},
        }
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


def test_evaluation_config_oetf_exposure_missing_target_mean_raises():
    """oetf.exposure.target_mean を省略したとき ValidationError が発生すること．

    仕様: target_mean: float = Field(...) — 必須（schema.py OetfExposureConfig）
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "exposure": {"mode": "anchor_mean"},
        }
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


# ---------------------------------------------------------------------------
# 4. eval_baseline.yaml 相当の YAML ロード検証
# ---------------------------------------------------------------------------


def test_eval_baseline_yaml_equivalent_parses_correctly():
    """eval_baseline.yaml 相当のデータが正しくパースされること．

    仕様: configs/eval_baseline.yaml の oetf セクション（SPEC_03 正規実装）
    """
    # configs/eval_baseline.yaml に対応するデータ
    data = {
        "roi": {"type": "image_pixel", "x": 24, "y": 37, "width": 464, "height": 450},
        "zones": {"enabled": True, "boundaries": [0.33, 0.66]},
        "metrics": {"cov": True, "std": True, "max_min": True, "radial_profile": True},
        "oetf": {
            "enabled": True,
            "transfer": "srgb",
            "exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
            "quantize_uint8": True,
        },
    }
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is True
    assert cfg.oetf.transfer == "srgb"
    assert cfg.oetf.exposure is not None
    assert cfg.oetf.exposure.mode == "anchor_mean"
    assert cfg.oetf.exposure.target_mean == pytest.approx(146.7146)
    assert cfg.oetf.quantize_uint8 is True
    assert cfg.roi.x == 24
    assert cfg.roi.y == 37
    assert cfg.roi.width == 464
    assert cfg.roi.height == 450
