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

仕様 (SPEC_01_アーキテクチャ設計.md):
  - export_results: 単一結果を CSV 出力
  - export_summary: バッチ結果を集約 CSV 出力
  - ensure_output_dirs: output/figures/ と output/results/ を作成

テスト方針 (GUIDE_08_テスト方針.md):
  - pytest を使用
  - tmp_path フィクスチャで一時ディレクトリを利用
"""

import csv
from pathlib import Path

import numpy as np
import pytest

from src.export.exporter import (
    SPATIAL_FIELDNAMES,
    SUMMARY_FIELDNAMES,
    calc_batch_statistics,
    ensure_output_dirs,
    export_batch_statistics,
    export_results,
    export_spatial_summary,
    export_summary,
)


# ---------------------------------------------------------------------------
# ensure_output_dirs テスト
# ---------------------------------------------------------------------------


class TestEnsureOutputDirs:
    """ensure_output_dirs 関数のテスト群."""

    def test_creates_figures_directory(self, tmp_path: Path) -> None:
        """正常: figures/ サブディレクトリが作成されること."""
        ensure_output_dirs(str(tmp_path))
        assert (tmp_path / "figures").is_dir()

    def test_creates_results_directory(self, tmp_path: Path) -> None:
        """正常: results/ サブディレクトリが作成されること."""
        ensure_output_dirs(str(tmp_path))
        assert (tmp_path / "results").is_dir()

    def test_idempotent_when_dirs_already_exist(self, tmp_path: Path) -> None:
        """正常: 既にディレクトリが存在する場合でもエラーにならないこと."""
        ensure_output_dirs(str(tmp_path))
        # 2 回目の呼び出しでも例外が発生しないこと
        ensure_output_dirs(str(tmp_path))
        assert (tmp_path / "figures").is_dir()
        assert (tmp_path / "results").is_dir()

    def test_creates_nested_base_directory(self, tmp_path: Path) -> None:
        """正常: base_path が存在しない場合でも自動作成されること."""
        nested_base = tmp_path / "new_output"
        ensure_output_dirs(str(nested_base))
        assert (nested_base / "figures").is_dir()
        assert (nested_base / "results").is_dir()


# ---------------------------------------------------------------------------
# export_results テスト
# ---------------------------------------------------------------------------


class TestExportResults:
    """export_results 関数のテスト群."""

    @pytest.fixture
    def sample_results(self) -> dict:
        """テスト用の均一性指標辞書を返す fixture."""
        return {
            "cov": 0.05,
            "std": 10.0,
            "max_min_ratio": 1.2,
            "mean": 200.0,
            "max": 220.0,
            "min": 180.0,
        }

    def test_csv_file_is_created(self, sample_results: dict, tmp_path: Path) -> None:
        """正常: CSV ファイルが生成されること."""
        output_path = str(tmp_path / "results.csv")
        export_results(sample_results, output_path)
        assert Path(output_path).exists()

    def test_csv_has_correct_header(self, sample_results: dict, tmp_path: Path) -> None:
        """正常: CSV の 1 行目にヘッダーが含まれること."""
        output_path = str(tmp_path / "results.csv")
        export_results(sample_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames
        assert set(fieldnames) == set(sample_results.keys())

    def test_csv_has_correct_values(self, sample_results: dict, tmp_path: Path) -> None:
        """正常: CSV の値が正しく書き込まれていること."""
        output_path = str(tmp_path / "results.csv")
        export_results(sample_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        assert len(rows) == 1
        row = rows[0]
        assert float(row["cov"]) == pytest.approx(0.05)
        assert float(row["std"]) == pytest.approx(10.0)
        assert float(row["mean"]) == pytest.approx(200.0)
        assert float(row["max"]) == pytest.approx(220.0)
        assert float(row["min"]) == pytest.approx(180.0)

    def test_creates_parent_directory_automatically(
        self, sample_results: dict, tmp_path: Path
    ) -> None:
        """正常: 出力ディレクトリが存在しない場合に自動作成されること."""
        nested_path = str(tmp_path / "results" / "uniformity.csv")
        export_results(sample_results, nested_path)
        assert Path(nested_path).exists()


# ---------------------------------------------------------------------------
# export_summary テスト
# ---------------------------------------------------------------------------


class TestExportSummary:
    """export_summary 関数のテスト群."""

    @pytest.fixture
    def sample_all_results(self) -> list[dict]:
        """テスト用の複数画像の均一性指標リストを返す fixture."""
        return [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
            },
            {
                "image_name": "image_002",
                "mean": 150.0,
                "std": 20.0,
                "cov": 0.133,
                "max_min_ratio": 1.5,
                "max": 200.0,
                "min": 100.0,
            },
        ]

    def test_csv_file_is_created(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV ファイルが生成されること."""
        output_path = str(tmp_path / "summary.csv")
        export_summary(sample_all_results, output_path)
        assert Path(output_path).exists()

    def test_csv_has_correct_summary_fieldnames(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV のヘッダーが SUMMARY_FIELDNAMES と一致すること."""
        output_path = str(tmp_path / "summary.csv")
        export_summary(sample_all_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames
        assert list(fieldnames) == SUMMARY_FIELDNAMES

    def test_csv_has_correct_row_count(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の行数が入力リストのサイズと一致すること."""
        output_path = str(tmp_path / "summary.csv")
        export_summary(sample_all_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        assert len(rows) == len(sample_all_results)

    def test_csv_first_row_values_are_correct(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の 1 行目の値が正しいこと."""
        output_path = str(tmp_path / "summary.csv")
        export_summary(sample_all_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        first = rows[0]
        assert first["image_name"] == "image_001"
        assert float(first["mean"]) == pytest.approx(200.0)
        assert float(first["std"]) == pytest.approx(10.0)

    def test_csv_second_row_values_are_correct(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の 2 行目の値が正しいこと."""
        output_path = str(tmp_path / "summary.csv")
        export_summary(sample_all_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        second = rows[1]
        assert second["image_name"] == "image_002"
        assert float(second["mean"]) == pytest.approx(150.0)

    def test_summary_fieldnames_constant_has_correct_keys(self) -> None:
        """正常: SUMMARY_FIELDNAMES 定数が期待されるキーを持つこと."""
        expected = ["image_name", "mean", "std", "cov", "max_min_ratio", "max", "min"]
        assert SUMMARY_FIELDNAMES == expected

    def test_creates_parent_directory_automatically(
        self, sample_all_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: 出力ディレクトリが存在しない場合に自動作成されること."""
        nested_path = str(tmp_path / "results" / "summary.csv")
        export_summary(sample_all_results, nested_path)
        assert Path(nested_path).exists()

    def test_empty_list_creates_header_only_csv(self, tmp_path: Path) -> None:
        """エッジケース: 空リストを渡した場合にヘッダーのみの CSV が生成されること."""
        output_path = str(tmp_path / "empty_summary.csv")
        export_summary([], output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        assert len(rows) == 0
        assert list(reader.fieldnames) == SUMMARY_FIELDNAMES


# ---------------------------------------------------------------------------
# export_spatial_summary テスト
# ---------------------------------------------------------------------------


class TestExportSpatialSummary:
    """export_spatial_summary 関数のテスト群."""

    @pytest.fixture
    def sample_spatial_results(self) -> list[dict]:
        """テスト用の空間解析結果リストを返す fixture."""
        return [
            {
                "image_name": "image_001",
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.29,
                "gradient_magnitude": 22.2,
            },
            {
                "image_name": "image_002",
                "center_mean": 200.0,
                "middle_mean": 190.0,
                "periphery_mean": 170.0,
                "center_periphery_ratio": 1.18,
                "gradient_magnitude": 15.0,
            },
        ]

    def test_csv_file_is_created(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV ファイルが生成されること."""
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, output_path)
        assert Path(output_path).exists()

    def test_csv_has_correct_spatial_fieldnames(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV のヘッダーが SPATIAL_FIELDNAMES と一致すること."""
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames
        assert list(fieldnames) == SPATIAL_FIELDNAMES

    def test_csv_has_correct_row_count(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の行数が入力リストのサイズと一致すること."""
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        assert len(rows) == len(sample_spatial_results)

    def test_csv_first_row_values_are_correct(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の 1 行目の値が正しいこと."""
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        first = rows[0]
        assert first["image_name"] == "image_001"
        assert float(first["center_mean"]) == pytest.approx(180.0)
        assert float(first["periphery_mean"]) == pytest.approx(140.0)
        assert float(first["center_periphery_ratio"]) == pytest.approx(1.29)

    def test_csv_second_row_values_are_correct(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: CSV の 2 行目の値が正しいこと."""
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        second = rows[1]
        assert second["image_name"] == "image_002"
        assert float(second["gradient_magnitude"]) == pytest.approx(15.0)

    def test_spatial_fieldnames_constant_has_correct_keys(self) -> None:
        """正常: SPATIAL_FIELDNAMES 定数が期待されるキーを持つこと."""
        expected = [
            "image_name",
            "center_mean",
            "middle_mean",
            "periphery_mean",
            "center_periphery_ratio",
            "gradient_magnitude",
        ]
        assert SPATIAL_FIELDNAMES == expected

    def test_creates_parent_directory_automatically(
        self, sample_spatial_results: list[dict], tmp_path: Path
    ) -> None:
        """正常: 出力ディレクトリが存在しない場合に自動作成されること."""
        nested_path = str(tmp_path / "results" / "spatial_summary.csv")
        export_spatial_summary(sample_spatial_results, nested_path)
        assert Path(nested_path).exists()

    def test_empty_list_creates_header_only_csv(self, tmp_path: Path) -> None:
        """エッジケース: 空リストを渡した場合にヘッダーのみの CSV が生成されること."""
        output_path = str(tmp_path / "empty_spatial.csv")
        export_spatial_summary([], output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        assert len(rows) == 0
        assert list(reader.fieldnames) == SPATIAL_FIELDNAMES

    def test_extra_keys_in_dict_are_ignored(self, tmp_path: Path) -> None:
        """正常: 余分なキーを含む辞書を渡しても CSV には SPATIAL_FIELDNAMES のみ出力されること."""
        results_with_extra = [
            {
                "image_name": "image_001",
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.29,
                "gradient_magnitude": 22.2,
                "extra_key": "should_be_ignored",
            }
        ]
        output_path = str(tmp_path / "spatial_summary.csv")
        export_spatial_summary(results_with_extra, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames
        assert list(fieldnames) == SPATIAL_FIELDNAMES


# ---------------------------------------------------------------------------
# calc_batch_statistics テスト
# ---------------------------------------------------------------------------


class TestCalcBatchStatistics:
    """calc_batch_statistics 関数のテスト群.

    仕様 (SPEC_02 バッチ解析 / 実装サマリー):
      - 入力: 均一性指標と空間解析結果を含む辞書リスト
      - 出力: {"uniformity": {...}, "spatial": {...}} の形式
      - uniformity 対象指標: mean, std, cov, max_min_ratio
      - spatial 対象指標: center_mean, middle_mean, periphery_mean,
                          center_periphery_ratio, gradient_magnitude
      - numpy 母集団標準偏差（ddof=0）を使用
      - cv = std / mean
    """

    @pytest.fixture
    def two_image_results(self) -> list[dict]:
        """テスト用の 2 画像分の解析結果リストを返す fixture."""
        return [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            },
            {
                "image_name": "image_002",
                "mean": 210.0,
                "std": 20.0,
                "cov": 0.095,
                "max_min_ratio": 1.4,
                "max": 240.0,
                "min": 160.0,
                "center_mean": 200.0,
                "middle_mean": 185.0,
                "periphery_mean": 160.0,
                "center_periphery_ratio": 1.25,
                "gradient_magnitude": 20.0,
            },
        ]

    def test_returns_dict_with_uniformity_and_spatial_keys(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: 戻り値が "uniformity" と "spatial" キーを持つ辞書であること."""
        result = calc_batch_statistics(two_image_results)
        assert isinstance(result, dict)
        assert "uniformity" in result
        assert "spatial" in result

    def test_uniformity_contains_exactly_four_metrics(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity 部に mean, std, cov, max_min_ratio の 4 指標のみが含まれること."""
        result = calc_batch_statistics(two_image_results)
        expected_metrics = {"mean", "std", "cov", "max_min_ratio"}
        assert expected_metrics == set(result["uniformity"].keys())

    def test_spatial_contains_exactly_five_metrics(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: spatial 部に 5 指標のみが含まれること."""
        result = calc_batch_statistics(two_image_results)
        expected_metrics = {
            "center_mean",
            "middle_mean",
            "periphery_mean",
            "center_periphery_ratio",
            "gradient_magnitude",
        }
        assert expected_metrics == set(result["spatial"].keys())

    def test_each_metric_has_five_stat_subkeys(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: 各指標が mean, std, min, max, cv の 5 つのサブキーを持つこと."""
        result = calc_batch_statistics(two_image_results)
        expected_subkeys = {"mean", "std", "min", "max", "cv"}
        for category in ("uniformity", "spatial"):
            for metric_name, metric_values in result[category].items():
                assert expected_subkeys == set(metric_values.keys()), (
                    f"{category}.{metric_name} のキーが期待と異なる"
                )

    def test_uniformity_mean_mean_is_arithmetic_mean(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.mean が算術平均と一致すること.

        (200.0 + 210.0) / 2 = 205.0
        """
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["mean"]["mean"] == pytest.approx(205.0)

    def test_uniformity_mean_std_uses_population_std_ddof0(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.std が母集団標準偏差（ddof=0）で算出されること.

        values = [200.0, 210.0]
        std(ddof=0) = sqrt(((200-205)^2 + (210-205)^2) / 2) = 5.0
        """
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["mean"]["std"] == pytest.approx(5.0)

    def test_uniformity_mean_std_differs_from_sample_std(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.std がサンプル標準偏差（ddof=1）と異なること.

        ddof=0 (母集団): 5.0
        ddof=1 (標本):  7.07... (2 画像の場合)
        """
        result = calc_batch_statistics(two_image_results)
        sample_std = float(np.std([200.0, 210.0], ddof=1))
        # ddof=0 の結果はサンプル標準偏差と一致しないはず
        assert result["uniformity"]["mean"]["std"] != pytest.approx(sample_std)

    def test_uniformity_mean_min_is_minimum_value(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.min が最小値と一致すること."""
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["mean"]["min"] == pytest.approx(200.0)

    def test_uniformity_mean_max_is_maximum_value(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.max が最大値と一致すること."""
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["mean"]["max"] == pytest.approx(210.0)

    def test_uniformity_mean_cv_is_std_divided_by_mean(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.mean.cv が std / mean と一致すること.

        std=5.0, mean=205.0 -> cv = 5.0 / 205.0
        """
        result = calc_batch_statistics(two_image_results)
        expected_cv = 5.0 / 205.0
        assert result["uniformity"]["mean"]["cv"] == pytest.approx(expected_cv)

    def test_uniformity_cov_mean_is_correct(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.cov.mean が (0.05 + 0.095) / 2 と一致すること."""
        result = calc_batch_statistics(two_image_results)
        expected = (0.05 + 0.095) / 2
        assert result["uniformity"]["cov"]["mean"] == pytest.approx(expected)

    def test_uniformity_max_min_ratio_min_is_correct(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.max_min_ratio.min が 1.2 と一致すること."""
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["max_min_ratio"]["min"] == pytest.approx(1.2)

    def test_uniformity_max_min_ratio_max_is_correct(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: uniformity.max_min_ratio.max が 1.4 と一致すること."""
        result = calc_batch_statistics(two_image_results)
        assert result["uniformity"]["max_min_ratio"]["max"] == pytest.approx(1.4)

    def test_spatial_center_mean_mean_is_correct(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: spatial.center_mean.mean が (180.0 + 200.0) / 2 = 190.0 と一致すること."""
        result = calc_batch_statistics(two_image_results)
        assert result["spatial"]["center_mean"]["mean"] == pytest.approx(190.0)

    def test_spatial_gradient_magnitude_std_is_correct(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: spatial.gradient_magnitude.std が母集団標準偏差と一致すること."""
        result = calc_batch_statistics(two_image_results)
        expected_std = float(np.std([22.2, 20.0], ddof=0))
        assert result["spatial"]["gradient_magnitude"]["std"] == pytest.approx(expected_std)

    def test_spatial_gradient_magnitude_cv_is_std_over_mean(
        self, two_image_results: list[dict]
    ) -> None:
        """正常: spatial.gradient_magnitude.cv が std / mean と一致すること."""
        result = calc_batch_statistics(two_image_results)
        values = [22.2, 20.0]
        expected_mean = float(np.mean(values))
        expected_std = float(np.std(values, ddof=0))
        expected_cv = expected_std / expected_mean
        assert result["spatial"]["gradient_magnitude"]["cv"] == pytest.approx(expected_cv)

    def test_all_stat_values_are_float_type(self, two_image_results: list[dict]) -> None:
        """正常: 全ての統計値が float 型であること."""
        result = calc_batch_statistics(two_image_results)
        for category in ("uniformity", "spatial"):
            for metric_name, metric_values in result[category].items():
                for stat_name, stat_value in metric_values.items():
                    assert isinstance(stat_value, float), (
                        f"{category}.{metric_name}.{stat_name} が float でない: {type(stat_value)}"
                    )

    def test_single_image_std_is_zero(self) -> None:
        """エッジケース: 画像が 1 枚の場合 std は 0.0 となること（母集団標準偏差 ddof=0）."""
        single = [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            }
        ]
        result = calc_batch_statistics(single)
        assert result["uniformity"]["mean"]["std"] == pytest.approx(0.0)

    def test_single_image_cv_is_zero_when_std_is_zero(self) -> None:
        """エッジケース: 画像が 1 枚の場合 cv は 0.0 となること（std=0 の場合）."""
        single = [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            }
        ]
        result = calc_batch_statistics(single)
        assert result["uniformity"]["mean"]["cv"] == pytest.approx(0.0)

    def test_single_image_min_equals_max(self) -> None:
        """エッジケース: 画像が 1 枚の場合 min と max は同一値となること."""
        single = [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            }
        ]
        result = calc_batch_statistics(single)
        assert result["uniformity"]["mean"]["min"] == pytest.approx(200.0)
        assert result["uniformity"]["mean"]["max"] == pytest.approx(200.0)

    def test_three_images_population_std_is_correct(self) -> None:
        """正常: 3 画像での std が母集団標準偏差（ddof=0）で正しく算出されること.

        values = [100, 200, 300]
        mean = 200, std(ddof=0) = sqrt(((100-200)^2 + (200-200)^2 + (300-200)^2) / 3)
                                 = sqrt(20000/3) = sqrt(6666.67) ≈ 81.65
        """
        three_images = [
            {
                "image_name": f"image_00{i}",
                "mean": m,
                "std": 5.0,
                "cov": 0.02,
                "max_min_ratio": 1.1,
                "max": m + 10,
                "min": m - 10,
                "center_mean": m,
                "middle_mean": m - 5.0,
                "periphery_mean": m - 10.0,
                "center_periphery_ratio": 1.1,
                "gradient_magnitude": 5.0,
            }
            for i, m in enumerate([100.0, 200.0, 300.0])
        ]
        result = calc_batch_statistics(three_images)
        expected_std = float(np.std([100.0, 200.0, 300.0], ddof=0))
        assert result["uniformity"]["mean"]["std"] == pytest.approx(expected_std)

    def test_uniformly_same_values_cv_is_zero(self) -> None:
        """エッジケース: 全画像で指標値が同一の場合 cv は 0.0 となること."""
        identical = [
            {
                "image_name": f"image_00{i}",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            }
            for i in range(3)
        ]
        result = calc_batch_statistics(identical)
        assert result["uniformity"]["mean"]["cv"] == pytest.approx(0.0)
        assert result["spatial"]["center_mean"]["cv"] == pytest.approx(0.0)


# ---------------------------------------------------------------------------
# export_batch_statistics テスト
# ---------------------------------------------------------------------------


class TestExportBatchStatistics:
    """export_batch_statistics 関数のテスト群.

    仕様 (実装サマリー):
      - 行: 各指標名，列: statistic, mean, std, min, max, cv
      - uniformity と spatial の全指標を 1 ファイルにまとめる
    """

    @pytest.fixture
    def sample_stats(self) -> dict:
        """テスト用の集計統計辞書を返す fixture."""
        return {
            "uniformity": {
                "mean": {"mean": 205.0, "std": 5.0, "min": 200.0, "max": 210.0, "cv": 0.0244},
                "std": {"mean": 15.0, "std": 5.0, "min": 10.0, "max": 20.0, "cv": 0.333},
                "cov": {"mean": 0.0725, "std": 0.0225, "min": 0.05, "max": 0.095, "cv": 0.310},
                "max_min_ratio": {"mean": 1.3, "std": 0.1, "min": 1.2, "max": 1.4, "cv": 0.077},
            },
            "spatial": {
                "center_mean": {"mean": 190.0, "std": 10.0, "min": 180.0, "max": 200.0, "cv": 0.053},
                "middle_mean": {"mean": 175.0, "std": 10.0, "min": 165.0, "max": 185.0, "cv": 0.057},
                "periphery_mean": {"mean": 150.0, "std": 10.0, "min": 140.0, "max": 160.0, "cv": 0.067},
                "center_periphery_ratio": {
                    "mean": 1.268, "std": 0.018, "min": 1.25, "max": 1.286, "cv": 0.014
                },
                "gradient_magnitude": {"mean": 21.1, "std": 1.1, "min": 20.0, "max": 22.2, "cv": 0.052},
            },
        }

    def test_csv_file_is_created(self, sample_stats: dict, tmp_path: Path) -> None:
        """正常: CSV ファイルが生成されること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)
        assert Path(output_path).exists()

    def test_csv_has_correct_header(self, sample_stats: dict, tmp_path: Path) -> None:
        """正常: CSV のヘッダーが statistic, mean, std, min, max, cv であること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            fieldnames = reader.fieldnames
        assert list(fieldnames) == ["statistic", "mean", "std", "min", "max", "cv"]

    def test_csv_row_count_equals_uniformity_plus_spatial(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: CSV の行数が uniformity 指標数 + spatial 指標数と一致すること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)
        expected_count = len(sample_stats["uniformity"]) + len(sample_stats["spatial"])
        assert len(rows) == expected_count

    def test_csv_statistic_column_contains_all_metric_names(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: statistic 列に全指標名（uniformity + spatial）が含まれること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            statistic_names = [row["statistic"] for row in reader]

        all_expected_metrics = list(sample_stats["uniformity"].keys()) + list(
            sample_stats["spatial"].keys()
        )
        assert set(statistic_names) == set(all_expected_metrics)

    def test_uniformity_mean_row_values_are_correct(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: uniformity の mean 指標行の全列値が正しく書き込まれていること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = {row["statistic"]: row for row in reader}

        mean_row = rows["mean"]
        assert float(mean_row["mean"]) == pytest.approx(205.0)
        assert float(mean_row["std"]) == pytest.approx(5.0)
        assert float(mean_row["min"]) == pytest.approx(200.0)
        assert float(mean_row["max"]) == pytest.approx(210.0)
        assert float(mean_row["cv"]) == pytest.approx(0.0244)

    def test_spatial_center_mean_row_values_are_correct(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: spatial の center_mean 指標行の全列値が正しく書き込まれていること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = {row["statistic"]: row for row in reader}

        cm_row = rows["center_mean"]
        assert float(cm_row["mean"]) == pytest.approx(190.0)
        assert float(cm_row["std"]) == pytest.approx(10.0)
        assert float(cm_row["min"]) == pytest.approx(180.0)
        assert float(cm_row["max"]) == pytest.approx(200.0)
        assert float(cm_row["cv"]) == pytest.approx(0.053)

    def test_spatial_gradient_magnitude_row_values_are_correct(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: spatial の gradient_magnitude 指標行の全列値が正しく書き込まれていること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = {row["statistic"]: row for row in reader}

        grad_row = rows["gradient_magnitude"]
        assert float(grad_row["mean"]) == pytest.approx(21.1)
        assert float(grad_row["std"]) == pytest.approx(1.1)
        assert float(grad_row["min"]) == pytest.approx(20.0)
        assert float(grad_row["max"]) == pytest.approx(22.2)
        assert float(grad_row["cv"]) == pytest.approx(0.052)

    def test_creates_parent_directory_automatically(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: 出力ディレクトリが存在しない場合に自動作成されること."""
        nested_path = str(tmp_path / "results" / "summary_statistics.csv")
        export_batch_statistics(sample_stats, nested_path)
        assert Path(nested_path).exists()

    def test_uniformity_metrics_appear_before_spatial_metrics(
        self, sample_stats: dict, tmp_path: Path
    ) -> None:
        """正常: CSV の先頭 4 行が uniformity 指標，後続 5 行が spatial 指標であること."""
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(sample_stats, output_path)

        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        uniformity_count = len(sample_stats["uniformity"])
        uniformity_names = set(sample_stats["uniformity"].keys())
        for row in rows[:uniformity_count]:
            assert row["statistic"] in uniformity_names, (
                f"先頭 {uniformity_count} 行に uniformity 以外の指標が含まれている: {row['statistic']}"
            )

        spatial_names = set(sample_stats["spatial"].keys())
        for row in rows[uniformity_count:]:
            assert row["statistic"] in spatial_names, (
                f"後続行に spatial 以外の指標が含まれている: {row['statistic']}"
            )

    def test_integration_calc_then_export(self, tmp_path: Path) -> None:
        """統合: calc_batch_statistics の結果を export_batch_statistics で出力できること."""
        all_results = [
            {
                "image_name": "image_001",
                "mean": 200.0,
                "std": 10.0,
                "cov": 0.05,
                "max_min_ratio": 1.2,
                "max": 220.0,
                "min": 180.0,
                "center_mean": 180.0,
                "middle_mean": 165.0,
                "periphery_mean": 140.0,
                "center_periphery_ratio": 1.286,
                "gradient_magnitude": 22.2,
            },
            {
                "image_name": "image_002",
                "mean": 210.0,
                "std": 20.0,
                "cov": 0.095,
                "max_min_ratio": 1.4,
                "max": 240.0,
                "min": 160.0,
                "center_mean": 200.0,
                "middle_mean": 185.0,
                "periphery_mean": 160.0,
                "center_periphery_ratio": 1.25,
                "gradient_magnitude": 20.0,
            },
        ]
        stats = calc_batch_statistics(all_results)
        output_path = str(tmp_path / "summary_statistics.csv")
        export_batch_statistics(stats, output_path)

        assert Path(output_path).exists()
        with open(output_path, encoding="utf-8") as f:
            reader = csv.DictReader(f)
            rows = list(reader)

        assert len(rows) == 9  # uniformity 4 + spatial 5
        statistic_names = {row["statistic"] for row in rows}
        assert "mean" in statistic_names
        assert "cov" in statistic_names
        assert "center_mean" in statistic_names
        assert "gradient_magnitude" in statistic_names
