"""render/runner.py の JIT 初期化統合テスト (Speed-1 仕様)．

仕様出典:
- docs/06_TEST/TEST_04_シミュレーション速度最適化履歴.md — Speed-1 セクション
- Phase 1 実装サマリー (runner.py の自動検出フック仕様)

テスト対象:
    render_scene() 内の JIT 初期化フロー:
    - C1: llvm_* variant のとき ensure_drjit_libllvm_path が呼ばれる
    - C2: scalar_* / cuda_* のとき ensure_drjit_libllvm_path が呼ばれない
    - C3: ImportError 時に診断ログが出て re-raise される
    - C4: set_variant 成功後に log_variant_status が呼ばれる
    - C5: 既に variant が設定済みなら警告を出して再設定しない

テスト方針:
    - mi.set_variant / mi.render / build_scene をモックして Mitsuba 実行を回避（軽量テスト）
    - caplog で診断ログを検証
    - monkeypatch で runner モジュールの _current_variant グローバルをリセットする
"""

from __future__ import annotations

import logging
from pathlib import Path
from unittest.mock import MagicMock, patch

import numpy as np
import pytest

import sm_tias_lightsim.render.runner as runner_module
from sm_tias_lightsim.scene.schema import SceneConfig

# ---------------------------------------------------------------------------
# フィクスチャ
# ---------------------------------------------------------------------------


def _make_scene_config(variant: str = "llvm_ad_rgb") -> SceneConfig:
    """最小限の SceneConfig を作成する（実 STL 不要 — runner 自体をモックするため）．"""
    data = {
        "name": "test_jit",
        "geometry": {
            "meshes": [
                {
                    "id": "dummy_mesh",
                    "path": "dummy.stl",
                    "unit": "mm",
                    "material": "white",
                }
            ]
        },
        "materials": {"white": {"type": "diffuse", "reflectance": 0.9}},
        "lights": [
            {
                "id": "led",
                "type": "point",
                "position": [0.0, 0.0, 100.0],
                "intensity": 1.0,
                "profile": "lambertian",
            }
        ],
        "sensor": {
            "type": "perspective",
            "position": [0.0, 0.0, 200.0],
            "target": [0.0, 0.0, 0.0],
            "up": [0.0, 1.0, 0.0],
            "fov_deg": 60.0,
            "film": {"width": 4, "height": 4},
        },
        "render": {
            "variant": variant,
            "integrator": "path",
            "max_depth": 2,
            "spp": 1,
        },
    }
    return SceneConfig.model_validate(data)


@pytest.fixture(autouse=True)
def reset_runner_variant():
    """各テスト前後に runner._current_variant をリセットする．

    runner.py の _current_variant はモジュールレベルのグローバル変数で、
    プロセス内で 1 度しか設定できない仕様のため、テスト間の干渉を防ぐためリセットする．
    """
    original = runner_module._current_variant
    runner_module._current_variant = None
    yield
    runner_module._current_variant = original


# ---------------------------------------------------------------------------
# C1: llvm_* variant のとき ensure_drjit_libllvm_path が呼ばれる
# ---------------------------------------------------------------------------


class TestLlvmVariantCallsEnsurePath:
    """C1: llvm_* variant のとき ensure_drjit_libllvm_path が呼ばれること．"""

    def test_llvm_variant_calls_ensure_drjit_libllvm_path(self) -> None:
        """C1: variant='llvm_ad_rgb' のとき ensure_drjit_libllvm_path が呼ばれること．

        仕様:
            desired_variant.startswith('llvm_') のとき、mi.set_variant の前に
            ensure_drjit_libllvm_path が呼ばれる．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_scene = MagicMock()
        dummy_image = np.zeros((4, 4, 3), dtype=np.float32)

        # 全モックを一括設定
        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path") as mock_ensure,
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, str(Path("/tmp/fake"))),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict(
                "sys.modules",
                {
                    "mitsuba": MagicMock(
                        set_variant=MagicMock(),
                        variant=MagicMock(return_value="llvm_ad_rgb"),
                        load_dict=MagicMock(return_value=dummy_scene),
                        render=MagicMock(
                            return_value=MagicMock(__array__=lambda s, d=None: dummy_image)
                        ),
                    )
                },
            ),
        ):
            # shutil.rmtree も no-op に
            with patch("shutil.rmtree"):
                try:
                    runner_module.render_scene(config, project_root=Path("/tmp"))
                except Exception:
                    pass  # レンダリング後の後処理エラーは無視

        mock_ensure.assert_called_once()

    def test_llvm_variant_ensure_called_before_set_variant(self) -> None:
        """C1: ensure_drjit_libllvm_path は mi.set_variant より先に呼ばれること．

        仕様: mitsuba の import 前に os.environ['DRJIT_LIBLLVM_PATH'] を設定することが必須．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")
        call_order: list[str] = []

        def ensure_side_effect() -> None:
            call_order.append("ensure")

        dummy_mi = MagicMock()

        def set_variant_side_effect(variant: str) -> None:
            call_order.append(f"set_variant:{variant}")

        dummy_mi.set_variant.side_effect = set_variant_side_effect
        dummy_mi.variant.return_value = "llvm_ad_rgb"

        with (
            patch(
                "sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path",
                side_effect=ensure_side_effect,
            ),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        # ensure が set_variant より前に来ること
        if "ensure" in call_order and any("set_variant" in c for c in call_order):
            ensure_idx = call_order.index("ensure")
            set_variant_idx = next(i for i, c in enumerate(call_order) if "set_variant" in c)
            assert ensure_idx < set_variant_idx, (
                f"ensure_drjit_libllvm_path は mi.set_variant より先に呼ばれること: {call_order}"
            )


# ---------------------------------------------------------------------------
# C2: scalar_* / cuda_* のとき ensure_drjit_libllvm_path が呼ばれない
# ---------------------------------------------------------------------------


class TestNonLlvmVariantSkipsEnsurePath:
    """C2: scalar_* / cuda_* のとき ensure_drjit_libllvm_path が呼ばれないこと．"""

    def test_scalar_variant_does_not_call_ensure_drjit_libllvm_path(self) -> None:
        """C2: variant='scalar_rgb' のとき ensure_drjit_libllvm_path が呼ばれないこと．

        仕様:
            scalar_* / cuda_* は LLVM-C.dll 不要なので探索しない．
        """
        config = _make_scene_config(variant="scalar_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "scalar_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path") as mock_ensure,
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        mock_ensure.assert_not_called()

    def test_cuda_variant_does_not_call_ensure_drjit_libllvm_path(self) -> None:
        """C2: variant='cuda_ad_rgb' のとき ensure_drjit_libllvm_path が呼ばれないこと．

        仕様:
            cuda_* は LLVM-C.dll 不要なので探索しない．
        """
        config = _make_scene_config(variant="cuda_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "cuda_ad_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path") as mock_ensure,
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        mock_ensure.assert_not_called()


# ---------------------------------------------------------------------------
# C3: ImportError 時に診断ログが出て re-raise される
# ---------------------------------------------------------------------------


class TestImportErrorDiagnostics:
    """C3: ImportError 時に診断ログが出て re-raise されること．"""

    def test_import_error_with_llvm_message_emits_error_log(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """C3: mi.set_variant が 'LLVM' 含む ImportError を出したとき ERROR ログが出ること．

        仕様:
            mi.set_variant が ImportError を出した場合、原因明示ログを出してから re-raise する．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.set_variant.side_effect = ImportError(
            "jit_init_thread_state(): LLVM backend is inactive"
        )

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
        ):
            with caplog.at_level(logging.ERROR, logger="sm_tias_lightsim.render.runner"):
                with pytest.raises(ImportError):
                    runner_module.render_scene(config, project_root=Path("/tmp"))

        error_records = [r for r in caplog.records if r.levelno >= logging.ERROR]
        assert error_records, "ImportError 時に ERROR レベルのログが出ること"

    def test_import_error_is_reraised(self) -> None:
        """C3: ImportError は診断ログ後に re-raise されること（握りつぶされない）．

        仕様:
            ImportError → 原因明示ログ → re-raise．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.set_variant.side_effect = ImportError("LLVM backend inactive")

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
        ):
            with pytest.raises(ImportError):
                runner_module.render_scene(config, project_root=Path("/tmp"))

    def test_import_error_without_llvm_in_message_is_still_reraised(self) -> None:
        """C3: ImportError のメッセージに 'LLVM' を含まない場合も re-raise されること．

        仕様: ImportError は必ず re-raise する（メッセージ内容に依らず）．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.set_variant.side_effect = ImportError("some other import error")

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
        ):
            with pytest.raises(ImportError):
                runner_module.render_scene(config, project_root=Path("/tmp"))

    def test_import_error_log_contains_variant_name(self, caplog: pytest.LogCaptureFixture) -> None:
        """C3: ImportError ログに variant 名が含まれること（診断容易化）．

        仕様: 原因明示ログを出す．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.set_variant.side_effect = ImportError("LLVM backend is inactive")

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
        ):
            with caplog.at_level(logging.ERROR, logger="sm_tias_lightsim.render.runner"):
                with pytest.raises(ImportError):
                    runner_module.render_scene(config, project_root=Path("/tmp"))

        error_messages = [r.message for r in caplog.records if r.levelno >= logging.ERROR]
        assert any("llvm_ad_rgb" in msg for msg in error_messages), (
            f"ERROR ログに variant 名が含まれること: {error_messages}"
        )


# ---------------------------------------------------------------------------
# C4: set_variant 成功後に log_variant_status が呼ばれる
# ---------------------------------------------------------------------------


class TestLogVariantStatusCalled:
    """C4: set_variant 成功後に log_variant_status が呼ばれること．"""

    def test_log_variant_status_called_after_successful_set_variant(self) -> None:
        """C4: mi.set_variant 成功後に log_variant_status が呼ばれること．

        仕様:
            set_variant 成功後に variant 状態を INFO ログで出力する．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "llvm_ad_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status") as mock_log,
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        mock_log.assert_called_once()

    def test_log_variant_status_receives_correct_variants(self) -> None:
        """C4: log_variant_status に requested_variant と actual_variant が正しく渡されること．

        仕様: log_variant_status(_logger, desired_variant, mi.variant()) が呼ばれる．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "llvm_ad_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status") as mock_log,
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        if mock_log.call_args is not None:
            _logger_arg, requested, actual = mock_log.call_args[0]
            assert requested == "llvm_ad_rgb", (
                f"requested_variant が 'llvm_ad_rgb' であること: got {requested}"
            )
            assert actual == "llvm_ad_rgb", (
                f"actual_variant が 'llvm_ad_rgb' であること: got {actual}"
            )

    def test_log_variant_status_not_called_on_import_error(self) -> None:
        """C4: ImportError 時には log_variant_status が呼ばれないこと．

        仕様: set_variant が失敗した場合は log_variant_status に到達しない．
        """
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.set_variant.side_effect = ImportError("LLVM inactive")

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch("sm_tias_lightsim.render.runner.log_variant_status") as mock_log,
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
        ):
            with pytest.raises(ImportError):
                runner_module.render_scene(config, project_root=Path("/tmp"))

        mock_log.assert_not_called()


# ---------------------------------------------------------------------------
# C5: 既に variant が設定済みなら警告を出して再設定しない
# ---------------------------------------------------------------------------


class TestVariantAlreadySetWarning:
    """C5: 既に variant が設定済みなら警告を出して再設定しない（既存挙動の維持）．"""

    def test_already_set_different_variant_emits_warning(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """C5: 既に異なる variant が設定済みのとき WARNING が出ること．

        仕様: 既に異なる variant が設定されている場合は警告ログを出して続行する．
        """
        # 事前に異なる variant を設定済みにする
        runner_module._current_variant = "scalar_rgb"
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "scalar_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            with caplog.at_level(logging.WARNING, logger="sm_tias_lightsim.render.runner"):
                try:
                    runner_module.render_scene(config, project_root=Path("/tmp"))
                except Exception:
                    pass

        warning_records = [r for r in caplog.records if r.levelno >= logging.WARNING]
        assert warning_records, "異なる variant が既設定のとき WARNING が出ること"

    def test_already_set_different_variant_does_not_call_set_variant_again(self) -> None:
        """C5: 既に設定済みのとき mi.set_variant が再呼び出しされないこと．

        仕様: 既に variant が設定済みなら再設定しない．
        """
        runner_module._current_variant = "scalar_rgb"
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "scalar_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            try:
                runner_module.render_scene(config, project_root=Path("/tmp"))
            except Exception:
                pass

        dummy_mi.set_variant.assert_not_called()

    def test_same_variant_already_set_does_not_emit_warning(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """C5: 同じ variant が既設定のとき WARNING が出ないこと．

        仕様: 同じ variant が既に設定されている場合は何もしない（警告なし）．
        """
        runner_module._current_variant = "llvm_ad_rgb"
        config = _make_scene_config(variant="llvm_ad_rgb")

        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "llvm_ad_rgb"

        with (
            patch("sm_tias_lightsim.render.runner.ensure_drjit_libllvm_path"),
            patch(
                "sm_tias_lightsim.render.runner.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ),
            patch("sm_tias_lightsim.render.runner.log_variant_status"),
            patch.dict("sys.modules", {"mitsuba": dummy_mi}),
            patch("shutil.rmtree"),
        ):
            with caplog.at_level(logging.WARNING, logger="sm_tias_lightsim.render.runner"):
                try:
                    runner_module.render_scene(config, project_root=Path("/tmp"))
                except Exception:
                    pass

        warning_records = [r for r in caplog.records if r.levelno >= logging.WARNING]
        messages = [r.message for r in warning_records]
        assert not warning_records, f"同じ variant 設定済みのとき WARNING は出ないこと: {messages}"
