"""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 すること（処理本体が render/pipeline.py の
                  render_yaml へ移ったため，順序の検証対象もそちら）

テスト方針:
    - 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 しないこと
# ---------------------------------------------------------------------------

_SRC = Path(__file__).parent.parent.parent / "src" / "sm_tias_lightsim"
_CLI_RENDER_PATH = _SRC / "cli" / "render.py"
_PIPELINE_PATH = _SRC / "render" / "pipeline.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_main_delegates_rendering_to_render_yaml(self) -> None:
        """E1: main() は処理本体を render_yaml に委譲していること．

        仕様:
            cli/render.py は引数のパースと例外 → 終了コード変換だけを担い，
            「YAML 読込 → variant 解決 → レンダ → 保存」は render/pipeline.py が行う．
            JIT 初期化（ensure_drjit_libllvm_path）の順序保証もそちらへ移っている．
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        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 に存在すること"

        called = {
            node.func.id
            for node in ast.walk(main_func)
            if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
        }
        assert "render_yaml" in called, (
            "main() は render_yaml を呼ぶこと（レンダ処理は render/pipeline.py に集約する）"
        )


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

    仕様:
        render_yaml() は llvm_* variant のとき ensure_drjit_libllvm_path を
        render_scene（＝ mitsuba import chain）より前に呼ぶ．
        そのためにモジュールレベルでは mitsuba 依存モジュールを import しない．
    """

    def test_no_top_level_mitsuba_or_runner_import_in_pipeline(self) -> None:
        """E1: render/pipeline.py のモジュールレベルに mitsuba / runner の import が無いこと．

        仕様:
            モジュールレベルで import すると DRJIT_LIBLLVM_PATH 設定より前に
            LLVM 初期化が走ってしまう（cli/render.py と同じ遅延 import 方針）．
        """
        src = open(_PIPELINE_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        offenders: list[str] = []
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name.startswith("mitsuba"):
                        offenders.append(alias.name)
            elif isinstance(node, ast.ImportFrom) and node.module:
                if node.module.startswith("mitsuba") or "runner" in node.module:
                    offenders.append(node.module)

        assert not offenders, (
            f"render/pipeline.py のモジュールレベルに遅延すべき import がある: {offenders}．"
            "仕様: mitsuba 依存 import はすべて render_yaml() 内で行う"
        )

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

        仕様:
            runner.py の import（build_scene → sensors.py / lights.py → mitsuba）より先に呼ぶ．
        """
        src = open(_PIPELINE_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        func = next(
            (
                n
                for n in ast.walk(tree)
                if isinstance(n, ast.FunctionDef) and n.name == "render_yaml"
            ),
            None,
        )
        assert func is not None, "render_yaml() 関数が render/pipeline.py に存在すること"

        ensure_calls: list[int] = []
        render_calls: list[int] = []
        for node in ast.walk(func):
            if not isinstance(node, ast.Call):
                continue
            name = None
            if isinstance(node.func, ast.Name):
                name = node.func.id
            elif isinstance(node.func, ast.Attribute):
                name = node.func.attr
            if name == "ensure_drjit_libllvm_path":
                ensure_calls.append(node.lineno)
            elif name == "render_scene":
                render_calls.append(node.lineno)

        assert ensure_calls, (
            "render_yaml() 内に ensure_drjit_libllvm_path の呼び出しが存在すること．"
            "仕様: llvm_* variant のとき render_scene より前に呼ぶ"
        )
        assert render_calls, "render_yaml() 内に render_scene の呼び出しが存在すること"
        assert min(ensure_calls) < min(render_calls), (
            f"ensure_drjit_libllvm_path (line {min(ensure_calls)}) は "
            f"render_scene (line {min(render_calls)}) より前に呼ばれること"
        )
