"""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 pytest

from src.export.exporter import (
    SPATIAL_FIELDNAMES,
    SUMMARY_FIELDNAMES,
    ensure_output_dirs,
    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_creates_reports_directory(self, tmp_path: Path) -> None:
        """正常: reports/ サブディレクトリが作成されること."""
        ensure_output_dirs(str(tmp_path))
        assert (tmp_path / "reports").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()
        assert (tmp_path / "reports").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()
        assert (nested_base / "reports").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
