"""runtime/llvm_init.py のバージョン付き libLLVM 探索（Ubuntu / Debian / Fedora）の仕様テスト．

仕様出典:
- 実装サマリー 仕様 9: 既存の Windows / POSIX 標準パスに加え，Ubuntu / Debian の
  `libLLVM-*.so`（例 `/usr/lib/x86_64-linux-gnu/libLLVM-15.so`）と `/usr/lib/llvm-*/lib/`
  （Fedora 形式も）を探す．新しい版を優先する．Windows の既存検出を壊さない．
- docs/02_ENV/ENV_02_環境構築手順.md「研究室サーバ編 / 既知事項」（LLVM）

テスト方針:
- 実ファイルシステムには依存しない．モジュールが参照する `os` と `glob` を
  モジュール内だけ差し替えて疑似的な POSIX / Windows 環境を作る
  （グローバルな os.name は変えない＝pathlib 等に影響させない）
- glob の `*` はパス区切りをまたがない．疑似 glob もその意味論を守る
"""

from __future__ import annotations

import fnmatch
import os
import types
from pathlib import Path

import pytest

import sm_tias_lightsim.runtime.llvm_init as llvm_init

_WINDOWS_STANDARD = [
    r"C:\Program Files\LLVM\bin\LLVM-C.dll",
    r"C:\Program Files (x86)\LLVM\bin\LLVM-C.dll",
    r"C:\ProgramData\chocolatey\lib\llvm\tools\LLVM\bin\LLVM-C.dll",
    r"C:\msys64\mingw64\bin\LLVM-C.dll",
    r"C:\msys64\clang64\bin\LLVM-C.dll",
]


def _fake_glob(files: set[str]):
    def glob(pattern: str) -> list[str]:
        pat = pattern.split("/")
        hits = []
        for f in files:
            parts = f.split("/")
            if len(parts) == len(pat) and all(
                fnmatch.fnmatchcase(p, q) for p, q in zip(parts, pat, strict=True)
            ):
                hits.append(f)
        return hits

    return glob


@pytest.fixture
def fake_fs(monkeypatch):
    """疑似ファイルシステムを組み立てる関数を返す．戻り値の environ は疑似 os.environ．"""

    def install(files: set[str], *, os_name: str = "posix", environ: dict | None = None):
        env = {} if environ is None else environ
        monkeypatch.setattr(llvm_init, "os", types.SimpleNamespace(name=os_name, environ=env))
        if os_name == "nt":

            def _no_glob(pattern):  # Windows 分岐では glob を使わない
                raise AssertionError(f"Windows で glob が呼ばれた: {pattern}")

            monkeypatch.setattr(llvm_init, "glob", types.SimpleNamespace(glob=_no_glob))
        else:
            monkeypatch.setattr(llvm_init, "glob", types.SimpleNamespace(glob=_fake_glob(files)))
        monkeypatch.setattr(Path, "is_file", lambda self: self.as_posix() in files)
        return env

    return install


# ---------------------------------------------------------------------------
# Ubuntu / Debian
# ---------------------------------------------------------------------------


def test_ubuntu_versioned_lib_is_found(fake_fs):
    """Newton（Ubuntu 22.04）の `/usr/lib/x86_64-linux-gnu/libLLVM-15.so` を検出すること．"""
    env = fake_fs({"/usr/lib/x86_64-linux-gnu/libLLVM-15.so"})
    result = llvm_init.ensure_drjit_libllvm_path()
    assert result == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"
    assert env["DRJIT_LIBLLVM_PATH"] == result


@pytest.mark.parametrize(
    "path",
    [
        "/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
        "/usr/lib/x86_64-linux-gnu/libLLVM.so.18.1",
        "/usr/lib/aarch64-linux-gnu/libLLVM-14.so",
    ],
)
def test_ubuntu_variants_are_found(fake_fs, path):
    fake_fs({path})
    assert llvm_init.ensure_drjit_libllvm_path() == path


def test_dev_symlink_preferred_over_soname_of_same_version(fake_fs):
    fake_fs(
        {
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
        }
    )
    assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"


def test_newest_version_is_preferred(fake_fs):
    """複数の版があるときは新しい版を優先すること（数値比較．9 < 11 < 15）．"""
    fake_fs(
        {
            "/usr/lib/x86_64-linux-gnu/libLLVM-9.so",
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
            "/usr/lib/x86_64-linux-gnu/libLLVM-11.so",
        }
    )
    assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"


def test_newest_version_is_preferred_across_directories(fake_fs):
    fake_fs(
        {
            "/usr/lib/llvm-14/lib/libLLVM-14.so",
            "/usr/lib/x86_64-linux-gnu/libLLVM-17.so.1",
        }
    )
    assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-17.so.1"


def test_libllvm_c_versions_sorted_numerically(fake_fs):
    """既存の `/usr/lib/llvm-*/lib/libLLVM-C.so` も新しい版が先（辞書順の 9 > 15 にしない）．"""
    fake_fs({"/usr/lib/llvm-9/lib/libLLVM-C.so", "/usr/lib/llvm-15/lib/libLLVM-C.so"})
    assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/llvm-15/lib/libLLVM-C.so"


# ---------------------------------------------------------------------------
# /usr/lib/llvm-*/lib/（Fedora 形式を含む）と /usr/lib64
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "path",
    [
        "/usr/lib/llvm-17/lib/libLLVM.so",
        "/usr/lib/llvm-16/lib/libLLVM-16.so",
        "/usr/lib/llvm-18/lib/libLLVM.so.18.1",
        "/usr/lib/llvm-18/lib/libLLVM-18.so.1",
        "/usr/lib64/libLLVM-17.so",
        "/usr/lib64/libLLVM.so",
        "/usr/lib64/libLLVM.so.18.1",
    ],
)
def test_llvm_dir_and_fedora_forms_are_found(fake_fs, path):
    fake_fs({path})
    assert llvm_init.ensure_drjit_libllvm_path() == path


# ---------------------------------------------------------------------------
# 既存挙動の維持・候補リストの性質
# ---------------------------------------------------------------------------


def test_existing_libllvm_c_is_still_found(fake_fs):
    fake_fs({"/usr/lib/llvm-15/lib/libLLVM-C.so"})
    assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/llvm-15/lib/libLLVM-C.so"


def test_existing_fixed_posix_paths_remain_candidates(fake_fs):
    fake_fs(set())
    candidates = llvm_init._iter_default_candidates()
    for path in [
        "/usr/local/lib/libLLVM-C.so",
        "/opt/homebrew/opt/llvm/lib/libLLVM-C.dylib",
        "/usr/local/opt/llvm/lib/libLLVM-C.dylib",
    ]:
        assert path in candidates


def test_all_forms_are_candidates_and_newest_first(fake_fs):
    """全形式を候補に含み，同一バージョンが並んでも .so（開発用リンク）から試すこと．

    ※ `libLLVM-*.so` パターンは `libLLVM-C.so` にも一致するため，libLLVM-C.so が候補に
    2 回現れうる（is_file の確認が 1 回増えるだけで結果は変わらない）．仕様上の要件では
    ないので重複の有無は検証しない．
    """
    fake_fs(
        {
            "/usr/lib/llvm-15/lib/libLLVM-C.so",
            "/usr/lib/llvm-15/lib/libLLVM.so",
            "/usr/lib/llvm-15/lib/libLLVM-15.so",
            "/usr/lib/llvm-15/lib/libLLVM-15.so.1",
            "/usr/lib/llvm-15/lib/libLLVM.so.15",
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
            "/usr/lib/x86_64-linux-gnu/libLLVM.so.15",
            "/usr/lib64/libLLVM-15.so",
        }
    )
    candidates = llvm_init._iter_default_candidates()
    for path in [
        "/usr/lib/llvm-15/lib/libLLVM.so",
        "/usr/lib/llvm-15/lib/libLLVM-15.so",
        "/usr/lib/llvm-15/lib/libLLVM.so.15",
        "/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
        "/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
        "/usr/lib/x86_64-linux-gnu/libLLVM.so.15",
        "/usr/lib64/libLLVM-15.so",
    ]:
        assert path in candidates
    so_1 = candidates.index("/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1")
    assert candidates.index("/usr/lib/x86_64-linux-gnu/libLLVM-15.so") < so_1


def test_unrelated_libraries_are_not_candidates(fake_fs, caplog):
    """libLLVM 以外（静的ライブラリ・別名のライブラリ）は拾わないこと．"""
    env = fake_fs(
        {
            "/usr/lib/x86_64-linux-gnu/libLLVM-15.a",
            "/usr/lib/x86_64-linux-gnu/libLLVMSupport.so",
            "/usr/lib/x86_64-linux-gnu/libclang-15.so",
            "/usr/lib/x86_64-linux-gnu/sub/libLLVM-15.so",  # glob の * は / をまたがない
        }
    )
    assert llvm_init.ensure_drjit_libllvm_path() is None
    assert "DRJIT_LIBLLVM_PATH" not in env


def test_existing_env_is_respected_on_posix(fake_fs):
    env = fake_fs(
        {"/usr/lib/x86_64-linux-gnu/libLLVM-15.so"},
        environ={"DRJIT_LIBLLVM_PATH": "/custom/libLLVM.so"},
    )
    assert llvm_init.ensure_drjit_libllvm_path() == "/custom/libLLVM.so"
    assert env["DRJIT_LIBLLVM_PATH"] == "/custom/libLLVM.so"


# ---------------------------------------------------------------------------
# Windows の既存検出を壊していない
# ---------------------------------------------------------------------------


def test_windows_candidates_unchanged(fake_fs):
    """Windows 分岐の候補は従来の 5 パスのまま（POSIX の glob を呼ばない）．"""
    fake_fs(set(), os_name="nt")
    assert llvm_init._iter_default_candidates() == _WINDOWS_STANDARD


def test_windows_llvm_dir_still_first(fake_fs):
    fake_fs(set(), os_name="nt", environ={"LLVM_DIR": r"D:\llvm"})
    candidates = llvm_init._iter_default_candidates()
    assert candidates[0] == str(Path(r"D:\llvm") / "bin" / "LLVM-C.dll")
    assert candidates[1:] == _WINDOWS_STANDARD


@pytest.mark.skipif(os.name != "nt", reason="実 Windows 環境での回帰確認")
def test_real_windows_candidates_have_no_posix_paths(monkeypatch):
    monkeypatch.delenv("LLVM_DIR", raising=False)
    assert llvm_init._iter_default_candidates() == _WINDOWS_STANDARD
