"""evaluation/normalize.py の仕様ベーステスト．

仕様出典:
- 実装サマリー（apply_mean_anchor 関数仕様）
- docs/04_SPEC/SPEC_03_基準設計の物理構成.md（「形状指標への影響（重要）」節）
- docs/04_SPEC/SPEC_04_評価仕様.md（評価パイプライン・メトリクス定義）
- .claude/rules/testing.md（テスト方針）

テスト対象:
  apply_mean_anchor(values, target_mean) -> (normalized, alpha)

テスト方針:
  - 既知の入力・出力で数値精度を検証する（実装詳細でなく振る舞いを検証）
  - ファイル I/O はモックせず実 ndarray を使う (testing.md)
  - 乱数を使う場合は seed を必ず固定する (testing.md)
"""

from __future__ import annotations

import numpy
import numpy.testing
import pytest

from sm_tias_lightsim.evaluation.normalize import apply_mean_anchor

# ---------------------------------------------------------------------------
# 1. 正常系 — mean が target_mean に一致すること
# ---------------------------------------------------------------------------


def test_mean_equals_target_mean_uniform_array():
    """一様配列に apply_mean_anchor を適用すると mean が target_mean に厳密一致すること．

    仕様: apply_mean_anchor: 出力の mean は target_mean に厳密一致（浮動小数点誤差を許容）
    """
    values = numpy.ones((10, 10), dtype=numpy.float64) * 50.0
    target_mean = 100.0
    normalized, _ = apply_mean_anchor(values, target_mean)
    assert numpy.mean(normalized) == pytest.approx(target_mean, rel=1e-10)


def test_mean_equals_target_mean_random_array():
    """乱数配列に apply_mean_anchor を適用すると mean が target_mean に一致すること．

    仕様: apply_mean_anchor: α = target_mean / values.mean() を全要素に乗算
         → normalized.mean() == target_mean
    """
    rng = numpy.random.RandomState(42)
    values = rng.uniform(1.0, 100.0, (50, 50))
    target_mean = 146.7146
    normalized, _ = apply_mean_anchor(values, target_mean)
    assert numpy.mean(normalized) == pytest.approx(target_mean, rel=1e-10)


def test_mean_equals_target_mean_1d_array():
    """1D 配列でも mean が target_mean に一致すること（任意形状サポート）．

    仕様: apply_mean_anchor: 入力 numpy.ndarray（任意形状・任意次元）
    """
    values = numpy.array([10.0, 20.0, 30.0, 40.0, 50.0])
    target_mean = 75.0
    normalized, _ = apply_mean_anchor(values, target_mean)
    assert numpy.mean(normalized) == pytest.approx(target_mean, rel=1e-10)


# ---------------------------------------------------------------------------
# 2. alpha 値の正確性
# ---------------------------------------------------------------------------


def test_alpha_equals_target_mean_over_src_mean():
    """返却される alpha が target_mean / values.mean() に一致すること．

    仕様: apply_mean_anchor: alpha = target_mean / values.mean()
    """
    values = numpy.ones((5, 5), dtype=numpy.float64) * 30.0
    target_mean = 90.0
    _, alpha = apply_mean_anchor(values, target_mean)
    expected_alpha = target_mean / numpy.mean(values)
    assert alpha == pytest.approx(expected_alpha, rel=1e-12)


def test_alpha_less_than_one_when_src_mean_larger():
    """src_mean > target_mean のとき alpha < 1 になること（スケールダウン）．

    仕様: apply_mean_anchor: α = target_mean / src_mean
         src_mean > target_mean → α < 1
    """
    values = numpy.ones((10,)) * 200.0
    target_mean = 100.0
    _, alpha = apply_mean_anchor(values, target_mean)
    assert alpha < 1.0


def test_alpha_greater_than_one_when_src_mean_smaller():
    """src_mean < target_mean のとき alpha > 1 になること（スケールアップ）．

    仕様: apply_mean_anchor: α = target_mean / src_mean
         src_mean < target_mean → α > 1
    """
    values = numpy.ones((10,)) * 50.0
    target_mean = 200.0
    _, alpha = apply_mean_anchor(values, target_mean)
    assert alpha > 1.0


def test_alpha_equals_one_when_src_mean_equals_target():
    """src_mean == target_mean のとき alpha == 1.0 になること．

    仕様: α = target_mean / values.mean()
         values.mean() == target_mean → α = 1.0
    """
    values = numpy.ones((10,)) * 100.0
    target_mean = 100.0
    normalized, alpha = apply_mean_anchor(values, target_mean)
    assert alpha == pytest.approx(1.0, rel=1e-12)
    numpy.testing.assert_array_almost_equal(normalized, values)


def test_alpha_matches_documented_example():
    """実装サマリーの既知値（α=0.961934）に近いことを確認する．

    仕様（実装サマリー「動作確認済み（参考）」）:
        入力: output/baseline/luminance.npy
        α = 0.961934
    NOTE: luminance.npy の実際の値は不明なため，
          同等の src_mean から alpha を計算して検証する（alpha の計算式テスト）．
    仕様の target_mean=146.7146, α=0.961934 から逆算すると
    src_mean = 146.7146 / 0.961934 ≈ 152.52
    """
    src_mean = 146.7146 / 0.961934  # ≈ 152.52
    values = numpy.ones((10, 10), dtype=numpy.float64) * src_mean
    target_mean = 146.7146
    _, alpha = apply_mean_anchor(values, target_mean)
    assert alpha == pytest.approx(0.961934, rel=1e-5)


# ---------------------------------------------------------------------------
# 3. 線形ゲインの不変性（形状指標 CoV / max-min / C/P 比）
# ---------------------------------------------------------------------------


def test_cov_is_invariant_to_mean_anchor():
    """apply_mean_anchor の適用後に CoV（std/mean）が不変であること．

    仕様（SPEC_03「形状指標への影響（重要）」，実装サマリー「線形ゲインの不変性」）:
        線形ゲインは形状指標（CoV = std/mean）に厳密に不変
    数学的根拠: (α·std) / (α·mean) = std / mean
    """
    rng = numpy.random.RandomState(0)
    values = rng.uniform(50.0, 150.0, (100, 100))
    target_mean = 200.0

    cov_before = numpy.std(values) / numpy.mean(values)
    normalized, _ = apply_mean_anchor(values, target_mean)
    cov_after = numpy.std(normalized) / numpy.mean(normalized)

    assert cov_after == pytest.approx(cov_before, rel=1e-10)


def test_max_min_ratio_is_invariant_to_mean_anchor():
    """apply_mean_anchor の適用後に max/min 比が不変であること．

    仕様（SPEC_03「形状指標への影響（重要）」，実装サマリー「線形ゲインの不変性」）:
        線形ゲインは max/min 比に不変
    数学的根拠: (α·max) / (α·min) = max / min
    """
    rng = numpy.random.RandomState(1)
    values = rng.uniform(10.0, 200.0, (50, 50))  # min > 0 を保証
    target_mean = 80.0

    ratio_before = numpy.max(values) / numpy.min(values)
    normalized, _ = apply_mean_anchor(values, target_mean)
    ratio_after = numpy.max(normalized) / numpy.min(normalized)

    assert ratio_after == pytest.approx(ratio_before, rel=1e-10)


def test_center_periphery_ratio_is_invariant_to_mean_anchor():
    """apply_mean_anchor の適用後に中心/周辺比が不変であること．

    仕様（SPEC_03「形状指標への影響（重要）」，実装サマリー「線形ゲインの不変性」）:
        線形ゲインは C/P 比に不変
    数学的根拠: (α·center_mean) / (α·periphery_mean) = center_mean / periphery_mean
    """
    rng = numpy.random.RandomState(2)
    # 中心と周辺で異なる輝度を持つ配列を合成する
    arr = rng.uniform(50.0, 100.0, (60, 60))
    center = arr[20:40, 20:40]
    periphery = numpy.concatenate([arr[:20, :].ravel(), arr[40:, :].ravel()])

    cp_before = numpy.mean(center) / numpy.mean(periphery)
    normalized, _ = apply_mean_anchor(arr, 120.0)
    center_norm = normalized[20:40, 20:40]
    periphery_norm = numpy.concatenate([normalized[:20, :].ravel(), normalized[40:, :].ravel()])
    cp_after = numpy.mean(center_norm) / numpy.mean(periphery_norm)

    assert cp_after == pytest.approx(cp_before, rel=1e-10)


def test_gradient_magnitude_is_invariant_to_mean_anchor():
    """apply_mean_anchor の適用後に勾配（(center-periphery)/center * 100）が不変であること．

    仕様（SPEC_03「形状指標への影響（重要）」，実装サマリー「線形ゲインの不変性」）:
        線形ゲインは勾配 % に不変
    数学的根拠: (α·c - α·p) / (α·c) * 100 = (c - p) / c * 100
    """
    rng = numpy.random.RandomState(3)
    arr = rng.uniform(50.0, 100.0, (60, 60))
    center = arr[20:40, 20:40]
    periphery = numpy.concatenate([arr[:20, :].ravel(), arr[40:, :].ravel()])

    c_mean = numpy.mean(center)
    p_mean = numpy.mean(periphery)
    gradient_before = (c_mean - p_mean) / c_mean * 100.0

    normalized, _ = apply_mean_anchor(arr, 200.0)
    center_norm = normalized[20:40, 20:40]
    periphery_norm = numpy.concatenate([normalized[:20, :].ravel(), normalized[40:, :].ravel()])
    c_mean_norm = numpy.mean(center_norm)
    p_mean_norm = numpy.mean(periphery_norm)
    gradient_after = (c_mean_norm - p_mean_norm) / c_mean_norm * 100.0

    assert gradient_after == pytest.approx(gradient_before, rel=1e-10)


# ---------------------------------------------------------------------------
# 4. 形状保持（入力 shape == 出力 shape）
# ---------------------------------------------------------------------------


def test_output_shape_matches_input_shape_2d():
    """2D 入力の shape が出力でも保持されること．

    仕様: apply_mean_anchor: 入力 numpy.ndarray（任意形状・任意次元）
         出力 normalized は values と同形状
    """
    values = numpy.ones((100, 120), dtype=numpy.float64) * 50.0
    normalized, _ = apply_mean_anchor(values, 100.0)
    assert normalized.shape == (100, 120)


def test_output_shape_matches_input_shape_1d():
    """1D 入力の shape が出力でも保持されること．"""
    values = numpy.ones((500,), dtype=numpy.float64) * 30.0
    normalized, _ = apply_mean_anchor(values, 60.0)
    assert normalized.shape == (500,)


def test_output_shape_matches_input_shape_3d():
    """3D 入力の shape が出力でも保持されること（任意次元サポート）．"""
    values = numpy.ones((10, 20, 3), dtype=numpy.float64) * 50.0
    normalized, _ = apply_mean_anchor(values, 100.0)
    assert normalized.shape == (10, 20, 3)


# ---------------------------------------------------------------------------
# 5. dtype 保持
# ---------------------------------------------------------------------------


def test_dtype_preserved_float64():
    """float64 入力の dtype が出力でも保持されること．

    仕様: apply_mean_anchor: 出力 dtype は values と同じ
    """
    values = numpy.ones((10, 10), dtype=numpy.float64) * 50.0
    normalized, _ = apply_mean_anchor(values, 100.0)
    assert normalized.dtype == numpy.float64


def test_dtype_preserved_float32():
    """float32 入力の dtype が出力でも保持されること．

    仕様: apply_mean_anchor: 出力 dtype は values と同じ
    """
    values = numpy.ones((10, 10), dtype=numpy.float32) * 50.0
    normalized, _ = apply_mean_anchor(values, 100.0)
    assert normalized.dtype == numpy.float32


def test_dtype_float_int_input_casts_correctly():
    """整数配列を渡したとき，numpy の演算ルールで正しく動作すること．

    仕様: apply_mean_anchor は任意の numpy 数値配列を受け付ける
    NOTE: int 配列 * float alpha は numpy がキャストして float64 になる
    """
    values = numpy.array([10, 20, 30, 40, 50], dtype=numpy.int32)
    target_mean = 60.0
    normalized, alpha = apply_mean_anchor(values, target_mean)
    assert numpy.mean(normalized) == pytest.approx(target_mean, rel=1e-10)


# ---------------------------------------------------------------------------
# 6. エラー: values.mean() == 0 で ValueError
# ---------------------------------------------------------------------------


def test_zero_mean_raises_value_error():
    """values の mean が 0 のとき ValueError が発生すること．

    仕様: apply_mean_anchor: values.mean() == 0 の場合は ValueError
    """
    values = numpy.zeros((10, 10), dtype=numpy.float64)
    with pytest.raises(ValueError, match="mean"):
        apply_mean_anchor(values, 100.0)


def test_all_negative_sum_to_zero_raises_value_error():
    """[-1, 0, 1] のように mean が 0 になる配列でも ValueError が発生すること．

    仕様: values.mean() == 0 の場合は ValueError
    """
    values = numpy.array([-1.0, 0.0, 1.0])
    with pytest.raises(ValueError):
        apply_mean_anchor(values, 50.0)


# ---------------------------------------------------------------------------
# 7. 返却値の型チェック（tuple[ndarray, float]）
# ---------------------------------------------------------------------------


def test_return_type_is_tuple_of_ndarray_and_float():
    """戻り値が (numpy.ndarray, float) のタプルであること．

    仕様: apply_mean_anchor: 出力 (normalized_array, alpha) のタプル
    """
    values = numpy.ones((10,), dtype=numpy.float64) * 50.0
    result = apply_mean_anchor(values, 100.0)
    assert isinstance(result, tuple)
    assert len(result) == 2
    normalized, alpha = result
    assert isinstance(normalized, numpy.ndarray)
    assert isinstance(alpha, float)


# ---------------------------------------------------------------------------
# 8. 連続適用の冪等性（同じ target_mean で 2 回適用すると 2 回目は変化なし）
# ---------------------------------------------------------------------------


def test_idempotent_when_applied_twice():
    """target_mean に揃えた配列に再度同じ target_mean で適用すると変化がないこと．

    仕様: 正規化後 mean == target_mean → 2 回目の alpha == 1.0
    """
    rng = numpy.random.RandomState(99)
    values = rng.uniform(10.0, 100.0, (30, 30))
    target_mean = 80.0

    normalized_once, _ = apply_mean_anchor(values, target_mean)
    normalized_twice, alpha2 = apply_mean_anchor(normalized_once, target_mean)

    assert alpha2 == pytest.approx(1.0, rel=1e-10)
    numpy.testing.assert_array_almost_equal(normalized_twice, normalized_once)
