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

仕様出典:
- docs/06_TEST/TEST_04_シミュレーション速度最適化履歴.md — Speed-1 セクション
- Phase 1 実装サマリー (cli/render.py の import 順制御仕様)

テスト対象:
    - E1: cli/render.py がモジュール import の段階で mitsuba を import しないこと
    - E1 (動作): llvm_* variant 設定時に ensure_drjit_libllvm_path を呼んでから
                  render_scene を import すること

テスト方針:
    - AST レベルで import 文の存在をチェック（モジュールレベルの mitsuba import 不在を検証）
    - main() の動作は既存 tests/cli/test_render.py でカバー済み
    - ここでは「import 順の設計意図」を仕様として検証する
"""

from __future__ import annotations

import ast
from pathlib import Path

# ---------------------------------------------------------------------------
# E1: cli/render.py がモジュール import 時点で mitsuba を import しないこと
# ---------------------------------------------------------------------------

_CLI_RENDER_PATH = (
    Path(__file__).parent.parent.parent / "src" / "sm_tias_lightsim" / "cli" / "render.py"
)


class TestCliRenderImportOrder:
    """E1: cli/render.py の import 順が仕様通りであることを検証する．

    仕様:
        cli/render.py がモジュール import の段階で mitsuba を import しないこと．
        ensure_drjit_libllvm_path を呼ぶための前提として、mitsuba は遅延 import される必要がある．
    """

    def test_no_top_level_mitsuba_import_in_cli_render(self) -> None:
        """E1: cli/render.py のモジュールレベルに 'import mitsuba' が存在しないこと．

        仕様:
            --help だけ使う場面（Mitsuba 不在の軽量環境）でも起動できるようにするため，
            Mitsuba 依存 import（load_scene_yaml / render_scene 等）は main() 内で遅延 import する．
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # トップレベル（関数外）の import 文のみを取得
        top_level_imports: list[ast.Import | ast.ImportFrom] = []
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                top_level_imports.append(node)

        # mitsuba を直接 import している行がないこと
        mitsuba_imports = []
        for imp in top_level_imports:
            if isinstance(imp, ast.Import):
                for alias in imp.names:
                    if alias.name.startswith("mitsuba"):
                        mitsuba_imports.append(ast.dump(imp))
            elif isinstance(imp, ast.ImportFrom):
                if imp.module and imp.module.startswith("mitsuba"):
                    mitsuba_imports.append(ast.dump(imp))

        assert not mitsuba_imports, (
            f"cli/render.py のモジュールレベルに mitsuba import が存在する: {mitsuba_imports}\n"
            "仕様: mitsuba はモジュールレベルで import してはならない（遅延 import 必須）"
        )

    def test_no_top_level_render_runner_import_in_cli_render(self) -> None:
        """E1: cli/render.py のモジュールレベルに render.runner の import が存在しないこと．

        仕様:
            runner.py は mitsuba を遅延 import するが，
            cli/render.py がモジュールレベルで runner を import すると
            ensure_drjit_libllvm_path の前に mitsuba import chain が走る可能性がある．
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        runner_imports = []
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.ImportFrom):
                if node.module and "runner" in node.module:
                    runner_imports.append(ast.dump(node))

        assert not runner_imports, (
            f"cli/render.py のモジュールレベルに runner import が存在する: {runner_imports}\n"
            "仕様: render_scene は main() 内で遅延 import すること"
        )

    def test_only_standard_library_imports_at_top_level(self) -> None:
        """E1: cli/render.py のモジュールレベルには標準ライブラリのみ import されていること．

        仕様:
            モジュール import 段階では標準ライブラリ（argparse / sys / pathlib）のみ使用．
            Mitsuba 依存の内部モジュールは main() 内で遅延 import する．
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # 許可される標準ライブラリモジュール名
        allowed_std_modules = frozenset(
            ["__future__", "argparse", "sys", "pathlib", "os", "logging"]
        )

        top_level_imports: list[ast.Import | ast.ImportFrom] = [
            node
            for node in ast.iter_child_nodes(tree)
            if isinstance(node, (ast.Import, ast.ImportFrom))
        ]

        non_std_imports = []
        for imp in top_level_imports:
            if isinstance(imp, ast.Import):
                for alias in imp.names:
                    root = alias.name.split(".")[0]
                    if root not in allowed_std_modules:
                        non_std_imports.append(alias.name)
            elif isinstance(imp, ast.ImportFrom):
                if imp.module:
                    root = imp.module.split(".")[0]
                    if root not in allowed_std_modules:
                        non_std_imports.append(imp.module)

        assert not non_std_imports, (
            f"cli/render.py のモジュールレベルに非標準ライブラリの import がある:"
            f" {non_std_imports}\n"
            "仕様: モジュールレベルは標準ライブラリのみ（Mitsuba / 内部モジュールは遅延 import）"
        )

    def test_ensure_drjit_libllvm_path_is_called_in_main_before_render_scene(self) -> None:
        """E1 (動作): main() の llvm_* 分岐で ensure_drjit_libllvm_path を
           render_scene より前に呼んでいること（AST による検証）．

        仕様:
            runner.py の import（build_scene → sensors.py / lights.py → mitsuba）より先に呼ぶ．
            ensure_drjit_libllvm_path は llvm_* variant のとき main() 内で先行呼び出しされる．
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # main 関数の AST を取得
        main_func = next(
            (n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name == "main"),
            None,
        )
        assert main_func is not None, "main() 関数が cli/render.py に存在すること"

        # main 関数内で ensure_drjit_libllvm_path と render_scene の呼び出し順を確認
        # (AST の lineno ベースで順序を検証)
        ensure_calls: list[int] = []
        render_calls: list[int] = []

        for node in ast.walk(main_func):
            if isinstance(node, ast.Call):
                # ensure_drjit_libllvm_path の呼び出し
                if isinstance(node.func, ast.Name) and node.func.id == "ensure_drjit_libllvm_path":
                    ensure_calls.append(node.lineno)
                elif (
                    isinstance(node.func, ast.Attribute)
                    and node.func.attr == "ensure_drjit_libllvm_path"
                ):
                    ensure_calls.append(node.lineno)
                # render_scene の呼び出し
                if isinstance(node.func, ast.Name) and node.func.id == "render_scene":
                    render_calls.append(node.lineno)
                elif isinstance(node.func, ast.Attribute) and node.func.attr == "render_scene":
                    render_calls.append(node.lineno)

        # ensure_drjit_libllvm_path は main() 内に存在すること
        assert ensure_calls, (
            "main() 内に ensure_drjit_libllvm_path の呼び出しが存在すること\n"
            "仕様: llvm_* variant のとき render_scene より前に呼ぶ"
        )

        # render_scene は main() 内に存在すること
        assert render_calls, "main() 内に render_scene の呼び出しが存在すること"

        # ensure が render より前の行にあること
        assert min(ensure_calls) < min(render_calls), (
            f"ensure_drjit_libllvm_path (line {min(ensure_calls)}) は "
            f"render_scene (line {min(render_calls)}) より前に呼ばれること"
        )
