"""viewer.py / cli/visualize.py の新引数仕様ベーステスト (Phase 2).

仕様出典:
- src/sm_tias_lightsim/visualize/viewer.py (show_scene docstring)
    cull_outward: bool = True (既定 ON が新仕様)
    hide_sensor_marker: bool = False
    hide_led_markers: bool = False
- src/sm_tias_lightsim/cli/visualize.py (_parse_args)
    --bake-max-depth N (既定 16)
    --cull-outward / --no-cull-outward
    --no-sensor-marker / --no-led-markers / --no-markers (OR 論理)

テスト方針(testing.md):
    - show_scene の引数シグネチャ・デフォルト値は Mitsuba 不要で純関数的に確認
    - CLI 引数パーズは _parse_args() で直接確認
    - 実描画テスト（smoke）は @pytest.mark.slow
    - 仕様変更（cull_outward 既定 True）で更新した既存テストは所見欄に明記

## 仕様変更注記 (cull_outward 既定 True)
既存 test_viewer.py の show_scene 呼び出しは cull_outward のデフォルト変更により
内部のカリング処理が走るようになった（既定 ON）。これは仕様変更による期待値更新であり
実装バグではない。既存の smoke テスト（ファイル生成確認）は動作に影響しない（PNG は生成される）。
"""

from __future__ import annotations

import inspect
from pathlib import Path

import pytest

from sm_tias_lightsim.visualize.viewer import show_scene

# ===========================================================================
# 1. show_scene シグネチャの後方互換
# ===========================================================================


class TestShowSceneSignatureCompatibility:
    """show_scene の新引数シグネチャ・デフォルト値の仕様テスト (Mitsuba 不要).

    仕様（viewer.py show_scene）:
        cull_outward: bool = True
        hide_sensor_marker: bool = False
        hide_led_markers: bool = False
    """

    def test_cull_outward_param_exists_with_default_true(self):
        """cull_outward 引数が存在し、デフォルト True であること.

        仕様: cull_outward: bool = True（既定 ON が新仕様）
        """
        sig = inspect.signature(show_scene)
        param = sig.parameters.get("cull_outward")
        assert param is not None, "show_scene に cull_outward 引数が存在すること"
        assert param.default is True, (
            f"cull_outward のデフォルトが True であること（新仕様）: {param.default}"
        )

    def test_hide_sensor_marker_param_exists_with_default_false(self):
        """hide_sensor_marker 引数が存在し、デフォルト False であること.

        仕様: hide_sensor_marker: bool = False（後方互換）
        """
        sig = inspect.signature(show_scene)
        param = sig.parameters.get("hide_sensor_marker")
        assert param is not None, "show_scene に hide_sensor_marker 引数が存在すること"
        assert param.default is False, (
            f"hide_sensor_marker のデフォルトが False であること: {param.default}"
        )

    def test_hide_led_markers_param_exists_with_default_false(self):
        """hide_led_markers 引数が存在し、デフォルト False であること.

        仕様: hide_led_markers: bool = False（後方互換）
        """
        sig = inspect.signature(show_scene)
        param = sig.parameters.get("hide_led_markers")
        assert param is not None, "show_scene に hide_led_markers 引数が存在すること"
        assert param.default is False, (
            f"hide_led_markers のデフォルトが False であること: {param.default}"
        )

    def test_existing_params_are_preserved(self):
        """既存引数（irradiance, luminance_map, off_screen 等）が維持されていること（後方互換）.

        仕様: 新 keyword 引数デフォルトで既存 show_scene 呼び出しが動く.
        """
        sig = inspect.signature(show_scene)
        existing_params = [
            "scene_config", "project_root", "irradiance", "luminance_map",
            "off_screen", "screenshot", "clip_low", "clip_high",
            "bake_opacity", "hide_below", "bake_smooth", "bake_scale",
        ]
        for name in existing_params:
            assert name in sig.parameters, (
                f"既存引数 '{name}' が show_scene に存在すること（後方互換）"
            )


# ===========================================================================
# 2. CLI 新引数のパース仕様テスト
# ===========================================================================


class TestCliNewArgParsing:
    """cli/visualize.py の新引数パース仕様テスト.

    仕様（cli/visualize.py _parse_args）:
        --bake-max-depth N: 既定 16
        --cull-outward (既定 True) / --no-cull-outward → dest="cull_outward"
        --no-sensor-marker: 既定 False
        --no-led-markers: 既定 False
        --no-markers: 既定 False（ショートカット = 両方 True）
    """

    def test_bake_max_depth_default_is_16(self):
        """--bake-max-depth の既定値が 16 であること.

        仕様（cli/visualize.py）: --bake-max-depth 既定 16
        SPEC_03: depth≥12 推奨（default=16 は推奨値を超えている）.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml"])
        assert args.bake_max_depth == 16

    def test_bake_max_depth_custom_value_12(self):
        """--bake-max-depth 12 が正しくパースされること（SPEC_03 推奨値）.

        SPEC_03: depth≥12 推奨.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--bake-max-depth", "12"])
        assert args.bake_max_depth == 12

    def test_bake_max_depth_type_is_int(self):
        """--bake-max-depth がパースされ int 型であること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--bake-max-depth", "8"])
        assert isinstance(args.bake_max_depth, int)

    def test_cull_outward_default_is_true(self):
        """--cull-outward/--no-cull-outward を指定しない場合、cull_outward=True であること.

        仕様（cli/visualize.py）: --cull-outward 既定有効.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml"])
        assert args.cull_outward is True

    def test_no_cull_outward_sets_false(self):
        """--no-cull-outward を指定すると cull_outward=False になること.

        仕様: --no-cull-outward → 外向き面を含む全面を描画する（後方互換）.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-cull-outward"])
        assert args.cull_outward is False

    def test_cull_outward_explicit_flag_stays_true(self):
        """--cull-outward を明示的に指定しても cull_outward=True であること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--cull-outward"])
        assert args.cull_outward is True

    def test_cull_outward_and_no_cull_outward_are_mutually_exclusive(self):
        """--cull-outward と --no-cull-outward が相互排他であること（argparse で強制）.

        仕様: mutually_exclusive_group で実装.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        with pytest.raises(SystemExit) as exc_info:
            _parse_args(["--scene", "scene.yaml", "--cull-outward", "--no-cull-outward"])
        assert exc_info.value.code != 0

    def test_no_sensor_marker_default_is_false(self):
        """--no-sensor-marker を指定しない場合、no_sensor_marker=False であること.

        仕様: hide_sensor_marker: bool = False（既定：センサマーカーを表示）.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml"])
        assert args.no_sensor_marker is False

    def test_no_sensor_marker_flag_sets_true(self):
        """--no-sensor-marker を指定すると no_sensor_marker=True になること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-sensor-marker"])
        assert args.no_sensor_marker is True

    def test_no_led_markers_default_is_false(self):
        """--no-led-markers を指定しない場合、no_led_markers=False であること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml"])
        assert args.no_led_markers is False

    def test_no_led_markers_flag_sets_true(self):
        """--no-led-markers を指定すると no_led_markers=True になること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-led-markers"])
        assert args.no_led_markers is True

    def test_no_markers_default_is_false(self):
        """--no-markers を指定しない場合、no_markers=False であること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml"])
        assert args.no_markers is False

    def test_no_markers_flag_sets_true(self):
        """--no-markers を指定すると no_markers=True になること."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-markers"])
        assert args.no_markers is True


# ===========================================================================
# 3. --no-markers が hide_sensor_marker と hide_led_markers 両方に OR 適用されること
# ===========================================================================


class TestNoMarkersOrLogic:
    """--no-markers が両マーカーに OR 論理で適用される仕様テスト.

    仕様（cli/visualize.py main）:
        hide_sensor_marker = bool(args.no_sensor_marker) or bool(args.no_markers)
        hide_led_markers = bool(args.no_led_markers) or bool(args.no_markers)
    """

    def _compute_hide_flags(
        self,
        no_sensor_marker: bool,
        no_led_markers: bool,
        no_markers: bool,
    ) -> tuple[bool, bool]:
        """main() の hide_sensor_marker / hide_led_markers 計算を再現する."""
        hide_sensor_marker = no_sensor_marker or no_markers
        hide_led_markers = no_led_markers or no_markers
        return hide_sensor_marker, hide_led_markers

    def test_no_markers_hides_both_sensor_and_led(self):
        """--no-markers のみ指定で両方の hide が True になること.

        仕様: --no-markers は --no-sensor-marker --no-led-markers の両方と等価.
        """
        hide_sensor, hide_led = self._compute_hide_flags(False, False, True)
        assert hide_sensor is True, "--no-markers → hide_sensor_marker=True"
        assert hide_led is True, "--no-markers → hide_led_markers=True"

    def test_no_sensor_marker_only_hides_sensor_not_led(self):
        """--no-sensor-marker のみ指定でセンサマーカーのみ非表示になること."""
        hide_sensor, hide_led = self._compute_hide_flags(True, False, False)
        assert hide_sensor is True
        assert hide_led is False

    def test_no_led_markers_only_hides_led_not_sensor(self):
        """--no-led-markers のみ指定で LED マーカーのみ非表示になること."""
        hide_sensor, hide_led = self._compute_hide_flags(False, True, False)
        assert hide_sensor is False
        assert hide_led is True

    def test_no_flags_means_both_visible(self):
        """フラグなしで両マーカーが表示（False）になること（後方互換）."""
        hide_sensor, hide_led = self._compute_hide_flags(False, False, False)
        assert hide_sensor is False
        assert hide_led is False

    def test_all_flags_hides_both(self):
        """全フラグを True にすると両方が True になること（OR 論理）."""
        hide_sensor, hide_led = self._compute_hide_flags(True, True, True)
        assert hide_sensor is True
        assert hide_led is True

    def test_cli_no_markers_parses_to_true_for_both_compute(self):
        """CLI --no-markers が main() 内で両 hide フラグを True にすること.

        _parse_args でパースした結果を main() ロジックで評価する.
        """
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-markers"])
        hide_sensor = bool(args.no_sensor_marker) or bool(args.no_markers)
        hide_led = bool(args.no_led_markers) or bool(args.no_markers)
        assert hide_sensor is True
        assert hide_led is True

    def test_cli_no_sensor_marker_only_parses_correctly(self):
        """CLI --no-sensor-marker のみ: センサのみ True, LED は False."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-sensor-marker"])
        hide_sensor = bool(args.no_sensor_marker) or bool(args.no_markers)
        hide_led = bool(args.no_led_markers) or bool(args.no_markers)
        assert hide_sensor is True
        assert hide_led is False

    def test_cli_no_led_markers_only_parses_correctly(self):
        """CLI --no-led-markers のみ: LED のみ True, センサは False."""
        from sm_tias_lightsim.cli.visualize import _parse_args
        args = _parse_args(["--scene", "scene.yaml", "--no-led-markers"])
        hide_sensor = bool(args.no_sensor_marker) or bool(args.no_markers)
        hide_led = bool(args.no_led_markers) or bool(args.no_markers)
        assert hide_sensor is False
        assert hide_led is True


# ===========================================================================
# 4. show_scene smoke テスト（新引数を渡して正常終了すること）
# ===========================================================================


MINIMAL_YAML = Path(__file__).parent.parent / "fixtures" / "minimal.yaml"


class TestShowSceneNewArgsSmokeTests:
    """show_scene に新引数を渡した smoke テスト.

    仕様: 新 keyword 引数デフォルトで既存 show_scene 呼び出しが動くこと.
    testing.md: 可視化は「ファイルが生成されたか」程度の smoke テストで十分.
    """

    @pytest.mark.slow
    def test_show_scene_with_cull_outward_true_creates_screenshot(self, tmp_path: Path):
        """cull_outward=True（既定値）で screenshot が生成されること.

        仕様変更注記: cull_outward=True が既定になったため、irradiance ありの場合
        内向き face のみ描画する処理が走る. screenshot 生成には影響しない.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "cull_true.png"
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
            cull_outward=True,  # 新仕様の既定値
        )
        assert screenshot_path.exists()
        assert screenshot_path.stat().st_size > 0

    @pytest.mark.slow
    def test_show_scene_with_cull_outward_false_creates_screenshot(self, tmp_path: Path):
        """cull_outward=False（後方互換）で screenshot が生成されること.

        仕様: cull_outward=False のとき全面描画（後方互換）.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "cull_false.png"
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
            cull_outward=False,  # 後方互換
        )
        assert screenshot_path.exists()
        assert screenshot_path.stat().st_size > 0

    @pytest.mark.slow
    def test_show_scene_hide_sensor_marker_true_creates_screenshot(self, tmp_path: Path):
        """hide_sensor_marker=True でセンサマーカーを非表示にして screenshot が生成されること.

        仕様: True のとき、センサマーカー（青い球＋視線矢印）の add_mesh をスキップ.
        メッシュ着色・scalar bar・座標軸はそのまま残る.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "no_sensor.png"
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
            hide_sensor_marker=True,
        )
        assert screenshot_path.exists()
        assert screenshot_path.stat().st_size > 0

    @pytest.mark.slow
    def test_show_scene_hide_led_markers_true_creates_screenshot(self, tmp_path: Path):
        """hide_led_markers=True で LED マーカーを非表示にして screenshot が生成されること.

        仕様: True のとき、LED マーカーの add_mesh をスキップ.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "no_led.png"
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
            hide_led_markers=True,
        )
        assert screenshot_path.exists()
        assert screenshot_path.stat().st_size > 0

    @pytest.mark.slow
    def test_show_scene_hide_both_markers_creates_screenshot(self, tmp_path: Path):
        """両マーカーを非表示にして screenshot が生成されること（--no-markers 相当）.

        仕様: hide_sensor_marker=True AND hide_led_markers=True で両方非表示.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "no_markers.png"
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
            hide_sensor_marker=True,
            hide_led_markers=True,
        )
        assert screenshot_path.exists()
        assert screenshot_path.stat().st_size > 0

    @pytest.mark.slow
    def test_show_scene_default_args_backward_compat(self, tmp_path: Path):
        """新引数のデフォルト値で既存の show_scene 呼び出しが動くこと（後方互換）.

        仕様: 新 keyword 引数デフォルトで既存 show_scene 呼び出しが動く.
        cull_outward=True（新デフォルト）も含めて正常終了すること.
        """
        from sm_tias_lightsim.scene.schema import load_scene_yaml
        config = load_scene_yaml(MINIMAL_YAML)
        project_root = MINIMAL_YAML.parent

        screenshot_path = tmp_path / "default_args.png"
        # 新引数を一切指定しない（全デフォルト値）
        show_scene(
            config,
            project_root,
            irradiance=None,
            screenshot=screenshot_path,
        )
        assert screenshot_path.exists()


# ===========================================================================
# 5. CLI 新引数の main() 統合テスト（smoke）
# ===========================================================================


class TestCliNewArgIntegration:
    """CLI 新引数の main() 統合 smoke テスト.

    --no-bake モード（Mitsuba 不要）で新引数が main() に渡ること確認.
    """

    @pytest.mark.slow
    def test_no_sensor_marker_in_cli_creates_screenshot(self, tmp_path: Path):
        """--no-sensor-marker + --screenshot で PNG が生成されること."""
        from sm_tias_lightsim.cli.visualize import main

        screenshot_path = tmp_path / "cli_no_sensor.png"
        result = main([
            "--scene", str(MINIMAL_YAML),
            "--no-bake",
            "--no-sensor-marker",
            "--screenshot", str(screenshot_path),
        ])
        assert result == 0
        assert screenshot_path.exists()

    @pytest.mark.slow
    def test_no_led_markers_in_cli_creates_screenshot(self, tmp_path: Path):
        """--no-led-markers + --screenshot で PNG が生成されること."""
        from sm_tias_lightsim.cli.visualize import main

        screenshot_path = tmp_path / "cli_no_led.png"
        result = main([
            "--scene", str(MINIMAL_YAML),
            "--no-bake",
            "--no-led-markers",
            "--screenshot", str(screenshot_path),
        ])
        assert result == 0
        assert screenshot_path.exists()

    @pytest.mark.slow
    def test_no_markers_in_cli_creates_screenshot(self, tmp_path: Path):
        """--no-markers + --screenshot で PNG が生成されること（ショートカット）."""
        from sm_tias_lightsim.cli.visualize import main

        screenshot_path = tmp_path / "cli_no_markers.png"
        result = main([
            "--scene", str(MINIMAL_YAML),
            "--no-bake",
            "--no-markers",
            "--screenshot", str(screenshot_path),
        ])
        assert result == 0
        assert screenshot_path.exists()

    @pytest.mark.slow
    def test_no_cull_outward_in_cli_creates_screenshot(self, tmp_path: Path):
        """--no-cull-outward + --screenshot で PNG が生成されること（後方互換）."""
        from sm_tias_lightsim.cli.visualize import main

        screenshot_path = tmp_path / "cli_no_cull.png"
        result = main([
            "--scene", str(MINIMAL_YAML),
            "--no-bake",
            "--no-cull-outward",
            "--screenshot", str(screenshot_path),
        ])
        assert result == 0
        assert screenshot_path.exists()

    @pytest.mark.slow
    def test_bake_max_depth_16_in_cli_no_bake_parses_without_error(self, tmp_path: Path):
        """--bake-max-depth 16 が CLI で受け付けられ、--no-bake モードで正常終了すること.

        --no-bake モードでは path ベイクを実行しないため、Mitsuba 依存なし.
        """
        from sm_tias_lightsim.cli.visualize import main

        screenshot_path = tmp_path / "cli_max_depth.png"
        result = main([
            "--scene", str(MINIMAL_YAML),
            "--no-bake",
            "--bake-max-depth", "16",
            "--screenshot", str(screenshot_path),
        ])
        assert result == 0
        assert screenshot_path.exists()
