"""Dependency compatibility tests for the sageLLM umbrella package (#72 / #73).

Validates that:
1. All declared sub-package dependencies can be imported.
2. The installed versions satisfy the ``pyproject.toml`` lower bounds.
3. There are no version conflicts in the installed environment.
4. Architecture conformance: no sub-package imports from a higher-level
   package (dependency hierarchy).

These tests run entirely on CPU without GPU/hardware requirements.
"""

from __future__ import annotations

import importlib
import importlib.metadata
import json
import re

import pytest


def _is_editable_install(pkg_name: str) -> bool:
    """Return True if *pkg_name* is installed in editable (local) mode."""
    try:
        dist = importlib.metadata.distribution(pkg_name)
        # direct_url.json with "editable": true indicates an editable install
        direct_url = dist.read_text("direct_url.json")
        if direct_url:
            info = json.loads(direct_url)
            if info.get("dir_info", {}).get("editable"):
                return True
        return False
    except Exception:
        return False


# ---------------------------------------------------------------------------
# Expected installed package names and their minimum versions
# (keep in sync with pyproject.toml [project.dependencies])
# ---------------------------------------------------------------------------

# Minimum versions aligned with pyproject.toml lower bounds.
# These match the installed dev-editable versions (see constraints.txt).
CORE_DEPENDENCIES: list[tuple[str, str]] = [
    ("isagellm-protocol", "0.5.2.10"),
    ("isagellm-backend", "0.5.3.9"),
    ("isagellm-core", "0.5.2.12"),
    ("isagellm-control-plane", "0.5.2.11"),
    ("isagellm-gateway", "0.5.1.0"),
    ("isagellm-kv-cache", "0.5.2.10"),
    ("isagellm-comm", "0.5.2.7"),
    ("isagellm-compression", "0.5.2.12"),
    ("isagellm-benchmark", "0.5.1.6"),
]

# Python module names for importability check
CORE_MODULES: list[str] = [
    "sagellm_protocol",
    "sagellm_backend",
    "sagellm_core",
    "sagellm_control",
    "sagellm_gateway",
    "sagellm_kv_cache",
    "sagellm_comm",
    "sagellm_compression",
]


def _parse_version(vstr: str) -> tuple[int, ...]:
    """Convert a version string like '0.5.2.11' to a comparable tuple."""
    return tuple(int(x) for x in re.split(r"[.\-]", vstr) if x.isdigit())


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestSubpackageVersions:
    """Issue #72/#73: verify installed sub-package versions meet lower bounds."""

    @pytest.mark.parametrize("pkg_name,min_version", CORE_DEPENDENCIES)
    def test_package_installed(self, pkg_name: str, min_version: str) -> None:
        """Package must be installed (importlib.metadata lookup succeeds)."""
        try:
            installed = importlib.metadata.version(pkg_name)
        except importlib.metadata.PackageNotFoundError:
            pytest.fail(
                f"Required package '{pkg_name}' is not installed.\n"
                f"Run: pip install {pkg_name}>={min_version}"
            )
        assert installed, f"{pkg_name} has empty version string"

    @pytest.mark.parametrize("pkg_name,min_version", CORE_DEPENDENCIES)
    def test_package_meets_minimum_version(self, pkg_name: str, min_version: str) -> None:
        """Installed version must be >= minimum required."""
        try:
            installed = importlib.metadata.version(pkg_name)
        except importlib.metadata.PackageNotFoundError:
            pytest.skip(f"{pkg_name} not installed — skipping version check")

        inst_tuple = _parse_version(installed)
        min_tuple = _parse_version(min_version)
        assert inst_tuple >= min_tuple, (
            f"{pkg_name}: installed {installed!r} < required {min_version!r}\n"
            f"Update with: pip install '{pkg_name}>={min_version}'"
        )

    @pytest.mark.parametrize("pkg_name,min_version", CORE_DEPENDENCIES)
    def test_package_below_major_upper_bound(self, pkg_name: str, min_version: str) -> None:
        """Installed version must be < 0.6.0 (major upper bound from pyproject.toml)."""
        try:
            installed = importlib.metadata.version(pkg_name)
        except importlib.metadata.PackageNotFoundError:
            pytest.skip(f"{pkg_name} not installed")

        inst_tuple = _parse_version(installed)
        assert inst_tuple < (0, 6, 0), (
            f"{pkg_name}: installed {installed!r} exceeds <0.6.0 upper bound.\n"
            f"This may cause API incompatibilities — pin to 0.5.x."
        )


class TestSubpackageImportability:
    """Ensure core sub-packages are importable (basic smoke)."""

    @pytest.mark.parametrize("module_name", CORE_MODULES)
    def test_module_importable(self, module_name: str) -> None:
        """Each sub-package module must be importable without errors."""
        try:
            importlib.import_module(module_name)
        except ImportError as exc:
            pytest.fail(
                f"Cannot import '{module_name}': {exc}\n"
                f"Ensure the corresponding isagellm-* package is installed."
            )


class TestUmbrellaExports:
    """Verify sagellm (umbrella) re-exports expected symbols."""

    REQUIRED_EXPORTS: list[str] = [
        "__version__",
        "Request",
        "Response",
        "Metrics",
        "Error",
        "ErrorCode",
        "LLMEngine",
        "LLMEngineConfig",
        "BackendConfig",
        "EngineConfig",
        "UnifiedInferenceClient",
        "MixedInferenceClient",
        "OllamaClient",
    ]

    def test_all_expected_symbols_in_all(self) -> None:
        """All required symbols appear in sagellm.__all__."""
        import sagellm

        missing = [s for s in self.REQUIRED_EXPORTS if s not in sagellm.__all__]
        assert not missing, f"Missing from sagellm.__all__: {missing}"

    @pytest.mark.parametrize("symbol", REQUIRED_EXPORTS)
    def test_symbol_accessible_via_getattr(self, symbol: str) -> None:
        """Each required symbol must be accessible via ``sagellm.<symbol>``."""
        import sagellm

        try:
            obj = getattr(sagellm, symbol)
            assert obj is not None or symbol == "__version__"
        except (ImportError, AttributeError) as exc:
            pytest.fail(f"sagellm.{symbol} not accessible: {exc}")


class TestConstraintsFile:
    """Verify constraints.txt is consistent with installed versions."""

    def test_constraints_file_exists(self) -> None:
        """constraints.txt must exist in the installed package."""
        from importlib.resources import files

        try:
            content = files("sagellm").joinpath("constraints.txt").read_text(encoding="utf-8")
            assert content, "constraints.txt is empty"
        except (FileNotFoundError, ModuleNotFoundError, TypeError):
            pytest.skip(
                "constraints.txt not found in installed package — "
                "expected only in editable/dev install"
            )

    def test_constraints_versions_match_installed(self) -> None:
        """Each pinned version in constraints.txt must match what is installed."""
        from importlib.resources import files

        try:
            raw = files("sagellm").joinpath("constraints.txt").read_text(encoding="utf-8")
            lines = raw.splitlines(keepends=True)
        except (FileNotFoundError, ModuleNotFoundError, TypeError):
            pytest.skip("constraints.txt not found")

        mismatches: list[str] = []
        for line in lines:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            # Parse "pkg==version"
            match = re.match(r"^([A-Za-z0-9_\-]+)==(.+)$", line)
            if not match:
                continue
            pkg, pinned = match.group(1), match.group(2)
            try:
                installed = importlib.metadata.version(pkg)
                if installed != pinned:
                    mismatches.append(f"{pkg}: constraints.txt={pinned!r}, installed={installed!r}")
            except importlib.metadata.PackageNotFoundError:
                mismatches.append(f"{pkg}: not installed (constraints.txt requires {pinned!r})")

        if mismatches:
            # If ALL mismatches are editable installs, this is a dev environment.
            # Editable installs may be at older versions than the published
            # constraints.txt snapshot — that is expected and not an error.
            non_editable_mismatches = [
                m for m in mismatches if not _is_editable_install(m.split(":")[0].strip())
            ]
            if not non_editable_mismatches:
                pytest.skip(
                    "All mismatched packages are editable local installs — "
                    "constraints.txt is the PyPI snapshot and differs from dev versions. "
                    "This is expected in local development.\n"
                    + "\n".join(f"  {m}" for m in mismatches)
                )
            pytest.fail(
                "constraints.txt is out of sync with installed packages:\n"
                + "\n".join(f"  {m}" for m in non_editable_mismatches)
            )
