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

仕様 (SPEC_01_アーキテクチャ設計.md):
  - to_grayscale: RGB 画像を Rec.709 輝度係数でグレースケールに変換する
  - calc_uniformity: CoV, std, max_min_ratio, mean, max, min を算出する

テスト方針 (GUIDE_08_テスト方針.md):
  - pytest を使用
  - numpy で合成した画像を使用
  - 既知の値で期待値と比較
"""

import numpy
import pytest

from src.analysis.uniformity import (
    REC709_COEFF_B,
    REC709_COEFF_G,
    REC709_COEFF_R,
    calc_uniformity,
    to_grayscale,
)


# ---------------------------------------------------------------------------
# to_grayscale テスト
# ---------------------------------------------------------------------------


class TestToGrayscale:
    """to_grayscale 関数のテスト群."""

    def test_uniform_white_image_returns_255(self) -> None:
        """正常: 全ピクセル [255, 255, 255] の画像は全ピクセル 255.0 を返すこと."""
        image = numpy.full((8, 8, 3), 255, dtype=numpy.uint8)
        result = to_grayscale(image)
        # Rec.709 係数の和が 1.0 なので、全チャンネル 255 なら輝度も 255
        expected = (REC709_COEFF_R + REC709_COEFF_G + REC709_COEFF_B) * 255.0
        assert numpy.all(numpy.isclose(result, expected))

    def test_uniform_black_image_returns_zero(self) -> None:
        """正常: 全ピクセル [0, 0, 0] の画像は全ピクセル 0.0 を返すこと."""
        image = numpy.zeros((8, 8, 3), dtype=numpy.uint8)
        result = to_grayscale(image)
        assert numpy.all(result == 0.0)

    def test_red_only_image(self) -> None:
        """正常: 赤のみ画像 [255, 0, 0] は 0.2126 * 255 = 54.213 を返すこと."""
        image = numpy.zeros((4, 4, 3), dtype=numpy.uint8)
        image[:, :, 0] = 255  # R チャンネルのみ 255
        result = to_grayscale(image)
        expected = REC709_COEFF_R * 255.0
        assert numpy.all(numpy.isclose(result, expected))

    def test_green_only_image(self) -> None:
        """正常: 緑のみ画像 [0, 255, 0] は 0.7152 * 255 = 182.376 を返すこと."""
        image = numpy.zeros((4, 4, 3), dtype=numpy.uint8)
        image[:, :, 1] = 255  # G チャンネルのみ 255
        result = to_grayscale(image)
        expected = REC709_COEFF_G * 255.0
        assert numpy.all(numpy.isclose(result, expected))

    def test_blue_only_image(self) -> None:
        """正常: 青のみ画像 [0, 0, 255] は 0.0722 * 255 = 18.411 を返すこと."""
        image = numpy.zeros((4, 4, 3), dtype=numpy.uint8)
        image[:, :, 2] = 255  # B チャンネルのみ 255
        result = to_grayscale(image)
        expected = REC709_COEFF_B * 255.0
        assert numpy.all(numpy.isclose(result, expected))

    def test_output_shape_is_hw(self) -> None:
        """正常: 戻り値の shape が (H, W) であること."""
        image = numpy.zeros((6, 10, 3), dtype=numpy.uint8)
        result = to_grayscale(image)
        assert result.shape == (6, 10)

    def test_output_dtype_is_float64(self) -> None:
        """正常: 戻り値の dtype が float64 であること."""
        image = numpy.full((4, 4, 3), 128, dtype=numpy.uint8)
        result = to_grayscale(image)
        assert result.dtype == numpy.float64

    def test_rec709_coefficients_applied_correctly(self) -> None:
        """正常: Rec.709 係数が正しく適用されること（既知の RGB 値で検証）."""
        # 1x1 画像で R=100, G=150, B=200 のピクセル
        image = numpy.array([[[100, 150, 200]]], dtype=numpy.uint8)
        result = to_grayscale(image)
        expected = REC709_COEFF_R * 100 + REC709_COEFF_G * 150 + REC709_COEFF_B * 200
        assert numpy.isclose(result[0, 0], expected)

    def test_non_uniform_image_preserves_spatial_values(self) -> None:
        """正常: 非均一画像で各ピクセルの輝度値が独立して計算されること."""
        image = numpy.zeros((2, 2, 3), dtype=numpy.uint8)
        image[0, 0] = [255, 0, 0]  # 左上: 赤
        image[0, 1] = [0, 255, 0]  # 右上: 緑
        image[1, 0] = [0, 0, 255]  # 左下: 青
        image[1, 1] = [255, 255, 255]  # 右下: 白

        result = to_grayscale(image)

        assert numpy.isclose(result[0, 0], REC709_COEFF_R * 255.0)
        assert numpy.isclose(result[0, 1], REC709_COEFF_G * 255.0)
        assert numpy.isclose(result[1, 0], REC709_COEFF_B * 255.0)
        assert numpy.isclose(
            result[1, 1], (REC709_COEFF_R + REC709_COEFF_G + REC709_COEFF_B) * 255.0
        )


# ---------------------------------------------------------------------------
# calc_uniformity テスト
# ---------------------------------------------------------------------------


class TestCalcUniformity:
    """calc_uniformity 関数のテスト群."""

    def test_uniform_luminance_returns_zero_cov(self) -> None:
        """正常: 均一輝度（全ピクセル 200.0）は cov=0.0, std=0.0, max_min_ratio=1.0 を返すこと."""
        luminance = numpy.full((8, 8), 200.0, dtype=numpy.float64)
        result = calc_uniformity(luminance)
        assert numpy.isclose(result["cov"], 0.0)
        assert numpy.isclose(result["std"], 0.0)
        assert numpy.isclose(result["max_min_ratio"], 1.0)
        assert numpy.isclose(result["mean"], 200.0)

    def test_known_values_cov(self) -> None:
        """正常: 既知の輝度配列で CoV が正しく計算されること."""
        # 値が 100 と 200 の 2 要素: mean=150, std=50, cov=50/150
        luminance = numpy.array([[100.0, 200.0]], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        mean = 150.0
        std = float(numpy.std(numpy.array([100.0, 200.0])))
        expected_cov = std / mean
        assert numpy.isclose(result["cov"], expected_cov)

    def test_known_values_max_min_ratio(self) -> None:
        """正常: 既知の輝度配列で max_min_ratio が正しく計算されること."""
        luminance = numpy.array([[50.0, 100.0, 200.0]], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        assert numpy.isclose(result["max_min_ratio"], 200.0 / 50.0)

    def test_raises_value_error_when_lmin_is_zero(self) -> None:
        """異常: lmin==0 の場合に ValueError が送出されること."""
        luminance = numpy.array([[0.0, 100.0, 200.0]], dtype=numpy.float64)
        with pytest.raises(ValueError):
            calc_uniformity(luminance)

    def test_raises_value_error_for_all_zero_image(self) -> None:
        """異常: 全ゼロ画像で ValueError が送出されること."""
        luminance = numpy.zeros((8, 8), dtype=numpy.float64)
        with pytest.raises(ValueError):
            calc_uniformity(luminance)

    def test_return_value_has_all_required_keys(self) -> None:
        """正常: 戻り値のキーが全て存在すること（cov, std, max_min_ratio, mean, max, min）."""
        luminance = numpy.full((4, 4), 128.0, dtype=numpy.float64)
        result = calc_uniformity(luminance)
        expected_keys = {"cov", "std", "max_min_ratio", "mean", "max", "min"}
        assert set(result.keys()) == expected_keys

    def test_mean_value_is_correct(self) -> None:
        """正常: mean が正しく計算されること."""
        luminance = numpy.array([[100.0, 200.0, 300.0]], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        assert numpy.isclose(result["mean"], 200.0)

    def test_max_and_min_values_are_correct(self) -> None:
        """正常: max と min が正しく返ること."""
        luminance = numpy.array([[50.0, 150.0, 250.0]], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        assert numpy.isclose(result["max"], 250.0)
        assert numpy.isclose(result["min"], 50.0)

    def test_std_value_is_correct(self) -> None:
        """正常: std が numpy.std と一致すること."""
        values = [80.0, 120.0, 160.0, 200.0]
        luminance = numpy.array([values], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        expected_std = float(numpy.std(numpy.array(values)))
        assert numpy.isclose(result["std"], expected_std)

    def test_cov_is_inf_when_mean_is_zero(self) -> None:
        """エッジケース: mean=0 の場合に cov が inf となること.

        ただし lmin=0 のため ValueError が先に発生するはず。
        mean=0 かつ全値が 0 でない状況（負の値）は float64 では可能だが
        輝度の性質上このケースは仕様外のため、実装の副作用確認のみ。
        """
        # 負の輝度は現実には存在しないが、mean=0 のコードパスを検証
        # lmin が 0 以外になるケースのみテスト可能
        luminance = numpy.array([[1.0, -1.0, 2.0, -2.0]], dtype=numpy.float64)
        # mean = 0.0 となるが lmin = -2.0 != 0.0 なので ValueError は発生しない
        result = calc_uniformity(luminance)
        assert result["cov"] == float("inf")

    def test_uniform_luminance_mean_equals_value(self) -> None:
        """正常: 均一輝度の mean が設定値と等しいこと（複数値で確認）."""
        for val in [50.0, 100.0, 200.0, 255.0]:
            luminance = numpy.full((4, 4), val, dtype=numpy.float64)
            result = calc_uniformity(luminance)
            assert numpy.isclose(result["mean"], val)

    def test_return_values_are_float(self) -> None:
        """正常: 戻り値の各フィールドが Python float 型であること."""
        luminance = numpy.array([[100.0, 200.0]], dtype=numpy.float64)
        result = calc_uniformity(luminance)
        for key in ["cov", "std", "max_min_ratio", "mean", "max", "min"]:
            assert isinstance(result[key], float), f"{key} が float でない"
