"""
Tests for GateWay dependency resolution and installation.
"""


import pytest

from verlex_server.gateway.backend.dependencies import (
    Dependency,
    DependencyInstaller,
    DependencyManager,
    DependencyParser,
    DependencyResolution,
    DependencyResolver,
    DependencyType,
    EnvironmentSpec,
    detect_gpu_packages,
    parse_requirements,
)


class TestDependency:
    """Tests for Dependency dataclass."""

    def test_simple_pip_spec(self):
        """Test converting a simple dependency to pip spec."""
        dep = Dependency(name="numpy")
        assert dep.to_pip_spec() == "numpy"

    def test_versioned_pip_spec(self):
        """Test converting a versioned dependency to pip spec."""
        dep = Dependency(name="numpy", version=">=1.20.0")
        assert dep.to_pip_spec() == "numpy>=1.20.0"

    def test_pip_spec_with_extras(self):
        """Test converting dependency with extras to pip spec."""
        dep = Dependency(name="torch", extras=["cuda11"])
        assert dep.to_pip_spec() == "torch[cuda11]"

    def test_pip_spec_with_markers(self):
        """Test converting dependency with markers to pip spec."""
        dep = Dependency(
            name="numpy",
            version=">=1.20.0",
            markers="python_version >= '3.9'"
        )
        assert "numpy>=1.20.0; python_version >= '3.9'" == dep.to_pip_spec()

    def test_conda_spec(self):
        """Test converting to conda spec."""
        dep = Dependency(name="numpy", version="==1.20.0", dep_type=DependencyType.CONDA)
        assert dep.to_conda_spec() == "numpy=1.20.0"

    def test_dependency_hash(self):
        """Test dependency hashing for set operations."""
        dep1 = Dependency(name="numpy", version=">=1.20.0")
        dep2 = Dependency(name="numpy", version=">=1.20.0")
        dep3 = Dependency(name="numpy", version=">=1.21.0")

        assert hash(dep1) == hash(dep2)
        assert hash(dep1) != hash(dep3)
        assert dep1 == dep2
        assert dep1 != dep3


class TestDependencyParser:
    """Tests for DependencyParser."""

    def test_parse_simple_requirements(self):
        """Test parsing simple requirements.txt."""
        content = """
numpy
pandas
scikit-learn
"""
        deps = DependencyParser.parse_requirements_txt(content)

        assert len(deps) == 3
        assert deps[0].name == "numpy"
        assert deps[1].name == "pandas"
        assert deps[2].name == "scikit-learn"

    def test_parse_versioned_requirements(self):
        """Test parsing versioned requirements."""
        content = """
numpy>=1.20.0
pandas==1.5.0
torch~=2.0
"""
        deps = DependencyParser.parse_requirements_txt(content)

        assert len(deps) == 3
        assert deps[0].version == ">=1.20.0"
        assert deps[1].version == "==1.5.0"
        assert deps[2].version == "~=2.0"

    def test_parse_requirements_with_comments(self):
        """Test parsing requirements with comments."""
        content = """
# ML libraries
numpy>=1.20.0
# This is a comment
pandas
torch  # inline comment should not be parsed as part of name
"""
        deps = DependencyParser.parse_requirements_txt(content)

        assert len(deps) == 3
        # Note: inline comments might be included in name, depends on parser
        assert any(d.name == "numpy" for d in deps)
        assert any(d.name == "pandas" for d in deps)

    def test_parse_requirements_with_extras(self):
        """Test parsing requirements with extras."""
        content = """
torch[cuda11]
fastapi[all]>=0.100.0
"""
        deps = DependencyParser.parse_requirements_txt(content)

        assert len(deps) == 2
        assert deps[0].name == "torch"
        assert "cuda11" in deps[0].extras
        assert deps[1].name == "fastapi"
        assert "all" in deps[1].extras

    def test_skip_flags_and_includes(self):
        """Test skipping pip flags and includes."""
        content = """
-r base.txt
--extra-index-url https://example.com
-e git+https://github.com/user/repo.git
numpy
"""
        deps = DependencyParser.parse_requirements_txt(content)

        # Should only get numpy and maybe the editable install
        package_names = [d.name for d in deps]
        assert "numpy" in package_names

    def test_parse_pyproject_dependencies(self):
        """Test parsing pyproject.toml dependencies."""
        content = '''
[project]
name = "myproject"
dependencies = [
    "numpy>=1.20.0",
    "pandas",
    "torch>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest",
    "black",
]
'''
        deps = DependencyParser.parse_pyproject_toml(content)

        # Should find both required and optional
        names = [d.name for d in deps]
        assert "numpy" in names
        assert "pandas" in names
        assert "torch" in names

    def test_extract_from_source(self):
        """Test extracting imports from source code."""
        source = """
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
import torch
from torch.nn import Module
import os  # should be ignored (stdlib)
import sys  # should be ignored (stdlib)
"""
        deps = DependencyParser.extract_from_source(source)

        names = [d.name for d in deps]
        assert "numpy" in names
        assert "pandas" in names
        assert "scikit-learn" in names  # sklearn -> scikit-learn
        assert "torch" in names
        # stdlib should not be included
        assert "os" not in names
        assert "sys" not in names

    def test_detect_gpu_requirements_pytorch(self):
        """Test detecting GPU requirements for PyTorch."""
        deps = [Dependency(name="torch")]
        requires_gpu, cuda_version = DependencyParser.detect_gpu_requirements(deps)

        assert requires_gpu is True
        assert cuda_version is not None

    def test_detect_gpu_requirements_tensorflow(self):
        """Test detecting GPU requirements for TensorFlow."""
        deps = [Dependency(name="tensorflow")]
        requires_gpu, cuda_version = DependencyParser.detect_gpu_requirements(deps)

        assert requires_gpu is True

    def test_detect_no_gpu_requirements(self):
        """Test detecting no GPU requirements."""
        deps = [
            Dependency(name="numpy"),
            Dependency(name="pandas"),
        ]
        requires_gpu, cuda_version = DependencyParser.detect_gpu_requirements(deps)

        assert requires_gpu is False
        assert cuda_version is None


class TestDependencyResolver:
    """Tests for DependencyResolver."""

    @pytest.mark.asyncio
    async def test_resolve_simple_dependencies(self):
        """Test resolving simple dependencies."""
        resolver = DependencyResolver()

        deps = [
            Dependency(name="numpy"),
            Dependency(name="pandas"),
        ]

        result = await resolver.resolve(deps)

        assert isinstance(result, DependencyResolution)
        assert len(result.resolved) == 2
        assert not result.has_conflicts

    @pytest.mark.asyncio
    async def test_detect_version_conflicts(self):
        """Test detecting version conflicts."""
        resolver = DependencyResolver()

        deps = [
            Dependency(name="numpy", version=">=1.20.0"),
            Dependency(name="numpy", version="==1.19.0"),
        ]

        result = await resolver.resolve(deps)

        assert result.has_conflicts
        assert len(result.conflicts) > 0

    @pytest.mark.asyncio
    async def test_deduplicate_dependencies(self):
        """Test deduplicating dependencies."""
        resolver = DependencyResolver()

        deps = [
            Dependency(name="numpy", version=">=1.20.0"),
            Dependency(name="numpy", version=">=1.20.0"),
            Dependency(name="pandas"),
        ]

        result = await resolver.resolve(deps)

        # Should deduplicate
        names = [d.name for d in result.resolved]
        assert names.count("numpy") == 1

    @pytest.mark.asyncio
    async def test_caching(self):
        """Test resolution caching."""
        resolver = DependencyResolver()

        deps = [Dependency(name="numpy")]

        # First resolution
        result1 = await resolver.resolve(deps, use_cache=True)

        # Second resolution should use cache
        result2 = await resolver.resolve(deps, use_cache=True)

        # Results should be the same object or equivalent
        assert result1.resolved == result2.resolved


class TestDependencyInstaller:
    """Tests for DependencyInstaller."""

    @pytest.mark.asyncio
    async def test_get_installed_packages(self):
        """Test getting installed packages."""
        installer = DependencyInstaller()

        packages = installer.get_installed_packages()

        # Should at least have pip
        assert isinstance(packages, dict)
        assert len(packages) > 0

    @pytest.mark.asyncio
    async def test_verify_installation(self):
        """Test verifying installation."""
        installer = DependencyInstaller()

        # Check for a package we know is installed
        deps = [Dependency(name="pip")]
        verified, missing = installer.verify_installation(deps)

        assert len(verified) == 1
        assert len(missing) == 0

    @pytest.mark.asyncio
    async def test_verify_missing_package(self):
        """Test verifying missing package."""
        installer = DependencyInstaller()

        deps = [Dependency(name="nonexistent-package-xyz-12345")]
        verified, missing = installer.verify_installation(deps)

        assert len(verified) == 0
        assert len(missing) == 1


class TestEnvironmentSpec:
    """Tests for EnvironmentSpec."""

    def test_to_dict(self):
        """Test converting to dictionary."""
        spec = EnvironmentSpec(
            python_version="3.12",
            pip_packages=[Dependency(name="numpy")],
            requires_gpu=True,
            cuda_version="12.1",
        )

        data = spec.to_dict()

        assert data["python_version"] == "3.12"
        assert len(data["pip_packages"]) == 1
        assert data["requires_gpu"] is True
        assert data["cuda_version"] == "12.1"

    def test_compute_hash(self):
        """Test computing hash for caching."""
        spec1 = EnvironmentSpec(
            python_version="3.12",
            pip_packages=[Dependency(name="numpy")],
        )

        spec2 = EnvironmentSpec(
            python_version="3.12",
            pip_packages=[Dependency(name="numpy")],
        )

        spec3 = EnvironmentSpec(
            python_version="3.10",
            pip_packages=[Dependency(name="numpy")],
        )

        assert spec1.compute_hash() == spec2.compute_hash()
        assert spec1.compute_hash() != spec3.compute_hash()


class TestDependencyManager:
    """Tests for DependencyManager."""

    @pytest.mark.asyncio
    async def test_from_serialized_function(self):
        """Test creating environment spec from serialized function."""
        manager = DependencyManager()

        packages = ["numpy", "pandas", "torch"]
        spec = await manager.from_serialized_function(packages)

        assert spec.python_version == "3.12"
        assert len(spec.pip_packages) == 3
        assert spec.requires_gpu is True  # torch requires GPU

    @pytest.mark.asyncio
    async def test_from_source_code(self):
        """Test creating environment spec from source code."""
        manager = DependencyManager()

        source = """
import numpy as np
import pandas as pd
"""
        spec = await manager.from_source_code(source)

        names = [d.name for d in spec.pip_packages]
        assert "numpy" in names
        assert "pandas" in names


class TestConvenienceFunctions:
    """Tests for module-level convenience functions."""

    def test_parse_requirements(self):
        """Test parse_requirements function."""
        content = """
numpy>=1.20.0
pandas
"""
        specs = parse_requirements(content)

        assert len(specs) == 2
        assert "numpy>=1.20.0" in specs
        assert "pandas" in specs

    def test_detect_gpu_packages_with_torch(self):
        """Test detect_gpu_packages function."""
        packages = ["numpy", "torch", "pandas"]
        requires_gpu, cuda_version = detect_gpu_packages(packages)

        assert requires_gpu is True
        assert cuda_version is not None

    def test_detect_gpu_packages_without_gpu(self):
        """Test detect_gpu_packages with no GPU packages."""
        packages = ["numpy", "pandas", "requests"]
        requires_gpu, cuda_version = detect_gpu_packages(packages)

        assert requires_gpu is False
        assert cuda_version is None


class TestModuleToPackageMapping:
    """Tests for module to package name mapping."""

    def test_common_mappings(self):
        """Test common module to package mappings."""
        mappings = DependencyParser.MODULE_TO_PACKAGE

        assert mappings.get("cv2") == "opencv-python"
        assert mappings.get("PIL") == "Pillow"
        assert mappings.get("sklearn") == "scikit-learn"
        assert mappings.get("yaml") == "PyYAML"

    def test_ml_framework_mappings(self):
        """Test ML framework mappings."""
        mappings = DependencyParser.MODULE_TO_PACKAGE

        assert mappings.get("torch") == "torch"
        assert mappings.get("tensorflow") == "tensorflow"
        assert mappings.get("jax") == "jax"
        assert mappings.get("transformers") == "transformers"


class TestStdlibExclusion:
    """Tests for stdlib module exclusion."""

    def test_stdlib_not_in_dependencies(self):
        """Test that stdlib modules are excluded from dependencies."""
        source = """
import os
import sys
import json
import asyncio
import numpy
"""
        deps = DependencyParser.extract_from_source(source)

        names = [d.name for d in deps]
        assert "os" not in names
        assert "sys" not in names
        assert "json" not in names
        assert "asyncio" not in names
        assert "numpy" in names

    def test_stdlib_list_completeness(self):
        """Test that stdlib list contains common modules."""
        stdlib = DependencyParser.STDLIB

        # Common stdlib modules
        assert "os" in stdlib
        assert "sys" in stdlib
        assert "json" in stdlib
        assert "asyncio" in stdlib
        assert "collections" in stdlib
        assert "functools" in stdlib
        assert "typing" in stdlib
        assert "pathlib" in stdlib


class TestCondaParsing:
    """Tests for conda environment file parsing."""

    def test_parse_simple_conda_yaml(self):
        """Test parsing simple conda environment.yml."""
        content = """
name: myenv
dependencies:
  - numpy
  - pandas
  - python=3.12
  - pip:
    - torch
    - transformers
"""
        conda_deps, pip_deps = DependencyParser.parse_conda_yaml(content)

        conda_names = [d.name for d in conda_deps]
        pip_names = [d.name for d in pip_deps]

        assert "numpy" in conda_names
        assert "pandas" in conda_names
        assert "torch" in pip_names
        assert "transformers" in pip_names

    def test_parse_conda_with_channels(self):
        """Test parsing conda file with channel prefixes."""
        content = """
name: myenv
channels:
  - pytorch
  - conda-forge
dependencies:
  - pytorch::torch
  - conda-forge::numpy
"""
        conda_deps, pip_deps = DependencyParser.parse_conda_yaml(content)

        names = [d.name for d in conda_deps]
        assert "torch" in names
        assert "numpy" in names
