# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team.
"""Pinned-symbol compat check across PEFT minor versions unsloth + unsloth-zoo
target. For each tracked tag, fetch source from github.com/huggingface/peft and
assert every PEFT symbol unsloth touches is present, catching API drift.
Versioning covers unsloth/pyproject.toml's `peft>=0.18.0,!=0.11.0` window + main.
"""

from __future__ import annotations

import re

import pytest

from tests.version_compat._fetch import fetch_text, first_match, has_def


# pyproject pin: peft>=0.18.0. Test the floor + each minor since.
# `main` catches breakage before a release lands.
PEFT_TAGS = [
    "v0.18.0",
    "v0.18.1",
    "v0.19.0",
    "v0.19.1",
    "main",
]


# Top-level re-exports: sentence_transformer.py:1948 does `from peft import
# LoraConfig, get_peft_model`; unsloth_zoo saving_utils/lora extractors hit PeftModel.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_top_level_exports(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/__init__.py")
    assert src is not None, f"{tag}: src/peft/__init__.py missing"
    needed = (
        "LoraConfig",
        "get_peft_model",
        "PeftModel",
    )
    missing = [n for n in needed if n not in src]
    assert not missing, (
        f"{tag}: peft top-level missing {missing}; "
        f"unsloth.models.sentence_transformer:1948 + unsloth-zoo saving_utils "
        f"will ImportError"
    )


# LoraConfig at the canonical sub-module path: peft.tuners.lora.LoraConfig
# (or peft.tuners.lora.config.LoraConfig). unsloth-zoo's normaliser inspects
# it via getattr() and dataclass field introspection.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_lora_config_class(tag: str):
    candidates = [
        "src/peft/tuners/lora/config.py",
        "src/peft/tuners/lora/__init__.py",
        "src/peft/tuners/lora.py",
    ]
    found_in = []
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is not None and has_def(src, "LoraConfig", "class"):
            found_in.append(p)
    assert found_in, f"{tag}: peft.tuners.lora.LoraConfig not in any of {candidates}"


# get_peft_model: top-level helper used by sentence_transformer.py:2043.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_get_peft_model_function(tag: str):
    """get_peft_model may live in mapping.py or mapping_func.py (0.18+ split)."""
    candidates = [
        "src/peft/mapping.py",
        "src/peft/mapping_func.py",
        "src/peft/__init__.py",
        "src/peft/peft_model.py",
    ]
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is not None and has_def(src, "get_peft_model", "func"):
            return
    pytest.fail(f"{tag}: def get_peft_model(...) not found in any of {candidates}")


# LoraLayer base class: unsloth-zoo's MoE LoRA extractor walks subclasses of
# peft.tuners.lora.LoraLayer. A rename/move makes the walk silently return 0.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_lora_layer_class(tag: str):
    candidates = [
        "src/peft/tuners/lora/layer.py",
        "src/peft/tuners/lora/__init__.py",
        "src/peft/tuners/lora.py",
    ]
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is not None and has_def(src, "LoraLayer", "class"):
            return
    pytest.fail(
        f"{tag}: class LoraLayer not in any of {candidates} — "
        f"unsloth-zoo MoE LoRA extractor relies on isinstance checks "
        f"against this class"
    )


# bnb-aware LoRA: peft.tuners.lora.bnb is the bitsandbytes integration point.
# Missing it -> 4bit LoRA silently falls back to fp16 (bigger memory footprint).


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_lora_bnb_integration(tag: str):
    candidates = [
        "src/peft/tuners/lora/bnb.py",
        "src/peft/tuners/lora/_bnb.py",
    ]
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is None:
            continue
        # At least one bnb-flavoured Linear must exist (either name is fine).
        has_4bit = any(
            cls in src
            for cls in (
                "class Linear4bit",
                "class Linear8bitLt",
                "class _Linear4bit",
                "class _Linear8bitLt",
            )
        )
        if has_4bit:
            return
    pytest.fail(
        f"{tag}: peft.tuners.lora.bnb missing or no Linear4bit/Linear8bitLt "
        f"class found; unsloth's 4-bit LoRA path silently degrades to fp16"
    )


# Coverage extension (added 2026-05): symbols from the 8-PR audit
#   unsloth#5015, #5167, #5036, #4807 + unsloth-zoo#618, #596, #482, #430.


# 1. peft.tuners.lora.layer.VARIANT_KWARG_KEYS — added in peft 0.18.
#    unsloth-zoo#430 injects the import into the compiled forward.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_variant_kwarg_keys_const(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/layer.py")
    if src is None:
        pytest.skip(f"{tag}: src/peft/tuners/lora/layer.py missing")
    if "VARIANT_KWARG_KEYS" not in src:
        pytest.fail(
            f"{tag}: peft.tuners.lora.layer.VARIANT_KWARG_KEYS missing; "
            f"unsloth_zoo/compiler.py:2645 import injection breaks (unsloth-zoo#430)"
        )


# 2. peft.tuners.lora.layer.ParamWrapper — peft 0.18 added the class for MoE
#    3D-parameter LoRA. unsloth-zoo#618 monkey-patches the MoE LoRA extractor.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_param_wrapper_class(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/layer.py")
    if src is None:
        pytest.skip(f"{tag}: layer.py missing")
    assert has_def(src, "ParamWrapper", "class"), (
        f"{tag}: peft.tuners.lora.layer.ParamWrapper missing; "
        f"unsloth_zoo/temporary_patches/qwen3_moe.py:43-130 + "
        f"moe_utils.py:757 ImportError (unsloth-zoo#618)"
    )
    # Member names: informational only; the real bug to catch is full removal.
    for name in ("parameter_name", "forward", "lora_A", "get_base_layer"):
        _present = name in src


# 3. peft.tuners.lora.LoraConfig.target_parameters — peft 0.19+. Used by
#    unsloth-zoo's MoE target-parameter extractor.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_lora_config_target_parameters(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/config.py")
    if src is None:
        pytest.skip(f"{tag}: src/peft/tuners/lora/config.py missing")
    # Optional on 0.18.x, required from 0.19.0+
    has_it = "target_parameters" in src
    if "0.18" in tag and not has_it:
        pytest.skip(f"{tag}: target_parameters not yet introduced (peft 0.18)")
    assert has_it, (
        f"{tag}: LoraConfig.target_parameters missing on peft >=0.19; "
        f"unsloth-zoo MoE target-parameter extraction breaks"
    )


# 4. peft.tuners.lora.model.LoraModel._create_and_replace — unsloth#4807
#    monkey-patches this for Gemma4ClippableLinear.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_lora_model_create_and_replace(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/model.py")
    if src is None:
        pytest.skip(f"{tag}: src/peft/tuners/lora/model.py missing")
    assert has_def(src, "LoraModel", "class"), f"{tag}: class LoraModel missing"
    assert has_def(src, "_create_and_replace", "func"), (
        f"{tag}: LoraModel._create_and_replace missing; "
        f"unsloth/models/loader.py:1535-1601 monkey-patch breaks (unsloth#4807)"
    )


# 5. peft.utils.transformers_weight_conversion.build_peft_weight_mapping —
#    unsloth#5167 wraps it to handle WeightConversion.__init__ kwargs.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_transformers_weight_conversion_module(tag: str):
    candidates = [
        "src/peft/utils/transformers_weight_conversion.py",
        "src/peft/utils/transformers_weight_conversion/__init__.py",
    ]
    hit = first_match("huggingface/peft", tag, candidates)
    if hit is None:
        pytest.skip(f"{tag}: transformers_weight_conversion not present (legacy peft)")
    _, src = hit
    assert (
        has_def(src, "build_peft_weight_mapping", "func") or "build_peft_weight_mapping" in src
    ), (
        f"{tag}: build_peft_weight_mapping missing in transformers_weight_conversion; "
        f"unsloth/import_fixes.py:1375-1456 wrap breaks (unsloth#5167)"
    )


# 6. peft.utils.integrations.dequantize_module_weight — used by 3 unsloth/
#    unsloth-zoo callsites.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_integrations_dequantize_module_weight(tag: str):
    candidates = [
        "src/peft/utils/integrations.py",
        "src/peft/utils/integrations/__init__.py",
    ]
    hit = first_match("huggingface/peft", tag, candidates)
    assert hit is not None, f"{tag}: src/peft/utils/integrations[.py|/__init__.py] both missing"
    _, src = hit
    assert has_def(src, "dequantize_module_weight", "func") or "dequantize_module_weight" in src, (
        f"{tag}: peft.utils.integrations.dequantize_module_weight missing; "
        f"unsloth-zoo vllm_utils.py:2701, unsloth/_utils.py:1550, "
        f"saving_utils.py:270 ImportError"
    )


# 7. peft.PeftType.LORA — used by unsloth-zoo vllm_utils.py:2520-2559.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_type_lora_enum(tag: str):
    candidates = [
        "src/peft/utils/peft_types.py",
        "src/peft/utils/__init__.py",
        "src/peft/__init__.py",
    ]
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is None:
            continue
        # Either `class PeftType(...)` definition with LORA member, or
        # re-export from a submodule.
        if "PeftType" in src and ("LORA" in src or "lora" in src.lower()):
            return
    pytest.fail(
        f"{tag}: peft.PeftType (with LORA member) not in any of {candidates}; "
        f"unsloth-zoo vllm_utils.py:2520 reference breaks"
    )


# 8. peft.utils.ModulesToSaveWrapper — both peft.utils.* and
#    peft.utils.other.* import paths used.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_modules_to_save_wrapper(tag: str):
    candidates = [
        "src/peft/utils/other.py",
        "src/peft/utils/__init__.py",
    ]
    found_in = []
    for p in candidates:
        src = fetch_text("huggingface/peft", tag, p)
        if src is None:
            continue
        if has_def(src, "ModulesToSaveWrapper", "class"):
            found_in.append(p)
    assert found_in, (
        f"{tag}: ModulesToSaveWrapper not defined in {candidates}; "
        f"unsloth/training_utils.py:239 + models/llama.py:153 ImportError"
    )


# 9. peft.PeftModel.from_pretrained signature pin — unsloth#4807.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_peft_model_from_pretrained_signature(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/peft_model.py")
    assert src is not None, f"{tag}: src/peft/peft_model.py missing"
    # Just check the method name; the full kwarg list is too brittle.
    assert has_def(
        src, "from_pretrained", "func"
    ), f"{tag}: PeftModel.from_pretrained missing in peft_model.py"


# 10. peft.__version__ exported via known mechanism.


@pytest.mark.parametrize("tag", PEFT_TAGS)
def test_peft_version_parseable(tag: str):
    src = fetch_text("huggingface/peft", tag, "src/peft/__init__.py")
    assert src is not None
    # Same gates as the TRL test: literal / submodule / metadata
    has_literal = bool(re.search(r'^__version__\s*=\s*["\']', src, re.MULTILINE))
    has_subimport = bool(re.search(r"^from\s+\.version\s+import\s+__version__", src, re.MULTILINE))
    has_metadata = bool(
        re.search(
            r"^from\s+importlib\.metadata\s+import\s+(?:[\w,\s]+,\s*)?version",
            src,
            re.MULTILINE,
        )
        and re.search(r"^\s*__version__\s*=\s*version\s*\(", src, re.MULTILINE)
    )
    assert (
        has_literal or has_subimport or has_metadata
    ), f"{tag}: peft.__version__ not exported via any known mechanism"
