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

仕様出典:
- docs/06_TEST/TEST_04_シミュレーション速度最適化履歴.md — Speed-1 セクション
- Phase 1 実装サマリー (baker.py の log_variant_status 呼び出し仕様)

テスト対象:
    _set_variant_once() 内の JIT 初期化フロー:
    - D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれる
    - D2: _bake_direct 経由でも variant 設定後にログが出る
    - D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれる（Speed-2 前準備）

テスト方針:
    - Mitsuba をモックして軽量テスト（実 bake は slow テストでカバー済み）
    - caplog / mock で診断ログとフック呼び出しを検証
    - _set_variant_once は内部 API だが Speed-1 仕様の核となる関数なので直接テスト対象とする
"""

from __future__ import annotations

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

import pytest

import sm_tias_lightsim.visualize.baker as baker_module
from sm_tias_lightsim.visualize.baker import _set_variant_once

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


@pytest.fixture(autouse=True)
def reset_mitsuba_variant_mock():
    """各テストで mitsuba をモック化してクリーンな状態を保つ．

    _set_variant_once は内部で mitsuba.variant() / mitsuba.set_variant() を呼ぶため、
    テスト用のモック mi を sys.modules に注入する．
    """
    # conftest.py で scalar_rgb が設定済みのため、テスト内でのバリアント状態は
    # モック mi.variant() の戻り値で制御する
    yield


# ---------------------------------------------------------------------------
# D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれる
# ---------------------------------------------------------------------------


class TestSetVariantOnceCallsLogVariantStatus:
    """D1: _set_variant_once 成功後に log_variant_status が呼ばれること．"""

    def test_scalar_variant_calls_log_variant_status(self) -> None:
        """D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれること．

        仕様:
            _set_variant_once 内で mi.set_variant が成功したら
            log_variant_status(_logger, desired_variant, mi.variant()) を呼ぶ．
        """
        dummy_mi = MagicMock()
        # variant() が None を返す（未設定状態）→ set_variant を呼ぶ分岐に入る
        dummy_mi.variant.return_value = None

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
                _set_variant_once("scalar_rgb")

        mock_log.assert_called_once()

    def test_log_variant_status_receives_scalar_variant(self) -> None:
        """D1: log_variant_status に 'scalar_rgb' が渡されること．

        仕様: _set_variant_once の desired_variant と mi.variant() 戻り値が log に渡される．
        """
        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = None

        # set_variant 呼び出し後に variant() が 'scalar_rgb' を返すように設定
        def variant_side_effect() -> str | None:
            # set_variant が呼ばれた後は 'scalar_rgb' を返す
            if dummy_mi.set_variant.called:
                return "scalar_rgb"
            return None

        dummy_mi.variant.side_effect = variant_side_effect

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
                _set_variant_once("scalar_rgb")

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

    def test_already_same_variant_does_not_call_log(self) -> None:
        """D1: 既に同じ variant が設定済みのとき log_variant_status が呼ばれないこと．

        仕様: 既に同じ variant が設定されている場合は何もしない（log も出さない）．
        """
        dummy_mi = MagicMock()
        # variant() が既に 'scalar_rgb' を返す（設定済み）
        dummy_mi.variant.return_value = "scalar_rgb"

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
                _set_variant_once("scalar_rgb")

        mock_log.assert_not_called()

    def test_already_different_variant_emits_warning_not_log_variant_status(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """D1: 既に異なる variant が設定済みのとき警告が出て log_variant_status は呼ばれないこと．

        仕様: 異なる variant が設定されている場合は警告を出して続行する（変更しない）．
        """
        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = "llvm_ad_rgb"  # 既設定（異なる）

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
                with caplog.at_level(logging.WARNING, logger="sm_tias_lightsim.visualize.baker"):
                    _set_variant_once("scalar_rgb")

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


# ---------------------------------------------------------------------------
# D2: _bake_direct 経由でも variant 設定後にログが出る
# ---------------------------------------------------------------------------


class TestBakeDirectCallsLogVariantStatus:
    """D2: _bake_direct 経由（bake_irradiance mode='direct'）でも variant ログが出ること．

    bake_irradiance(mode='direct') は内部で mi.set_variant('scalar_rgb') を呼び、
    成功後に log_variant_status を呼ぶ仕様．
    """

    def test_bake_direct_variant_log_is_emitted_on_success(
        self, caplog: pytest.LogCaptureFixture
    ) -> None:
        """D2: bake_irradiance(mode='direct') が variant 設定後にログを出すこと．

        仕様:
            mode='direct' の分岐で _variant_set_succeeded のとき
            log_variant_status(_logger, desired_variant, mi.variant()) が呼ばれる．
        """
        from sm_tias_lightsim.scene.schema import SceneConfig

        # 最小限のシーン設定（実ファイル不要 — build_scene をモック）
        data = {
            "name": "test_bake_direct_log",
            "geometry": {
                "meshes": [{"id": "m1", "path": "/fake/m1.stl", "unit": "mm", "material": "white"}]
            },
            "materials": {"white": {"type": "diffuse", "reflectance": 0.9}},
            "lights": [
                {
                    "id": "led",
                    "type": "point",
                    "position": [0, 0, 100],
                    "intensity": 1.0,
                    "profile": "lambertian",
                }
            ],
            "sensor": {
                "type": "perspective",
                "position": [0, 0, 200],
                "target": [0, 0, 0],
                "up": [0, 1, 0],
                "fov_deg": 60.0,
                "film": {"width": 4, "height": 4},
            },
            "render": {"variant": "scalar_rgb", "integrator": "path", "max_depth": 2, "spp": 1},
        }
        config = SceneConfig.model_validate(data)

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

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            # build_scene は baker.py 内で遅延 import されるため元モジュールをモックする
            # (from sm_tias_lightsim.scene.builder import build_scene)
            with patch(
                "sm_tias_lightsim.scene.builder.build_scene",
                return_value=({"type": "scene"}, "/tmp/fake"),
            ):
                with patch.object(dummy_mi, "load_dict", return_value=dummy_scene):
                    with patch("shutil.rmtree"):
                        with patch(
                            "sm_tias_lightsim.visualize.baker.log_variant_status"
                        ) as mock_log:
                            with patch(
                                "sm_tias_lightsim.visualize.baker._bake_direct",
                                return_value={},
                            ):
                                with caplog.at_level(
                                    logging.INFO, logger="sm_tias_lightsim.visualize.baker"
                                ):
                                    try:
                                        baker_module.bake_irradiance(
                                            config,
                                            Path("/tmp"),
                                            mode="direct",
                                        )
                                    except Exception:
                                        pass

        # log_variant_status が呼ばれたこと（_variant_set_succeeded == True のとき）
        mock_log.assert_called()


# ---------------------------------------------------------------------------
# D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれる（Speed-2 前準備）
# ---------------------------------------------------------------------------


class TestSetVariantOnceLlvmCallsEnsurePath:
    """D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれること．

    仕様（Speed-2 wavefront 化前準備）:
        llvm_* variant 要求時は DRJIT_LIBLLVM_PATH を自動探索して設定する．
        scalar_* / cuda_* は LLVM-C.dll 不要なので探索しない．
    """

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

        仕様:
            Speed-2 wavefront 化用の前準備:
            llvm_* variant の場合は DRJIT_LIBLLVM_PATH を自動探索する．
        """
        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = None  # 未設定 → set_variant 分岐に入る

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path") as mock_ensure:
                with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
                    _set_variant_once("llvm_ad_rgb")

        mock_ensure.assert_called_once()

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

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

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch("sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path") as mock_ensure:
                with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
                    _set_variant_once("scalar_rgb")

        mock_ensure.assert_not_called()

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

        仕様: mitsuba の import 前に os.environ を設定することが必須．
        """
        dummy_mi = MagicMock()
        dummy_mi.variant.return_value = None
        call_order: list[str] = []

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

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

        dummy_mi.set_variant.side_effect = set_variant_side_effect

        with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
            with patch(
                "sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path",
                side_effect=ensure_side_effect,
            ):
                with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
                    _set_variant_once("llvm_ad_rgb")

        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 は set_variant より先に呼ばれること: {call_order}"
            )
