"""cli/render.py の仕様ベーステスト．

仕様出典:
- docs/03_PLAN/PLAN_02_開発ステップ.md（Step 5: render CLI 完了条件）
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md（CLI 層の責務）
- .claude/rules/testing.md（テスト方針）

Step 5 完了条件:
  `sm-render --scene <yaml> --output <dir>` で PNG と luminance.npy が生成されること

テスト方針:
  - CLI の振る舞いを main() を直接呼び出す形で検証する（subprocess 不使用）
  - Mitsuba 実行を伴うテストは @pytest.mark.slow を付ける
  - 軽量テスト（argparse / エラーハンドリング）は @pytest.mark.slow なし
"""

from __future__ import annotations

from pathlib import Path

import pytest

from sm_tias_lightsim.cli.render import main

# ---------------------------------------------------------------------------
# テスト用定数
# ---------------------------------------------------------------------------

#: tests/fixtures/minimal.yaml の絶対パス
MINIMAL_YAML = Path(__file__).parent.parent / "fixtures" / "minimal.yaml"


# ---------------------------------------------------------------------------
# 1. 必須引数の欠落（軽量 — argparse 動作）
# ---------------------------------------------------------------------------


def test_missing_scene_arg_exits_with_error():
    """--scene を渡さないと SystemExit が発生すること．

    仕様: --scene は必須引数（PLAN_02 Step 5「sm-render --scene <yaml>」）
    argparse の required=True は SystemExit を発生させる．
    """
    with pytest.raises(SystemExit) as exc_info:
        main(["--output", "/tmp/out"])
    assert exc_info.value.code != 0


def test_missing_output_arg_exits_with_error():
    """--output を渡さないと SystemExit が発生すること．

    仕様: --output は必須引数（PLAN_02 Step 5「--output <dir>」）
    argparse の required=True は SystemExit を発生させる．
    """
    with pytest.raises(SystemExit) as exc_info:
        main(["--scene", str(MINIMAL_YAML)])
    assert exc_info.value.code != 0


def test_no_args_exits_with_error():
    """引数なしで呼ぶと SystemExit が発生すること．

    仕様: --scene と --output の両方が必須
    """
    with pytest.raises(SystemExit) as exc_info:
        main([])
    assert exc_info.value.code != 0


# ---------------------------------------------------------------------------
# 2. --scene が存在しないパス（軽量 — return code 1, stderr メッセージ）
# ---------------------------------------------------------------------------


def test_nonexistent_scene_returns_1(tmp_path: Path, capsys):
    """存在しない YAML パスを渡すと return 1 かつ stderr にエラーメッセージが出ること．

    仕様: FileNotFoundError 捕捉 → stderr「シーン YAML ファイルが見つかりません」→ return 1
    (cli/render.py 処理フロー ステップ 2)
    """
    nonexistent = tmp_path / "does_not_exist.yaml"
    result = main(["--scene", str(nonexistent), "--output", str(tmp_path / "out")])
    assert result == 1
    captured = capsys.readouterr()
    assert "シーン YAML ファイルが見つかりません" in captured.err


def test_nonexistent_scene_error_message_contains_path(tmp_path: Path, capsys):
    """存在しない YAML パスを渡したときのエラーメッセージにパス情報が含まれること．

    仕様: stderr「エラー: シーン YAML ファイルが見つかりません — ...」
    (cli/render.py 処理フロー ステップ 2)
    """
    nonexistent = tmp_path / "no_such_scene.yaml"
    main(["--scene", str(nonexistent), "--output", str(tmp_path / "out")])
    captured = capsys.readouterr()
    assert "エラー:" in captured.err


# ---------------------------------------------------------------------------
# 3. --scene が不正な YAML（ValidationError 等）（軽量 — return code 1）
# ---------------------------------------------------------------------------


def test_invalid_yaml_returns_1(invalid_yaml_file: Path, tmp_path: Path, capsys):
    """必須フィールド欠落の YAML を渡すと return 1 が返ること．

    仕様: Pydantic ValidationError 等の一般例外 → stderr「シーン YAML の読み込みに失敗しました」
    → return 1（FileNotFoundError と区別される経路）
    (cli/render.py 処理フロー ステップ 2)
    """
    result = main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
    assert result == 1


def test_invalid_yaml_stderr_message(invalid_yaml_file: Path, tmp_path: Path, capsys):
    """不正 YAML のエラーメッセージに「シーン YAML の読み込みに失敗しました」が含まれること．

    仕様: 一般例外捕捉 → stderr「エラー: シーン YAML の読み込みに失敗しました — ...」
    (cli/render.py 処理フロー ステップ 2)
    """
    main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
    captured = capsys.readouterr()
    assert "シーン YAML の読み込みに失敗しました" in captured.err


def test_invalid_yaml_does_not_show_file_not_found_message(
    invalid_yaml_file: Path, tmp_path: Path, capsys
):
    """存在するが不正な YAML に対して「ファイルが見つかりません」メッセージが出ないこと．

    仕様: FileNotFoundError と Pydantic ValidationError の経路が明確に分離されていること
    (cli/render.py 処理フロー ステップ 2)
    """
    main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
    captured = capsys.readouterr()
    assert "ファイルが見つかりません" not in captured.err


# ---------------------------------------------------------------------------
# 4 & 5 & 6. end-to-end 統合テスト（slow — Mitsuba 実行）
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_end_to_end_returns_0(tmp_path: Path):
    """minimal.yaml を入力として main() が return 0 を返すこと．

    仕様: Step 5 完了条件「sm-render --scene tests/fixtures/minimal.yaml --output <dir> が動く」
    (PLAN_02 Step 5 完了条件)
    """
    result = main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
    assert result == 0


@pytest.mark.slow
def test_end_to_end_creates_render_png(tmp_path: Path):
    """end-to-end 実行後に render.png が出力ディレクトリに存在すること．

    仕様: sm-render は render.png を出力する (cli/render.py 処理フロー ステップ 5)
    """
    main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
    assert (tmp_path / "render.png").exists()


@pytest.mark.slow
def test_end_to_end_creates_luminance_npy(tmp_path: Path):
    """end-to-end 実行後に luminance.npy が出力ディレクトリに存在すること．

    仕様: sm-render は luminance.npy を出力する (cli/render.py 処理フロー ステップ 6)
    """
    main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
    assert (tmp_path / "luminance.npy").exists()


@pytest.mark.slow
def test_end_to_end_stdout_contains_render_complete(tmp_path: Path, capsys):
    """end-to-end 実行後に stdout に「レンダリング完了」が含まれること．

    仕様: 成功時 stdout に「レンダリング完了: ...」を出力する (cli/render.py 処理フロー ステップ 6)
    """
    main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
    captured = capsys.readouterr()
    assert "レンダリング完了" in captured.out


@pytest.mark.slow
def test_end_to_end_stdout_contains_luminance_saved(tmp_path: Path, capsys):
    """end-to-end 実行後に stdout に「輝度マップ保存」が含まれること．

    仕様: 成功時 stdout に「輝度マップ保存: ...」を出力する (cli/render.py 処理フロー ステップ 6)
    """
    main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
    captured = capsys.readouterr()
    assert "輝度マップ保存" in captured.out


# ---------------------------------------------------------------------------
# 5. 短縮引数 -s / -o（slow）
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_short_args_s_and_o_work(tmp_path: Path):
    """-s と -o の短縮形でも --scene / --output と同等に動くこと．

    仕様: --scene/-s, --output/-o の短縮形を提供する (cli/render.py _parse_args)
    """
    result = main(["-s", str(MINIMAL_YAML), "-o", str(tmp_path)])
    assert result == 0
    assert (tmp_path / "render.png").exists()
    assert (tmp_path / "luminance.npy").exists()


# ---------------------------------------------------------------------------
# 6. --output ディレクトリの自動作成（slow）
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_deep_nested_output_dir_is_created_automatically(tmp_path: Path):
    """存在しない深いネストのディレクトリも mkdir(parents=True) で自動作成されること．

    仕様: output_dir.mkdir(parents=True, exist_ok=True) で親ディレクトリも含めて作成する
    (cli/render.py 処理フロー ステップ 3)
    """
    deep_output = tmp_path / "deep" / "nested" / "out"
    assert not deep_output.exists()
    result = main(["--scene", str(MINIMAL_YAML), "--output", str(deep_output)])
    assert result == 0
    assert deep_output.exists()
    assert (deep_output / "render.png").exists()
    assert (deep_output / "luminance.npy").exists()
