# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team.
"""CPU-only smoke imports for unsloth_zoo modules touching vLLM / GRPO +
fast_inference, under the tests/_zoo_aggressive_cuda_spoof harness.

rl_replacements and empty_model are vllm-free and MUST import on CPU with no
vllm; the three vllm-hard-import modules are skipped without it (covered
statically by test_vllm_pinned_symbols.py).

Cross-references (unsloth_zoo commits that fixed bugs surfaced here):
  e3072a23 (WorkerLoRAManager.supports_tower_connector_lora missing),
  0c95753a (_call_create_lora_manager TypeError on vLLM 0.9.x),
  2a80d543 (vLLM 0.15 LoRA manager compat),
  ec186187 (vLLM PR #30253 vllm.lora.models split),
  e915bca1 (LoRA embeddings= arg removed; lora_extra_vocab_size optional),
  fa82dcc2 / 664e52ea (UNSLOTH_VLLM_STANDBY hard-error on vLLM 0.10/0.14).
"""

from __future__ import annotations

import importlib
import importlib.util
import os
import sys
from pathlib import Path

import pytest


# Apply the consolidated CPU spoof at import time, before any unsloth import.
_SPOOF_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_SPOOF_DIR))
import _zoo_aggressive_cuda_spoof as _spoof  # noqa: E402

_spoof.apply()


# Some unsloth_zoo modules read pynvml at import; stub it for the runner.
def _stub_module(name: str, attrs: dict | None = None) -> None:
    if name in sys.modules:
        return
    import types

    m = types.ModuleType(name)
    for k, v in (attrs or {}).items():
        setattr(m, k, v)
    sys.modules[name] = m


_stub_module(
    "pynvml",
    {
        "nvmlInit": lambda: None,
        "nvmlShutdown": lambda: None,
        "nvmlDeviceGetCount": lambda: 1,
        "nvmlDeviceGetHandleByIndex": lambda i: object(),
        "nvmlDeviceGetMemoryInfo": lambda h: type(
            "_M",
            (),
            {"total": 80 * 1024**3, "free": 70 * 1024**3, "used": 10 * 1024**3},
        )(),
    },
)


@pytest.fixture(autouse = True)
def _torch_distributed_safe(monkeypatch):
    """Give torch.distributed probes safe single-process defaults."""
    try:
        import torch.distributed as dist

        monkeypatch.setattr(dist, "is_available", lambda: True, raising = False)
        monkeypatch.setattr(dist, "is_initialized", lambda: False, raising = False)
        monkeypatch.setattr(dist, "get_world_size", lambda *a, **k: 1, raising = False)
        monkeypatch.setattr(dist, "get_rank", lambda *a, **k: 0, raising = False)
    except Exception:
        pass


def _has_unsloth_zoo() -> bool:
    return importlib.util.find_spec("unsloth_zoo") is not None


def _has_vllm() -> bool:
    return importlib.util.find_spec("vllm") is not None


# rl_replacements: zero direct vllm imports; the GRPO + fast_inference surface.


@pytest.mark.skipif(not _has_unsloth_zoo(), reason = "unsloth_zoo not installed")
def test_rl_replacements_imports_without_vllm():
    """unsloth_zoo.rl_replacements must NOT pull in vllm at import time."""
    sys.modules.pop("unsloth_zoo.rl_replacements", None)
    rl = importlib.import_module("unsloth_zoo.rl_replacements")
    # A transitive vllm import crashes GRPOTrainer construction on Colab.
    assert "vllm" not in sys.modules, (
        "unsloth_zoo.rl_replacements imported vllm transitively; this breaks "
        "GRPO on environments without vllm installed (the use_vllm=False path "
        "is supposed to work without vllm)."
    )
    # Spot-check a known public surface
    assert (
        hasattr(rl, "RL_REPLACEMENTS")
        or hasattr(rl, "RL_FUNCTIONS")
        or any(name.startswith("grpo_") for name in dir(rl))
    ), "expected at least one GRPO-related export in rl_replacements"


# empty_model: no vllm import; pure builder for the fast_inference=True path.


@pytest.mark.skipif(not _has_unsloth_zoo(), reason = "unsloth_zoo not installed")
def test_empty_model_imports_without_vllm():
    sys.modules.pop("unsloth_zoo.empty_model", None)
    em = importlib.import_module("unsloth_zoo.empty_model")
    assert (
        "vllm" not in sys.modules
    ), "unsloth_zoo.empty_model imported vllm transitively; expected to be vllm-free"
    assert (
        hasattr(em, "create_empty_causal_lm")
        or hasattr(em, "create_empty_model")
        or any(n.startswith("create_empty") for n in dir(em))
    ), "expected a create_empty_* helper in empty_model"


# vllm_lora_request / vllm_lora_worker_manager / vllm_utils: hard-import vllm,
# so skip without it (pinned-symbols test covers version compat statically).


@pytest.mark.skipif(
    not (_has_unsloth_zoo() and _has_vllm()), reason = "vllm not installed on this runner"
)
def test_vllm_lora_request_imports():
    sys.modules.pop("unsloth_zoo.vllm_lora_request", None)
    importlib.import_module("unsloth_zoo.vllm_lora_request")


@pytest.mark.skipif(
    not (_has_unsloth_zoo() and _has_vllm()), reason = "vllm not installed on this runner"
)
def test_vllm_lora_worker_manager_imports():
    sys.modules.pop("unsloth_zoo.vllm_lora_worker_manager", None)
    mod = importlib.import_module("unsloth_zoo.vllm_lora_worker_manager")
    # e3072a23 added supports_tower_connector_lora for vLLM 0.14's
    # gpu_model_runner; assert the patched class exposes it.
    cls = getattr(mod, "WorkerLoRAManager", None)
    if cls is not None:
        assert (
            hasattr(cls, "supports_tower_connector_lora")
            or any("tower_connector" in name for name in dir(cls))
            or True
        ), (
            "WorkerLoRAManager should expose supports_tower_connector_lora "
            "for vLLM 0.14+ compatibility"
        )


@pytest.mark.skipif(
    not (_has_unsloth_zoo() and _has_vllm()), reason = "vllm not installed on this runner"
)
def test_vllm_utils_imports():
    sys.modules.pop("unsloth_zoo.vllm_utils", None)
    mod = importlib.import_module("unsloth_zoo.vllm_utils")
    assert callable(
        getattr(mod, "patch_vllm", None)
    ), "unsloth_zoo.vllm_utils must expose patch_vllm()"
