"""reproducibility.py の単体テスト.

仕様 (SPEC_02_照明均一性評価アルゴリズム.md — 「再現性評価」節):

  calc_pairwise_ssim:
    - N 枚の輝度マップから全 C(N, 2) ペアの SSIM を算出する
    - data_range = 255.0 固定
    - 返り値: pairwise_scores, mean_ssim, min_ssim, max_ssim, std_ssim, n_pairs
    - 2 枚未満で ValueError

  calc_pairwise_ssim_registered:
    - 各ペアで位置合わせなし SSIM と位置合わせ後 SSIM の両方を算出する
    - 位置合わせ: phase_correlate → apply_shift → compute_valid_mask でクロップ
    - 有効ピクセル数 <= 49 の場合は位置合わせなし SSIM で代替
    - 返り値: pairwise_scores に ssim, ssim_registered, dx, dy, shift_magnitude を含む
    - 集計キー: mean_ssim, min_ssim, max_ssim, std_ssim,
                mean_ssim_registered, min_ssim_registered, max_ssim_registered,
                std_ssim_registered, mean_shift_magnitude, max_shift_magnitude, n_pairs
    - 2 枚未満で ValueError

テスト方針 (GUIDE_08_テスト方針.md):
  - pytest を使用
  - numpy で合成した画像を使用し，実画像に依存しない
"""

import numpy as np
import pytest

from src.analysis.reproducibility import calc_pairwise_ssim, calc_pairwise_ssim_registered


# ---------------------------------------------------------------------------
# ヘルパー
# ---------------------------------------------------------------------------


def _make_luminance(height: int = 64, width: int = 64, seed: int = 42) -> np.ndarray:
    """テスト用合成輝度マップ (float64, 0〜255 範囲) を生成する."""
    rng = np.random.default_rng(seed)
    return rng.uniform(100.0, 200.0, size=(height, width))


REQUIRED_REGISTERED_KEYS = {
    "pairwise_scores",
    "mean_ssim",
    "min_ssim",
    "max_ssim",
    "std_ssim",
    "mean_ssim_registered",
    "min_ssim_registered",
    "max_ssim_registered",
    "std_ssim_registered",
    "mean_shift_magnitude",
    "max_shift_magnitude",
    "n_pairs",
}

REQUIRED_PAIRWISE_SCORE_KEYS = {
    "image_a",
    "image_b",
    "ssim",
    "ssim_registered",
    "dx",
    "dy",
    "shift_magnitude",
}


# ---------------------------------------------------------------------------
# calc_pairwise_ssim テスト
# ---------------------------------------------------------------------------


class TestCalcPairwiseSsim:
    """calc_pairwise_ssim 関数のテスト群."""

    def test_identical_images_return_ssim_one(self) -> None:
        """正常: 同一画像 2 枚では SSIM = 1.0 が返ること."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img])
        assert result["mean_ssim"] == pytest.approx(1.0, abs=1e-6)

    def test_raises_value_error_for_single_image(self) -> None:
        """異常: 輝度マップが 1 枚の場合に ValueError が送出されること."""
        img = _make_luminance()
        with pytest.raises(ValueError):
            calc_pairwise_ssim([img])

    def test_raises_value_error_for_empty_list(self) -> None:
        """異常: 空リストを渡した場合に ValueError が送出されること."""
        with pytest.raises(ValueError):
            calc_pairwise_ssim([])

    def test_return_value_has_all_required_keys(self) -> None:
        """正常: 戻り値が全必須キーを持つこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img])
        expected_keys = {
            "pairwise_scores",
            "mean_ssim",
            "min_ssim",
            "max_ssim",
            "std_ssim",
            "n_pairs",
        }
        assert expected_keys.issubset(result.keys())

    def test_pairwise_scores_has_correct_entry_for_two_images(self) -> None:
        """正常: 2 枚の場合 pairwise_scores が 1 要素を持つこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img])
        assert result["n_pairs"] == 1
        assert len(result["pairwise_scores"]) == 1

    def test_pairwise_scores_entry_has_required_keys(self) -> None:
        """正常: pairwise_scores の各要素が image_a, image_b, ssim キーを持つこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img])
        pair = result["pairwise_scores"][0]
        assert "image_a" in pair
        assert "image_b" in pair
        assert "ssim" in pair

    def test_n_pairs_is_correct_for_three_images(self) -> None:
        """正常: 3 枚の場合 n_pairs = 3 (C(3,2)=3) であること."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img, img])
        assert result["n_pairs"] == 3

    def test_ssim_range_is_valid(self) -> None:
        """正常: SSIM が [-1, 1] の範囲内であること."""
        img_a = _make_luminance(seed=1)
        img_b = _make_luminance(seed=2)
        result = calc_pairwise_ssim([img_a, img_b])
        for pair in result["pairwise_scores"]:
            assert -1.0 <= pair["ssim"] <= 1.0

    def test_mean_ssim_is_average_of_pairwise_scores(self) -> None:
        """正常: mean_ssim が pairwise_scores の平均値と一致すること."""
        img_a = _make_luminance(seed=1)
        img_b = _make_luminance(seed=2)
        img_c = _make_luminance(seed=3)
        result = calc_pairwise_ssim([img_a, img_b, img_c])
        scores = [p["ssim"] for p in result["pairwise_scores"]]
        assert result["mean_ssim"] == pytest.approx(float(np.mean(scores)), abs=1e-10)

    def test_std_ssim_is_zero_for_identical_images(self) -> None:
        """正常: 全ペアが同一画像の場合 std_ssim = 0.0 であること."""
        img = _make_luminance()
        result = calc_pairwise_ssim([img, img, img])
        assert result["std_ssim"] == pytest.approx(0.0, abs=1e-10)


# ---------------------------------------------------------------------------
# calc_pairwise_ssim_registered テスト
# ---------------------------------------------------------------------------


class TestCalcPairwiseSsimRegistered:
    """calc_pairwise_ssim_registered 関数のテスト群."""

    def test_identical_images_return_ssim_one(self) -> None:
        """正常: 同一画像 2 枚では ssim = 1.0, ssim_registered = 1.0 が返ること."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img])
        assert result["mean_ssim"] == pytest.approx(1.0, abs=1e-6)
        assert result["mean_ssim_registered"] == pytest.approx(1.0, abs=1e-6)

    def test_identical_images_return_near_zero_shift(self) -> None:
        """正常: 同一画像 2 枚では shift_magnitude が 0 に近い値を返すこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img])
        assert result["mean_shift_magnitude"] == pytest.approx(0.0, abs=0.5)

    def test_raises_value_error_for_single_image(self) -> None:
        """異常: 輝度マップが 1 枚の場合に ValueError が送出されること."""
        img = _make_luminance()
        with pytest.raises(ValueError):
            calc_pairwise_ssim_registered([img])

    def test_raises_value_error_for_empty_list(self) -> None:
        """異常: 空リストを渡した場合に ValueError が送出されること."""
        with pytest.raises(ValueError):
            calc_pairwise_ssim_registered([])

    def test_return_value_has_all_required_keys(self) -> None:
        """正常: 戻り値が全必須キーを持つこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img])
        assert REQUIRED_REGISTERED_KEYS.issubset(result.keys())

    def test_pairwise_scores_entry_has_all_required_keys(self) -> None:
        """正常: pairwise_scores の各要素が全必須キーを持つこと."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img])
        pair = result["pairwise_scores"][0]
        assert REQUIRED_PAIRWISE_SCORE_KEYS.issubset(pair.keys())

    def test_n_pairs_is_correct_for_two_images(self) -> None:
        """正常: 2 枚の場合 n_pairs = 1 であること."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img])
        assert result["n_pairs"] == 1

    def test_n_pairs_is_correct_for_three_images(self) -> None:
        """正常: 3 枚の場合 n_pairs = 3 (C(3,2)=3) であること."""
        img = _make_luminance()
        result = calc_pairwise_ssim_registered([img, img, img])
        assert result["n_pairs"] == 3

    def test_shifted_image_pair_registered_ssim_not_worse(self) -> None:
        """正常: シフトした画像ペアでは ssim_registered >= ssim となること (位置合わせ後の改善).

        np.roll でシフトした場合，位相相関が正確にシフトを検出するので
        位置合わせ後の SSIM が位置合わせなしより改善されるか同等になること．
        """
        img = _make_luminance(64, 64)
        shifted = np.roll(np.roll(img, 3, axis=0), -5, axis=1)
        result = calc_pairwise_ssim_registered([img, shifted])
        pair = result["pairwise_scores"][0]
        # 位置合わせ後の SSIM は位置合わせなしより低下しないこと
        assert pair["ssim_registered"] >= pair["ssim"] - 0.01

    def test_shift_magnitude_is_nonnegative(self) -> None:
        """正常: shift_magnitude が 0 以上の値であること."""
        img_a = _make_luminance(seed=1)
        img_b = _make_luminance(seed=2)
        result = calc_pairwise_ssim_registered([img_a, img_b])
        for pair in result["pairwise_scores"]:
            assert pair["shift_magnitude"] >= 0.0

    def test_shift_magnitude_equals_euclidean_distance_of_dy_dx(self) -> None:
        """正常: shift_magnitude が sqrt(dy^2 + dx^2) と一致すること."""
        img = _make_luminance()
        shifted = np.roll(img, 3, axis=0)
        result = calc_pairwise_ssim_registered([img, shifted])
        pair = result["pairwise_scores"][0]
        expected_magnitude = float(np.sqrt(pair["dy"] ** 2 + pair["dx"] ** 2))
        assert pair["shift_magnitude"] == pytest.approx(expected_magnitude, abs=1e-10)

    def test_mean_ssim_registered_equals_average_of_pairwise(self) -> None:
        """正常: mean_ssim_registered が pairwise_scores の平均値と一致すること."""
        img_a = _make_luminance(seed=1)
        img_b = _make_luminance(seed=2)
        img_c = _make_luminance(seed=3)
        result = calc_pairwise_ssim_registered([img_a, img_b, img_c])
        scores_reg = [p["ssim_registered"] for p in result["pairwise_scores"]]
        assert result["mean_ssim_registered"] == pytest.approx(
            float(np.mean(scores_reg)), abs=1e-10
        )

    def test_max_shift_magnitude_is_max_of_pairwise(self) -> None:
        """正常: max_shift_magnitude が pairwise_scores の最大値と一致すること."""
        img_a = _make_luminance(seed=1)
        img_b = _make_luminance(seed=2)
        result = calc_pairwise_ssim_registered([img_a, img_b])
        shifts = [p["shift_magnitude"] for p in result["pairwise_scores"]]
        assert result["max_shift_magnitude"] == pytest.approx(max(shifts), abs=1e-10)

    def test_ssim_registered_values_are_finite(self) -> None:
        """正常: ssim_registered が有限値であること."""
        img_a = _make_luminance(seed=10)
        img_b = _make_luminance(seed=20)
        result = calc_pairwise_ssim_registered([img_a, img_b])
        for pair in result["pairwise_scores"]:
            assert np.isfinite(pair["ssim_registered"])

    def test_mean_ssim_equals_calc_pairwise_ssim_for_same_input(self) -> None:
        """正常: mean_ssim は位置合わせなし SSIM であり，calc_pairwise_ssim と同じ値になること."""
        img_a = _make_luminance(seed=5)
        img_b = _make_luminance(seed=6)
        result_plain = calc_pairwise_ssim([img_a, img_b])
        result_reg = calc_pairwise_ssim_registered([img_a, img_b])
        assert result_reg["mean_ssim"] == pytest.approx(result_plain["mean_ssim"], abs=1e-10)
