# pyright: reportArgumentType=none, reportCallIssue=none
# Pydantic v2 の ValidationError は pydantic_core 由来で，Pylance/Pyright が
# BaseException のサブクラスと推論しないため pytest.raises(ValidationError) を
# 誤検知する．また「必須フィールドを意図的に欠落させる」テスト（line 78 等）でも
# reportCallIssue が誤発火するため，ファイル単位で抑止する（builder.py と同じ方針）．
"""scene/schema.py の NormalizationConfig / EvaluationConfig.normalization 仕様ベーステスト．

仕様出典:
- 実装サマリー（NormalizationConfig クラス，EvaluationConfig.normalization フィールド，
  OETF + normalization 同時有効化禁止 model_validator）
- docs/04_SPEC/SPEC_04_評価仕様.md（評価パイプライン）
- .claude/rules/testing.md（テスト方針）
- src/sm_tias_lightsim/scene/schema.py（NormalizationConfig, EvaluationConfig 定義）

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

from __future__ import annotations

import pytest
from pydantic import ValidationError

from sm_tias_lightsim.scene.schema import EvaluationConfig, NormalizationConfig

# ---------------------------------------------------------------------------
# 共通ヘルパー
# ---------------------------------------------------------------------------


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


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


def test_normalization_config_valid_mean_anchor():
    """type=mean_anchor + target_mean で正常にパースされること．

    仕様: NormalizationConfig: type=mean_anchor（Literal），target_mean が必須（schema.py）
    """
    cfg = NormalizationConfig(enabled=True, type="mean_anchor", target_mean=146.7146)
    assert cfg.enabled is True
    assert cfg.type == "mean_anchor"
    assert cfg.target_mean == pytest.approx(146.7146)


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

    仕様:
    - enabled: bool = False（省略時は正規化無効）
    - type: Literal["mean_anchor"] = "mean_anchor"（デフォルト）
    （schema.py NormalizationConfig）
    """
    cfg = NormalizationConfig(target_mean=100.0)
    assert cfg.enabled is False
    assert cfg.type == "mean_anchor"


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

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


def test_normalization_config_invalid_type_raises():
    """type に未対応の値（例: max_anchor）で ValidationError が発生すること．

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


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

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


def test_normalization_config_target_mean_various_values():
    """target_mean に任意の float 値が受け付けられること．

    仕様: target_mean: float — 任意 float 可（schema.py NormalizationConfig）
    """
    # uint8 スケールの実測 mean 相当
    cfg1 = NormalizationConfig(target_mean=146.7146)
    assert cfg1.target_mean == pytest.approx(146.7146)

    # 0 以外の任意 float
    cfg2 = NormalizationConfig(target_mean=255.0)
    assert cfg2.target_mean == pytest.approx(255.0)

    cfg3 = NormalizationConfig(target_mean=1.0)
    assert cfg3.target_mean == pytest.approx(1.0)


# ---------------------------------------------------------------------------
# 2. EvaluationConfig.normalization — 後方互換性
# ---------------------------------------------------------------------------


def test_evaluation_config_normalization_none_when_omitted():
    """normalization キーを省略したとき EvaluationConfig.normalization が None になること．

    仕様: normalization: NormalizationConfig | None = Field(default=None, ...)
         省略時は正規化無効（schema.py EvaluationConfig）
    """
    data = _make_minimal_eval_data()
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.normalization is None


def test_evaluation_config_normalization_none_means_disabled():
    """normalization=None のとき正規化が適用されないこと（enabled=False 相当を確認）．

    仕様: 省略時は正規化無効扱い（schema.py / cli/evaluate.py）
    """
    data = _make_minimal_eval_data(normalization=None)
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.normalization is None
    # cli/evaluate.py では `if norm_cfg is not None and norm_cfg.enabled:` でスキップ
    norm_cfg = cfg.normalization
    is_applied = norm_cfg is not None and norm_cfg.enabled
    assert is_applied is False


def test_evaluation_config_normalization_disabled_explicit():
    """normalization.enabled=False を明示したとき正規化が無効であること．

    仕様: enabled: bool = False（省略時デフォルト）（schema.py）
    """
    data = _make_minimal_eval_data(
        normalization={"enabled": False, "target_mean": 100.0}
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is False
    is_applied = cfg.normalization is not None and cfg.normalization.enabled
    assert is_applied is False


def test_evaluation_config_normalization_enabled():
    """normalization.enabled=True で正常にパースされること．

    仕様: eval_baseline_linear.yaml の normalization セクション相当
    """
    data = _make_minimal_eval_data(
        normalization={
            "enabled": True,
            "type": "mean_anchor",
            "target_mean": 146.7146,
        }
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is True
    assert cfg.normalization.type == "mean_anchor"
    assert cfg.normalization.target_mean == pytest.approx(146.7146)


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

    仕様: normalization フィールドを省略した既存 YAML も後方互換（正規化無効扱い）で動作する
         （schema.py EvaluationConfig docstring）
    """
    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)
    assert cfg.normalization is None
    assert cfg.oetf is None


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

    仕様: type: Literal["mean_anchor"]（schema.py NormalizationConfig）
    """
    data = _make_minimal_eval_data(
        normalization={"enabled": True, "type": "max_anchor", "target_mean": 100.0}
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


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

    仕様: target_mean: float = Field(...) — 必須（schema.py NormalizationConfig）
    """
    data = _make_minimal_eval_data(
        normalization={"enabled": True, "type": "mean_anchor"}
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


# ---------------------------------------------------------------------------
# 3. OETF + normalization 同時有効化の排他制御
# ---------------------------------------------------------------------------


def test_oetf_and_normalization_both_enabled_raises():
    """oetf.enabled=True かつ normalization.enabled=True のとき ValidationError が発生すること．

    仕様（実装サマリー「スキーマ排他性」，schema.py EvaluationConfig model_validator）:
        oetf.enabled=True かつ normalization.enabled=True は ValidationError
        （OETF が内部で露出アンカーを持つため，二重アンカーになるのを防ぐ）
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "transfer": "srgb",
            "exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
        },
        normalization={
            "enabled": True,
            "type": "mean_anchor",
            "target_mean": 146.7146,
        },
    )
    with pytest.raises(ValidationError):
        EvaluationConfig.model_validate(data)


def test_oetf_enabled_normalization_disabled_is_valid():
    """oetf.enabled=True かつ normalization.enabled=False は有効であること．

    仕様: 片方だけ有効化は OK（スキーマ排他性は両方 True のみ禁止）
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "transfer": "srgb",
            "exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
        },
        normalization={
            "enabled": False,
            "type": "mean_anchor",
            "target_mean": 146.7146,
        },
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is True
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is False


def test_oetf_disabled_normalization_enabled_is_valid():
    """oetf.enabled=False かつ normalization.enabled=True は有効であること．

    仕様: 片方だけ有効化は OK
    """
    data = _make_minimal_eval_data(
        oetf={"enabled": False},
        normalization={
            "enabled": True,
            "type": "mean_anchor",
            "target_mean": 146.7146,
        },
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is False
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is True


def test_both_disabled_is_valid():
    """oetf.enabled=False かつ normalization.enabled=False は有効であること（無変換）．

    仕様: 両方 None または両方 disabled は OK（無変換）
    """
    data = _make_minimal_eval_data(
        oetf={"enabled": False},
        normalization={"enabled": False, "target_mean": 100.0},
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is False
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is False


def test_both_none_is_valid():
    """oetf=None かつ normalization=None は有効であること（既存 YAML 後方互換）．

    仕様: 両方 None は OK
    """
    data = _make_minimal_eval_data()
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is None
    assert cfg.normalization is None


def test_oetf_none_normalization_enabled_is_valid():
    """oetf=None（省略）かつ normalization.enabled=True は有効であること．

    仕様: 片方だけ有効化は OK（None は disabled 扱い）
    """
    data = _make_minimal_eval_data(
        normalization={
            "enabled": True,
            "type": "mean_anchor",
            "target_mean": 100.0,
        }
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is None
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is True


def test_oetf_enabled_normalization_none_is_valid():
    """oetf.enabled=True かつ normalization=None（省略）は有効であること．

    仕様: 片方だけ有効化は OK
    """
    data = _make_minimal_eval_data(
        oetf={
            "enabled": True,
            "transfer": "srgb",
            "exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
        }
    )
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.oetf is not None
    assert cfg.oetf.enabled is True
    assert cfg.normalization is None


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


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

    仕様: configs/eval_baseline_linear.yaml の normalization セクション相当
    """
    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},
        "normalization": {
            "enabled": True,
            "type": "mean_anchor",
            "target_mean": 146.7146,
        },
    }
    cfg = EvaluationConfig.model_validate(data)
    assert cfg.normalization is not None
    assert cfg.normalization.enabled is True
    assert cfg.normalization.type == "mean_anchor"
    assert cfg.normalization.target_mean == pytest.approx(146.7146)
    assert cfg.oetf is None
    assert cfg.roi.x == 24
    assert cfg.roi.y == 37
    assert cfg.roi.width == 464
    assert cfg.roi.height == 450
