# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team.
"""Pinned-symbol compat check across vLLM PyPI minors >= 0.9.0 (GitHub raw-fetch, no pip/GPU).

Catches API drift like vLLM PR #30253 (vllm.lora.models split), 0.14
supports_tower_connector_lora(), 0.15 create_lora_manager rename, the
lora_path -> lora_dir rename, and the 0.11 v0 graph-capture removal.
Asserts every symbol unsloth-zoo's vllm_utils + vllm_lora_* expects is present.
"""

from __future__ import annotations

import json
import os
import re
import urllib.error
import urllib.request

import pytest


# Last patch release of each tracked vLLM minor (or first stable if none yet).
VLLM_TAGS = [
    "v0.9.0",
    "v0.9.2",
    "v0.10.0",
    "v0.10.2",
    "v0.11.0",
    "v0.12.0",
    "v0.13.0",
    "v0.14.0",
    "v0.15.0",
    "v0.16.0",
    "v0.17.1",
    "v0.18.1",
    "v0.19.1",
    "v0.20.1",
    # `main` catches drift before it ships to PyPI.
    "main",
]


def _fetch_text(repo: str, ref: str, path: str) -> str | None:
    """Fetch a file's text from GitHub; None on 404 (renamed/removed, informational)."""
    url = f"https://raw.githubusercontent.com/{repo}/{ref}/{path}"
    req = urllib.request.Request(url)
    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
    if token:
        req.add_header("Authorization", f"Bearer {token}")
    try:
        with urllib.request.urlopen(req, timeout = 15) as r:
            return r.read().decode("utf-8", errors = "replace")
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return None
        pytest.skip(f"GitHub fetch failed ({e.code}) for {url}")
    except (urllib.error.URLError, TimeoutError) as e:
        pytest.skip(f"GitHub fetch failed ({e}) for {url}")


def _has_def(
    src: str,
    name: str,
    kind: str = "any",
) -> bool:
    """Grep for `class Name`/`def name`/`Name = ...`; avoids ast.parse so one bad line doesn't false-fail."""
    if kind in ("any", "class") and re.search(rf"^class\s+{re.escape(name)}\b", src, re.MULTILINE):
        return True
    if kind in ("any", "func") and re.search(
        rf"^(?:async\s+)?def\s+{re.escape(name)}\b", src, re.MULTILINE
    ):
        return True
    if kind == "any" and re.search(rf"^{re.escape(name)}\s*[:=]", src, re.MULTILINE):
        return True
    return False


# HARD-import symbols: must be present in every tested version.


@pytest.mark.parametrize("tag", VLLM_TAGS)
def test_vllm_lora_request_hard_imports(tag: str):
    """LoRARequest, get_adapter_absolute_path, PEFTHelper -- hard-imported by unsloth-zoo's vllm_lora_worker_manager."""
    src = _fetch_text("vllm-project/vllm", tag, "vllm/lora/request.py")
    assert src is not None, f"vllm/lora/request.py missing in {tag}"
    assert _has_def(
        src, "LoRARequest", "class"
    ), f"vllm/lora/request.py:LoRARequest missing in {tag} (unsloth-zoo HARD-imports it)"

    src_utils = _fetch_text("vllm-project/vllm", tag, "vllm/lora/utils.py")
    assert src_utils is not None, f"vllm/lora/utils.py missing in {tag}"
    assert _has_def(
        src_utils, "get_adapter_absolute_path", "func"
    ), f"vllm/lora/utils.py:get_adapter_absolute_path missing in {tag}"

    src_peft = _fetch_text("vllm-project/vllm", tag, "vllm/lora/peft_helper.py")
    assert src_peft is not None, f"vllm/lora/peft_helper.py missing in {tag}"
    assert _has_def(
        src_peft, "PEFTHelper", "class"
    ), f"vllm/lora/peft_helper.py:PEFTHelper missing in {tag}"


@pytest.mark.parametrize("tag", VLLM_TAGS)
def test_vllm_config_lora_config(tag: str):
    """vllm.config.LoRAConfig -- hard-imported at module top of unsloth_zoo.vllm_lora_worker_manager."""
    candidates = [
        "vllm/config/__init__.py",
        "vllm/config.py",
        "vllm/config/lora.py",
    ]
    found = False
    for path in candidates:
        src = _fetch_text("vllm-project/vllm", tag, path)
        if src is None:
            continue
        if _has_def(src, "LoRAConfig", "class") or "LoRAConfig" in src:
            found = True
            break
    assert found, f"vllm.config.LoRAConfig missing in {tag} (checked {candidates})"


# SOFT-import symbols: either old path or new post-#30253 path is fine.


@pytest.mark.parametrize("tag", VLLM_TAGS)
def test_vllm_lora_models_either_path(tag: str):
    """The LoRA model/manager symbols must resolve via EITHER vllm.lora.models OR the post-#30253 split path."""
    needed = {
        "LoRAModel": ("class", None),
        "LoRAModelManager": ("class", None),
        "LRUCacheLoRAModelManager": ("class", None),
        "create_lora_manager": ("func", None),
    }
    # Old path: single vllm/lora/models.py (or models/__init__.py).
    old_candidates = ["vllm/lora/models.py", "vllm/lora/models/__init__.py"]
    old_src = next(
        (s for s in (_fetch_text("vllm-project/vllm", tag, p) for p in old_candidates) if s),
        None,
    )
    if old_src is not None:
        if all(_has_def(old_src, n, k) for n, (k, _) in needed.items()):
            return  # All resolve through the legacy single-file path.

    # New path (post vLLM PR #30253):
    lora_model_src = _fetch_text("vllm-project/vllm", tag, "vllm/lora/lora_model.py")
    model_mgr_src = _fetch_text("vllm-project/vllm", tag, "vllm/lora/model_manager.py")

    if lora_model_src is None and model_mgr_src is None:
        pytest.fail(
            f"{tag}: neither legacy vllm/lora/models.py nor split "
            f"vllm/lora/{{lora_model,model_manager}}.py found; "
            f"unsloth-zoo's try/except will fail-closed at import"
        )

    combined = (lora_model_src or "") + "\n" + (model_mgr_src or "")
    missing = [n for n, (k, _) in needed.items() if not _has_def(combined, n, k)]
    if missing:
        pytest.fail(
            f"{tag}: post-#30253 path missing symbols {missing}. "
            f"unsloth-zoo's try/except for vllm.lora.models will fall "
            f"through to the new path and crash."
        )


# Optional / version-gated symbols: assert presence only on minors claiming support.


@pytest.mark.parametrize("tag", VLLM_TAGS)
def test_vllm_worker_lora_manager_class(tag: str):
    """vllm.lora.worker_manager.WorkerLoRAManager -- unsloth-zoo subclasses it; signature drives old_init vs new_init."""
    src = _fetch_text("vllm-project/vllm", tag, "vllm/lora/worker_manager.py")
    if src is None:
        # Some vLLM versions split this; check fallback locations.
        alt = _fetch_text("vllm-project/vllm", tag, "vllm/v1/worker/lora_model_runner_mixin.py")
        if alt and ("WorkerLoRAManager" in alt or "LoRAModelRunnerMixin" in alt):
            return
        pytest.fail(
            f"{tag}: vllm/lora/worker_manager.py and "
            f"vllm/v1/worker/lora_model_runner_mixin.py both missing"
        )
    assert (
        _has_def(src, "WorkerLoRAManager", "class") or "WorkerLoRAManager" in src
    ), f"{tag}: vllm.lora.worker_manager.WorkerLoRAManager not in source"


@pytest.mark.parametrize("tag", VLLM_TAGS)
def test_lora_request_no_removed_kwargs(tag: str):
    """vLLM renamed lora_local_path -> lora_path -> lora_dir; assert LoRARequest still accepts lora_dir or lora_path."""
    src = _fetch_text("vllm-project/vllm", tag, "vllm/lora/request.py")
    assert src is not None
    has_dir = bool(re.search(r"\blora_dir\b", src))
    has_path = bool(re.search(r"\blora_path\b", src))
    assert has_dir or has_path, f"{tag}: vllm.lora.request has neither lora_dir nor lora_path"


# UNSLOTH_VLLM_STANDBY hard-error windows: unsloth-zoo refuses standby on
#   0.10.0 <= vllm < 0.11.0 (std::bad_alloc) and 0.14.0 <= vllm < 0.15.0 (cudaErrorIllegalAddress).


def _vllm_zoo_local_path() -> str | None:
    """Return the on-runner path to unsloth_zoo.vllm_utils source, or None."""
    try:
        import importlib.util
        spec = importlib.util.find_spec("unsloth_zoo.vllm_utils")
        if spec and spec.origin:
            return spec.origin
    except Exception:
        pass
    return None


def test_unsloth_zoo_standby_guards_present():
    """Sanity: the two hard-error windows exist in unsloth_zoo.vllm_utils; catches a revert that drops them."""
    path = _vllm_zoo_local_path()
    if path is None:
        pytest.skip("unsloth_zoo not installed on runner")
    src = open(path, encoding = "utf-8").read()
    has_10x_guard = re.search(r"0\.10\.0", src) and re.search(r"standby", src, re.IGNORECASE)
    has_14x_guard = re.search(r"0\.14\.0", src) and re.search(r"standby", src, re.IGNORECASE)
    assert has_10x_guard or has_14x_guard, (
        "unsloth_zoo.vllm_utils dropped the UNSLOTH_VLLM_STANDBY "
        "version-gate against vLLM 0.10.x / 0.14.x; that re-introduces the "
        "std::bad_alloc and cudaErrorIllegalAddress crashes the team fixed "
        "in unsloth-zoo commits 664e52ea / fa82dcc2."
    )
