"""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_ssim_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 定数が既存の基本キーをすべて含むこと.

        仕様変更（動径 min/max 比の追加）により新カラムが追加されるため，
        既存の 6 カラムが含まれていることを確認し，追加カラムを許容する．
        """
        base_fields = [
            "image_name",
            "center_mean",
            "middle_mean",
            "periphery_mean",
            "center_periphery_ratio",
            "gradient_magnitude",
        ]
        for field in base_fields:
            assert field in SPATIAL_FIELDNAMES, f"必須フィールド '{field}' が SPATIAL_FIELDNAMES に含まれていない"
        assert len(SPATIAL_FIELDNAMES) >= len(base_fields)

    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.

        仕様変更（動径 min/max 比の追加）に対応し radial_min_max_ratio / radial_min_distance を追加．
        """
        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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            },
            {
                "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,
                "radial_min_max_ratio": 0.80,
                "radial_min_distance": 0.90,
            },
        ]

    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 指標が含まれること.

        仕様変更（動径 min/max 比の追加）により spatial キーは 5 超になりうる．
        基本 5 指標の存在を確認し，追加指標を許容する形に修正する．
        """
        result = calc_batch_statistics(two_image_results)
        base_metrics = {
            "center_mean",
            "middle_mean",
            "periphery_mean",
            "center_periphery_ratio",
            "gradient_magnitude",
        }
        assert base_metrics.issubset(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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            }
        ]
        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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            }
        ]
        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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            }
        ]
        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,
                "radial_min_max_ratio": 0.8,
                "radial_min_distance": 0.9,
            }
            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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            }
            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 で出力できること.

        仕様変更（動径 min/max 比の追加）により行数は 9 以上になりうる．
        基本指標の存在を確認し，行数は >= 9 に修正する．
        """
        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,
                "radial_min_max_ratio": 0.75,
                "radial_min_distance": 0.95,
            },
            {
                "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,
                "radial_min_max_ratio": 0.80,
                "radial_min_distance": 0.90,
            },
        ]
        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)

        # uniformity 4 + spatial 5 以上（動径 min/max 比の追加で 7 になりうる）
        assert len(rows) >= 9
        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


# ---------------------------------------------------------------------------
# export_ssim_summary テスト
# ---------------------------------------------------------------------------


class TestExportSsimSummary:
    """export_ssim_summary 関数のテスト群.

    仕様 (SPEC_02_照明均一性評価アルゴリズム.md — 「再現性評価」節 / exporter.py docstring):
      - 1 行目: 集計統計ヘッダー (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)
      - 空行
      - ペアワイズスコア (image_a, image_b, ssim, ssim_registered, dx, dy, shift_magnitude)
      - 後方互換: 位置合わせキーなしの入力でもエラーにならない
    """

    @pytest.fixture
    def registered_ssim_results(self) -> dict:
        """位置合わせ付き SSIM 結果の fixture (calc_pairwise_ssim_registered の戻り値形式)."""
        return {
            "pairwise_scores": [
                {
                    "image_a": 0,
                    "image_b": 1,
                    "ssim": 0.911,
                    "ssim_registered": 0.985,
                    "dx": 1.2,
                    "dy": -0.5,
                    "shift_magnitude": 1.3,
                },
                {
                    "image_a": 0,
                    "image_b": 2,
                    "ssim": 0.920,
                    "ssim_registered": 0.970,
                    "dx": 0.8,
                    "dy": 0.3,
                    "shift_magnitude": 0.85,
                },
            ],
            "mean_ssim": 0.9155,
            "min_ssim": 0.911,
            "max_ssim": 0.920,
            "std_ssim": 0.0045,
            "mean_ssim_registered": 0.9775,
            "min_ssim_registered": 0.970,
            "max_ssim_registered": 0.985,
            "std_ssim_registered": 0.0075,
            "mean_shift_magnitude": 1.075,
            "max_shift_magnitude": 1.3,
            "n_pairs": 2,
        }

    @pytest.fixture
    def plain_ssim_results(self) -> dict:
        """位置合わせなし SSIM 結果の fixture (calc_pairwise_ssim の戻り値形式)."""
        return {
            "pairwise_scores": [
                {"image_a": 0, "image_b": 1, "ssim": 0.995},
            ],
            "mean_ssim": 0.995,
            "min_ssim": 0.995,
            "max_ssim": 0.995,
            "std_ssim": 0.0,
            "n_pairs": 1,
        }

    @pytest.fixture
    def image_names(self) -> list[str]:
        """テスト用画像名リストを返す fixture."""
        return ["image_001", "image_002", "image_003"]

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

    def test_summary_row_contains_mean_ssim(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行に mean_ssim が正しく書き込まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        assert float(summary_row["mean_ssim"]) == pytest.approx(0.9155)

    def test_summary_row_contains_mean_ssim_registered(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行に mean_ssim_registered が正しく書き込まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        assert float(summary_row["mean_ssim_registered"]) == pytest.approx(0.9775)

    def test_summary_row_contains_mean_shift_magnitude(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行に mean_shift_magnitude が正しく書き込まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        assert float(summary_row["mean_shift_magnitude"]) == pytest.approx(1.075)

    def test_summary_row_contains_max_shift_magnitude(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行に max_shift_magnitude が正しく書き込まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        assert float(summary_row["max_shift_magnitude"]) == pytest.approx(1.3)

    def test_pairwise_rows_have_ssim_registered_column(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: ペアワイズスコア行に ssim_registered 列が含まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

        # ファイルを直接読み込み，ペアワイズセクション（空行の後）を解析
        with open(output_path, encoding="utf-8") as f:
            content = f.read()

        lines = content.strip().split("\n")
        # 空行でセクション分割
        empty_line_idx = next(i for i, line in enumerate(lines) if line.strip() == "")
        pair_section = "\n".join(lines[empty_line_idx + 1 :])

        import io
        pair_reader = csv.DictReader(io.StringIO(pair_section))
        pair_rows = list(pair_reader)

        assert len(pair_rows) == 2
        assert "ssim_registered" in pair_rows[0]
        assert float(pair_rows[0]["ssim_registered"]) == pytest.approx(0.985)

    def test_pairwise_rows_have_dx_dy_shift_magnitude_columns(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: ペアワイズスコア行に dx, dy, shift_magnitude 列が含まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

        with open(output_path, encoding="utf-8") as f:
            content = f.read()

        lines = content.strip().split("\n")
        empty_line_idx = next(i for i, line in enumerate(lines) if line.strip() == "")
        pair_section = "\n".join(lines[empty_line_idx + 1 :])

        import io
        pair_reader = csv.DictReader(io.StringIO(pair_section))
        pair_rows = list(pair_reader)

        first = pair_rows[0]
        assert "dx" in first
        assert "dy" in first
        assert "shift_magnitude" in first
        assert float(first["dx"]) == pytest.approx(1.2)
        assert float(first["dy"]) == pytest.approx(-0.5)
        assert float(first["shift_magnitude"]) == pytest.approx(1.3)

    def test_pairwise_rows_use_image_names_not_indices(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: ペアワイズスコア行の image_a, image_b が画像名（インデックスではなく）になること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

        with open(output_path, encoding="utf-8") as f:
            content = f.read()

        lines = content.strip().split("\n")
        empty_line_idx = next(i for i, line in enumerate(lines) if line.strip() == "")
        pair_section = "\n".join(lines[empty_line_idx + 1 :])

        import io
        pair_reader = csv.DictReader(io.StringIO(pair_section))
        pair_rows = list(pair_reader)

        assert pair_rows[0]["image_a"] == "image_001"
        assert pair_rows[0]["image_b"] == "image_002"

    def test_backward_compatible_with_plain_ssim_results(
        self,
        plain_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """後方互換: 位置合わせキーなし（calc_pairwise_ssim の戻り値）でもエラーにならないこと."""
        output_path = str(tmp_path / "ssim_plain_summary.csv")
        # 例外が発生しないこと
        export_ssim_summary(plain_ssim_results, image_names, output_path)
        assert Path(output_path).exists()

    def test_backward_compatible_registered_fields_are_empty_for_plain_results(
        self,
        plain_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """後方互換: 位置合わせキーなしの場合，登録済み列は空文字列になること."""
        output_path = str(tmp_path / "ssim_plain_summary.csv")
        export_ssim_summary(plain_ssim_results, image_names, output_path)

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

        # 位置合わせキーなしの場合は空文字列
        assert summary_row["mean_ssim_registered"] == ""
        assert summary_row["mean_shift_magnitude"] == ""
        assert summary_row["max_shift_magnitude"] == ""

    def test_backward_compatible_pair_registered_fields_empty_for_plain_results(
        self,
        plain_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """後方互換: 位置合わせキーなしのペアデータでは ssim_registered 等が空文字列になること."""
        output_path = str(tmp_path / "ssim_plain_summary.csv")
        export_ssim_summary(plain_ssim_results, image_names, output_path)

        with open(output_path, encoding="utf-8") as f:
            content = f.read()

        lines = content.strip().split("\n")
        empty_line_idx = next(i for i, line in enumerate(lines) if line.strip() == "")
        pair_section = "\n".join(lines[empty_line_idx + 1 :])

        import io
        pair_reader = csv.DictReader(io.StringIO(pair_section))
        pair_rows = list(pair_reader)

        assert pair_rows[0]["ssim_registered"] == ""
        assert pair_rows[0]["dx"] == ""
        assert pair_rows[0]["dy"] == ""
        assert pair_rows[0]["shift_magnitude"] == ""

    def test_summary_fieldnames_include_all_registered_keys(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行のヘッダーが全必須キーを持つこと."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        expected = {
            "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",
        }
        assert expected.issubset(fieldnames)

    def test_n_pairs_in_summary_row_is_correct(
        self,
        registered_ssim_results: dict,
        image_names: list[str],
        tmp_path: Path,
    ) -> None:
        """正常: 集計統計行の n_pairs が正しく書き込まれること."""
        output_path = str(tmp_path / "ssim_summary.csv")
        export_ssim_summary(registered_ssim_results, image_names, output_path)

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

        assert int(summary_row["n_pairs"]) == 2

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