"""cli/evaluate.py の _run_radial_min_max 関数と CLI 統合動作の仕様ベーステスト．

仕様出典:
- docs/04_SPEC/SPEC_04_評価仕様.md（動径指標・評価パイプライン）
- docs/04_SPEC/SPEC_02_シーン定義仕様.md（evaluation/metrics セクション）
- src/sm_tias_lightsim/cli/evaluate.py（_run_radial_min_max 実装）
- src/sm_tias_lightsim/evaluation/radial.py（calc_radial_min_max_ratio 実装）

テスト方針:
  - subprocess ではなく main() / _run_radial_min_max() を直接呼ぶ（速度・カバレッジのため）
  - ファイル I/O はモックせず tmp_path フィクスチャで実ファイルを使う (GUIDE_08)
  - 乱数を使う場合は seed を必ず固定する (GUIDE_08)
  - monkeypatch で calc_radial_profile の呼び出し有無を検証する

注意:
  matplotlib の Agg バックエンドは evaluate.py 内部で設定される．
  テスト側でも安全のため import 前に設定する．
"""

from __future__ import annotations

import matplotlib  # noqa: I001

# headless 環境でも動くよう Agg バックエンドを pyplot より前に設定する
matplotlib.use("Agg")

from pathlib import Path
from unittest.mock import patch

import numpy
import pytest

from sm_tias_lightsim.cli.evaluate import _run_radial_min_max, main
from sm_tias_lightsim.evaluation.radial import calc_radial_profile
from sm_tias_lightsim.scene.schema import MetricsConfig

# ---------------------------------------------------------------------------
# テスト用ヘルパー
# ---------------------------------------------------------------------------


def _make_luminance_array(seed: int = 0, size: int = 128) -> numpy.ndarray:
    """ゼロを含まない合成輝度配列を返す（seed 固定）．

    center が明るく periphery が暗い放射状勾配で radial_min_max が意味を持つ値になる．
    """
    rng = numpy.random.RandomState(seed)
    h, w = size, size
    ys, xs = numpy.mgrid[0:h, 0:w]
    cy, cx = h / 2.0, w / 2.0
    dist = numpy.sqrt(((xs - cx) / cx) ** 2 + ((ys - cy) / cy) ** 2)
    luminance = 100.0 - 30.0 * dist + rng.normal(0, 2.0, (h, w))
    # ゼロを避けるためクリップ
    return numpy.clip(luminance, 1.0, None).astype(numpy.float64)


def _make_eval_yaml_with_radial_min_max(
    tmp_path: Path,
    radial_min_max: bool = True,
    radial_profile: bool = True,
    x: int = 0,
    y: int = 0,
    width: int = 128,
    height: int = 128,
) -> Path:
    """radial_min_max フラグを含む評価 YAML を tmp_path に書き出す．"""
    content = f"""\
roi:
  type: image_pixel
  x: {x}
  y: {y}
  width: {width}
  height: {height}
zones:
  enabled: true
  boundaries: [0.33, 0.66]
metrics:
  cov: true
  std: true
  max_min: true
  radial_profile: {"true" if radial_profile else "false"}
  radial_min_max: {"true" if radial_min_max else "false"}
"""
    yaml_path = tmp_path / "eval_radial_min_max.yaml"
    yaml_path.write_text(content, encoding="utf-8")
    return yaml_path


def _run_main(npy_path: Path, config_path: Path, output_dir: Path) -> int:
    """main() を呼ぶ共通ラッパー（行長削減のため）．"""
    return main(
        ["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)]
    )


# ---------------------------------------------------------------------------
# B1. 無効時は空辞書: metrics_config is None で {} を返す
# ---------------------------------------------------------------------------


def test_run_radial_min_max_returns_empty_dict_when_metrics_config_is_none():
    """metrics_config が None のとき _run_radial_min_max が {} を返すこと．

    仕様: metrics_config is None → 無効扱いで空辞書を返す
    (cli/evaluate.py _run_radial_min_max)
    """
    roi_luminance = _make_luminance_array(seed=0)
    result = _run_radial_min_max(roi_luminance, None, None)
    assert result == {}


# ---------------------------------------------------------------------------
# B2. 無効時は空辞書（False）: radial_min_max=False で {} を返す
# ---------------------------------------------------------------------------


def test_run_radial_min_max_returns_empty_dict_when_radial_min_max_false():
    """MetricsConfig(radial_min_max=False) で _run_radial_min_max が {} を返すこと．

    仕様: metrics_config.radial_min_max=False → 無効扱いで空辞書を返す
    (cli/evaluate.py _run_radial_min_max)
    """
    roi_luminance = _make_luminance_array(seed=1)
    metrics_cfg = MetricsConfig(radial_min_max=False)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
    assert result == {}


def test_run_radial_min_max_returns_empty_dict_when_radial_min_max_default():
    """MetricsConfig() のデフォルト（radial_min_max=False）で {} を返すこと．

    仕様: MetricsConfig.radial_min_max の既定値 False → 無効扱い
    """
    roi_luminance = _make_luminance_array(seed=2)
    metrics_cfg = MetricsConfig()  # radial_min_max=False がデフォルト
    result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
    assert result == {}


# ---------------------------------------------------------------------------
# B3. 有効時は 4 キー辞書
# ---------------------------------------------------------------------------


def test_run_radial_min_max_returns_four_keys_when_enabled():
    """MetricsConfig(radial_min_max=True) + プロファイル提供で 4 キー辞書を返すこと．

    仕様: radial_min_max=True のとき
    radial_min_max_ratio / radial_min_distance / radial_min / radial_max
    の 4 キーを持つ辞書を返す（cli/evaluate.py _run_radial_min_max）
    """
    roi_luminance = _make_luminance_array(seed=3)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)

    assert "radial_min_max_ratio" in result
    assert "radial_min_distance" in result
    assert "radial_min" in result
    assert "radial_max" in result


def test_run_radial_min_max_ratio_in_valid_range():
    """radial_min_max_ratio が [0, 1] の範囲内であること（均一性指標として妥当）．

    仕様: calc_radial_min_max_ratio — 最小輝度 / 最大輝度（0〜1）
    (evaluation/radial.py calc_radial_min_max_ratio)
    """
    roi_luminance = _make_luminance_array(seed=4)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)

    ratio = result["radial_min_max_ratio"]
    assert 0.0 <= ratio <= 1.0


def test_run_radial_min_max_radial_min_lte_radial_max():
    """radial_min が radial_max 以下であること（min ≤ max）．

    仕様: radial_min = プロファイルの最小輝度値，radial_max = 最大輝度値
    (evaluation/radial.py calc_radial_min_max_ratio)
    """
    roi_luminance = _make_luminance_array(seed=5)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)

    assert result["radial_min"] <= result["radial_max"]


def test_run_radial_min_max_ratio_equals_min_over_max():
    """radial_min_max_ratio が radial_min / radial_max と一致すること．

    仕様: radial_min_max_ratio = 最小輝度 / 最大輝度
    (evaluation/radial.py calc_radial_min_max_ratio)
    """
    roi_luminance = _make_luminance_array(seed=6)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, profile)

    expected_ratio = result["radial_min"] / result["radial_max"]
    assert result["radial_min_max_ratio"] == pytest.approx(expected_ratio, rel=1e-10)


# ---------------------------------------------------------------------------
# B4. radial_profile 再利用: プロファイル提供時は calc_radial_profile を呼ばない
# ---------------------------------------------------------------------------


def test_run_radial_min_max_reuses_provided_profile_and_does_not_recalculate():
    """radial_profile 引数が None でない時は calc_radial_profile が呼ばれないこと．

    仕様: radial_profile が既に算出されていればそれを再利用し，重複算出を避ける
    (cli/evaluate.py _run_radial_min_max docstring)
    """
    roi_luminance = _make_luminance_array(seed=7)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)

    with patch("sm_tias_lightsim.cli.evaluate.calc_radial_profile") as mock_calc:
        _run_radial_min_max(roi_luminance, metrics_cfg, profile)
        mock_calc.assert_not_called()


def test_run_radial_min_max_reuses_profile_values_unchanged():
    """提供した radial_profile のデータが再利用されて結果が変わらないこと．

    仕様: radial_profile を再利用する — プロファイルの値は変更されない
    """
    roi_luminance = _make_luminance_array(seed=8)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)

    # 同じプロファイルを 2 回渡しても結果が同じ
    result1 = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
    result2 = _run_radial_min_max(roi_luminance, metrics_cfg, profile)

    assert result1["radial_min_max_ratio"] == pytest.approx(result2["radial_min_max_ratio"])
    assert result1["radial_min_distance"] == pytest.approx(result2["radial_min_distance"])


# ---------------------------------------------------------------------------
# B5. radial_profile 内部算出: None + radial_min_max=True で calc_radial_profile が呼ばれる
# ---------------------------------------------------------------------------


def test_run_radial_min_max_calls_calc_radial_profile_when_profile_is_none():
    """radial_profile=None かつ radial_min_max=True で calc_radial_profile が呼ばれること．

    仕様: radial_profile が None（未算出）の場合はその場で calc_radial_profile を呼び出す
    (cli/evaluate.py _run_radial_min_max)
    """
    roi_luminance = _make_luminance_array(seed=9)
    metrics_cfg = MetricsConfig(radial_min_max=True)

    real_profile = calc_radial_profile(roi_luminance)

    with patch(
        "sm_tias_lightsim.cli.evaluate.calc_radial_profile",
        return_value=real_profile,
    ) as mock_calc:
        result = _run_radial_min_max(roi_luminance, metrics_cfg, None)
        mock_calc.assert_called_once_with(roi_luminance)

    # 結果も正常に返ること
    assert "radial_min_max_ratio" in result


def test_run_radial_min_max_with_none_profile_returns_valid_result():
    """radial_profile=None で呼んでも有効な 4 キー辞書が返ること（内部算出）．

    仕様: 未算出の場合はここで算出する（重複算出を避けるため既存結果を優先）
    (cli/evaluate.py _run_radial_min_max)
    """
    roi_luminance = _make_luminance_array(seed=10)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    result = _run_radial_min_max(roi_luminance, metrics_cfg, None)

    assert "radial_min_max_ratio" in result
    assert "radial_min_distance" in result
    assert "radial_min" in result
    assert "radial_max" in result


def test_run_radial_min_max_none_profile_yields_same_result_as_provided_profile():
    """radial_profile=None の内部算出結果がプロファイル提供版と一致すること．

    仕様: 内部算出は calc_radial_profile(roi_luminance) を呼ぶので同じ結果になる
    """
    roi_luminance = _make_luminance_array(seed=11)
    metrics_cfg = MetricsConfig(radial_min_max=True)
    profile = calc_radial_profile(roi_luminance)

    result_with_profile = _run_radial_min_max(roi_luminance, metrics_cfg, profile)
    result_without_profile = _run_radial_min_max(roi_luminance, metrics_cfg, None)

    assert result_with_profile["radial_min_max_ratio"] == pytest.approx(
        result_without_profile["radial_min_max_ratio"], rel=1e-10
    )
    assert result_with_profile["radial_min_distance"] == pytest.approx(
        result_without_profile["radial_min_distance"], rel=1e-10
    )


# ---------------------------------------------------------------------------
# C1. 既存 config では radial_min_max_ratio 列が存在しない
# ---------------------------------------------------------------------------


def test_existing_config_without_radial_min_max_flag_no_column(tmp_path: Path):
    """radial_min_max を指定しない既存 config では CSV に radial_min_max_ratio 列が存在しないこと．

    仕様: radial_min_max 既定 False で既存 configs は無修正で動作する
    (cli/evaluate.py 実装サマリー「後方互換」)
    """
    import pandas

    luminance = _make_luminance_array(seed=100)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    # radial_min_max を含まない既存相当 YAML
    yaml_content = """\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 128
  height: 128
metrics:
  cov: true
  std: true
  max_min: true
  radial_profile: true
"""
    config_path = tmp_path / "eval_no_radial_min_max.yaml"
    config_path.write_text(yaml_content, encoding="utf-8")

    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0

    df = pandas.read_csv(output_dir / "metrics.csv")
    assert "radial_min_max_ratio" not in df.columns
    assert "radial_min_distance" not in df.columns
    assert "radial_min" not in df.columns
    assert "radial_max" not in df.columns


# ---------------------------------------------------------------------------
# C2. 新フラグで 4 列追加される
# ---------------------------------------------------------------------------


def test_radial_min_max_true_adds_four_columns_to_csv(tmp_path: Path):
    """metrics.radial_min_max: true を有効化すると CSV に 4 列が追加されること．

    仕様: 出力 CSV: 既存末尾に radial_min_max_ratio / radial_min_distance /
    radial_min / radial_max の 4 列追加
    (cli/evaluate.py 実装サマリー「出力 CSV」)
    """
    import pandas

    luminance = _make_luminance_array(seed=101)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0

    df = pandas.read_csv(output_dir / "metrics.csv")
    assert "radial_min_max_ratio" in df.columns
    assert "radial_min_distance" in df.columns
    assert "radial_min" in df.columns
    assert "radial_max" in df.columns


def test_radial_min_max_true_csv_values_are_float(tmp_path: Path):
    """radial_min_max 列の値が float であること．

    仕様: すべて float（metrics.csv カラム定義，SPEC_04）
    """
    import pandas

    luminance = _make_luminance_array(seed=102)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    df = pandas.read_csv(output_dir / "metrics.csv")
    # 型として float に変換可能であること
    ratio = float(df["radial_min_max_ratio"].iloc[0])
    dist = float(df["radial_min_distance"].iloc[0])
    rmin = float(df["radial_min"].iloc[0])
    rmax = float(df["radial_max"].iloc[0])

    # 値域チェック
    assert 0.0 <= ratio <= 1.0
    assert 0.0 <= dist <= 1.0
    assert rmin <= rmax


def test_radial_min_max_true_main_returns_0(tmp_path: Path):
    """radial_min_max: true を含む YAML で main() が 0 を返すこと．

    仕様: 正常終了時は 0 を返す（cli/evaluate.py main）
    """
    luminance = _make_luminance_array(seed=103)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0


# ---------------------------------------------------------------------------
# C3. CSV 列順序の後方互換
# ---------------------------------------------------------------------------


def test_radial_min_max_columns_appended_after_existing_columns(tmp_path: Path):
    """radial_min_max の 4 列が既存列（cov/std/max_min_ratio 等）の後に追加されること．

    仕様: 既存列（cov/std/max 等）の順序が変わらない（4 列は末尾に追加）
    (cli/evaluate.py 実装サマリー「出力 CSV」)
    """
    import pandas

    luminance = _make_luminance_array(seed=104)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    df = pandas.read_csv(output_dir / "metrics.csv")
    cols = list(df.columns)

    # 既存列が先頭側に存在すること
    expected_early_cols = ["cov", "std", "max_min_ratio", "mean", "max", "min"]
    for col in expected_early_cols:
        assert col in cols, f"列 {col} が CSV に存在しない"

    # radial_min_max_ratio の位置が cov より後ろであること
    cov_idx = cols.index("cov")
    ratio_idx = cols.index("radial_min_max_ratio")
    assert ratio_idx > cov_idx, "radial_min_max_ratio は cov より後ろにあるべき"


def test_existing_columns_order_unchanged_with_radial_min_max(tmp_path: Path):
    """radial_min_max 有効時と無効時で既存列の順序が変わらないこと．

    仕様: 既存列の順序が変わらない（後方互換）
    """
    import pandas

    luminance = _make_luminance_array(seed=105)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    # radial_min_max=False（既存相当）
    cfg_dir_no = tmp_path / "no"
    cfg_dir_no.mkdir(parents=True, exist_ok=True)
    config_no = _make_eval_yaml_with_radial_min_max(cfg_dir_no, radial_min_max=False)
    out_no = tmp_path / "output_no"
    _run_main(npy_path, config_no, out_no)
    df_no = pandas.read_csv(out_no / "metrics.csv")

    # radial_min_max=True
    cfg_dir_yes = tmp_path / "yes"
    cfg_dir_yes.mkdir(parents=True, exist_ok=True)
    config_yes = _make_eval_yaml_with_radial_min_max(cfg_dir_yes, radial_min_max=True)
    out_yes = tmp_path / "output_yes"
    _run_main(npy_path, config_yes, out_yes)
    df_yes = pandas.read_csv(out_yes / "metrics.csv")

    # 既存列は両方の CSV に存在し，df_no の列順序が df_yes でも保たれること
    for col in df_no.columns:
        assert col in df_yes.columns, f"既存列 {col} が radial_min_max=True の CSV に存在しない"

    # 既存列部分の順序が同じ
    existing_in_yes = [c for c in df_yes.columns if c in list(df_no.columns)]
    assert existing_in_yes == list(df_no.columns)


# ---------------------------------------------------------------------------
# C4. radial_profile: false + radial_min_max: true で独立動作
# ---------------------------------------------------------------------------


def test_radial_profile_false_with_radial_min_max_true_no_png(tmp_path: Path):
    """radial_profile: false かつ radial_min_max: true のとき
    radial_profile.png が生成されないこと．

    仕様: radial_min_max は radial_profile フラグとは独立して動作する
    radial_profile.png は metrics.radial_profile=True のときのみ生成される
    (cli/evaluate.py 実装サマリー「C4. radial_profile: false + radial_min_max: true」)
    """
    luminance = _make_luminance_array(seed=106)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(
        tmp_path, radial_min_max=True, radial_profile=False
    )
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0

    # radial_profile.png は生成されない
    assert not (output_dir / "figures" / "radial_profile.png").exists()


def test_radial_profile_false_with_radial_min_max_true_has_four_csv_columns(tmp_path: Path):
    """radial_profile: false かつ radial_min_max: true のとき CSV に 4 列が追加されること．

    仕様: radial_min_max は radial_profile フラグとは独立して動作する
    radial_profile.png がなくても 4 列は CSV に追加される
    """
    import pandas

    luminance = _make_luminance_array(seed=107)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(
        tmp_path, radial_min_max=True, radial_profile=False
    )
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    df = pandas.read_csv(output_dir / "metrics.csv")
    assert "radial_min_max_ratio" in df.columns
    assert "radial_min_distance" in df.columns
    assert "radial_min" in df.columns
    assert "radial_max" in df.columns


def test_radial_profile_true_with_radial_min_max_true_both_work(tmp_path: Path):
    """radial_profile: true かつ radial_min_max: true のとき PNG も 4 列も生成されること．

    仕様: radial_profile と radial_min_max は独立して有効化できる
    両方 True のとき radial_profile.png + 4 列が揃う
    """
    import pandas

    luminance = _make_luminance_array(seed=108)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(
        tmp_path, radial_min_max=True, radial_profile=True
    )
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0

    # radial_profile.png が生成される
    assert (output_dir / "figures" / "radial_profile.png").exists()

    # 4 列も存在する
    df = pandas.read_csv(output_dir / "metrics.csv")
    assert "radial_min_max_ratio" in df.columns


# ---------------------------------------------------------------------------
# C5. エラー処理: _run_radial_min_max 内で例外が出たとき main() が 1 を返す
# ---------------------------------------------------------------------------


def test_radial_min_max_exception_returns_1(tmp_path: Path):
    """_run_radial_min_max が例外を投げたとき main() が 1 を返すこと．

    仕様: 動径 min/max 比の算出に失敗したとき return 1
    (cli/evaluate.py main: except Exception → return 1)
    """
    luminance = _make_luminance_array(seed=109)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"

    with patch(
        "sm_tias_lightsim.cli.evaluate._run_radial_min_max",
        side_effect=RuntimeError("テスト用エラー"),
    ):
        result = _run_main(npy_path, config_path, output_dir)

    assert result == 1


def test_radial_min_max_exception_writes_stderr(tmp_path: Path, capsys):
    """_run_radial_min_max が例外を投げたとき stderr にエラーメッセージが出ること．

    仕様: 動径 min/max 比の算出に失敗しました — {exc} が stderr に出力される
    (cli/evaluate.py main)
    """
    luminance = _make_luminance_array(seed=110)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"

    with patch(
        "sm_tias_lightsim.cli.evaluate._run_radial_min_max",
        side_effect=RuntimeError("テスト用エラー"),
    ):
        main(["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)])

    captured = capsys.readouterr()
    assert "エラー" in captured.err


def test_radial_min_max_inner_exception_returns_1(tmp_path: Path):
    """calc_radial_min_max_ratio が例外を投げたとき main() が 1 を返すこと．

    仕様: 内部関数の例外もキャッチして return 1 になる
    (cli/evaluate.py main: except Exception → return 1)
    """
    luminance = _make_luminance_array(seed=111)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"

    with patch(
        "sm_tias_lightsim.cli.evaluate.calc_radial_min_max_ratio",
        side_effect=ValueError("テスト用内部エラー"),
    ):
        result = _run_main(npy_path, config_path, output_dir)

    assert result == 1


# ---------------------------------------------------------------------------
# コンソール出力の確認
# ---------------------------------------------------------------------------


def test_radial_min_max_summary_printed_when_enabled(tmp_path: Path, capsys):
    """radial_min_max: true のとき radial min/max と radial min dist がコンソールに出ること．

    仕様: コンソール出力: 代表 2 行（radial min/max, radial min dist）のみ追加
    (cli/evaluate.py _print_summary)
    """
    luminance = _make_luminance_array(seed=112)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=True)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    captured = capsys.readouterr()
    assert "radial min/max" in captured.out
    assert "radial min dist" in captured.out


def test_radial_min_max_summary_not_printed_when_disabled(tmp_path: Path, capsys):
    """radial_min_max: false のとき radial min/max がコンソールに出ないこと．

    仕様: radial_min_max_ratio がメトリクスにないとき _print_summary は出力しない
    (cli/evaluate.py _print_summary: if "radial_min_max_ratio" in metrics)
    """
    luminance = _make_luminance_array(seed=113)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    config_path = _make_eval_yaml_with_radial_min_max(tmp_path, radial_min_max=False)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    captured = capsys.readouterr()
    assert "radial min/max" not in captured.out


# ---------------------------------------------------------------------------
# D2. slow テスト（実 output/baseline/luminance.npy がある場合）
# ---------------------------------------------------------------------------

#: output/baseline/luminance.npy の絶対パス
BASELINE_LUMINANCE_NPY = (
    Path(__file__).parent.parent.parent / "output" / "baseline" / "luminance.npy"
)


@pytest.mark.slow
@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_radial_min_max_with_real_baseline_npy(tmp_path: Path):
    """実 output/baseline/luminance.npy で radial_min_max: true が動作すること．

    仕様: 動径 min/max 比 — 20 点輪帯平均の min/max で 1.0 で均一（SPEC_04 動径指標）
    """
    import pandas

    config_path = _make_eval_yaml_with_radial_min_max(
        tmp_path,
        radial_min_max=True,
        radial_profile=True,
        # baseline の ROI に合わせる
        x=24,
        y=37,
        width=464,
        height=450,
    )
    output_dir = tmp_path / "output"
    result = main(
        [
            "--image",
            str(BASELINE_LUMINANCE_NPY),
            "--config",
            str(config_path),
            "--output",
            str(output_dir),
        ]
    )
    assert result == 0

    df = pandas.read_csv(output_dir / "metrics.csv")
    assert "radial_min_max_ratio" in df.columns

    ratio = float(df["radial_min_max_ratio"].iloc[0])
    # シミュ linear の均一性は悪いため ratio は 1.0 より相当小さいはず
    # 存在確認として 0.0〜1.0 の範囲内であることのみチェック
    assert 0.0 < ratio <= 1.0
