"""Performance benchmarks for pylibpack module using pytest-benchmark.

Benchmarks cover:
- Package name normalization
- Dependency filtering
- Cache operations
- Package extraction
- Dependency resolution
- Optimization strategy application

Run with: pytest --benchmark-only
"""

from __future__ import annotations

import io
import tarfile
import tempfile
import zipfile
from pathlib import Path

import pytest

from ..components.libpacker import (
    OptimizationRule,
    PyLibPacker,
    PyLibPackerConfig,
    SelectiveExtractionStrategy,
)
from ..models.dependency import Dependency
from ..models.libcache import (
    LibraryCache,
    normalize_package_name,
    should_skip_dependency,
)

# ==================== Name Normalization Benchmarks ====================


@pytest.mark.benchmark(group="normalization")
def test_benchmark_normalize_package_name_simple(benchmark):
    """Benchmark package name normalization for simple names."""

    def normalize():
        return [normalize_package_name(name) for name in ["requests", "flask", "numpy", "pandas", "django"]]

    result = benchmark(normalize)
    assert len(result) == 5


@pytest.mark.benchmark(group="normalization")
def test_benchmark_normalize_package_name_complex(benchmark):
    """Benchmark package name normalization for complex names with dashes."""

    def normalize():
        names = [
            "requests-oauthlib",
            "pytest-cov",
            "scikit-learn",
            "google-cloud-storage",
            "aws-sdk-python",
        ]
        return [normalize_package_name(name) for name in names]

    result = benchmark(normalize)
    assert len(result) == 5


@pytest.mark.benchmark(group="normalization")
def test_benchmark_normalize_package_name_large_list(benchmark):
    """Benchmark package name normalization for large list."""

    def normalize():
        names = [f"package-{i}-extension" for i in range(1000)]
        return [normalize_package_name(name) for name in names]

    result = benchmark(normalize)
    assert len(result) == 1000


# ==================== Dependency Filtering Benchmarks ====================


@pytest.mark.benchmark(group="filtering")
def test_benchmark_should_skip_dependency_normal(benchmark):
    """Benchmark dependency filtering for normal packages."""

    def filter_deps():
        deps = ["requests", "flask", "numpy", "pandas", "django"]
        return [should_skip_dependency(dep) for dep in deps]

    result = benchmark(filter_deps)
    assert all(not skipped for skipped in result)


@pytest.mark.benchmark(group="filtering")
def test_benchmark_should_skip_dependency_dev_tools(benchmark):
    """Benchmark dependency filtering for dev tools."""

    def filter_deps():
        deps = ["pytest", "black", "flake8", "mypy", "coverage"]
        return [should_skip_dependency(dep) for dep in deps]

    result = benchmark(filter_deps)
    assert all(result)


@pytest.mark.benchmark(group="filtering")
def test_benchmark_should_skip_dependency_large_list(benchmark):
    """Benchmark dependency filtering for large list."""

    def filter_deps():
        # Mix of normal and dev dependencies
        deps = []
        for i in range(500):
            if i % 3 == 0:
                deps.append(f"package-{i}")  # Normal
            elif i % 3 == 1:
                deps.append(f"pytest-{i}")  # Dev
            else:
                deps.append(f"dev-{i}-tool")  # Dev pattern
        return [should_skip_dependency(dep) for dep in deps]

    result = benchmark(filter_deps)
    assert len(result) == 500


# ==================== Dependency Creation Benchmarks ====================


@pytest.mark.benchmark(group="dependency_creation")
def test_benchmark_create_simple_dependency(benchmark):
    """Benchmark creating simple dependencies."""

    def create_deps():
        return [Dependency(name=f"package-{i}") for i in range(100)]

    result = benchmark(create_deps)
    assert len(result) == 100


@pytest.mark.benchmark(group="dependency_creation")
def test_benchmark_create_complex_dependency(benchmark):
    """Benchmark creating complex dependencies with version and extras."""

    def create_deps():
        return [
            Dependency(
                name=f"package-{i}",
                version=f">=1.{i}.0",
                extras={"extra1", "extra2"} if i % 2 == 0 else set(),
            )
            for i in range(100)
        ]

    result = benchmark(create_deps)
    assert len(result) == 100


# ==================== Cache Operations Benchmarks ====================


@pytest.fixture(scope="module")
def benchmark_cache_dir(tmp_path_factory):
    """Create cache directory with packages for benchmarking."""
    cache_dir = tmp_path_factory.mktemp("benchmark_cache")

    # Create fake wheel files
    for i in range(50):
        wheel_file = cache_dir / f"package-{i}-1.0.0-py3-none-any.whl"
        with zipfile.ZipFile(wheel_file, "w") as zf:
            metadata = f"""Metadata-Version: 2.1
Name: package-{i}
Version: 1.0.0
Requires-Dist: requests>=2.0.0
Requires-Dist: flask>=1.0.0
"""
            zf.writestr(f"package-{i}-1.0.0.dist-info/METADATA", metadata)
            zf.writestr(f"package-{i}/__init__.py", f"# package {i}")

    # Create fake sdist files
    for i in range(20):
        sdist_file = cache_dir / f"sdist-package-{i}-1.0.0.tar.gz"
        with tarfile.open(sdist_file, "w:gz") as tf:
            content = f"Name: sdist-package-{i}\nVersion: 1.0.0\n"
            info = tarfile.TarInfo(f"sdist-package-{i}-1.0.0/PKG-INFO")
            info.size = len(content.encode())
            tf.addfile(info, io.BytesIO(content.encode()))

    return cache_dir


@pytest.mark.benchmark(group="cache")
def test_benchmark_cache_get_package_wheel(benchmark, benchmark_cache_dir):
    """Benchmark getting package from cache (wheel)."""
    cache = LibraryCache(cache_dir=benchmark_cache_dir)

    def get_packages():
        results = []
        for i in range(50):
            result = cache.get_package_path(f"package-{i}")
            results.append(result)
        return results

    result = benchmark(get_packages)
    assert len(result) == 50


@pytest.mark.benchmark(group="cache")
def test_benchmark_cache_get_package_sdist(benchmark, benchmark_cache_dir):
    """Benchmark getting package from cache (sdist)."""
    cache = LibraryCache(cache_dir=benchmark_cache_dir)

    def get_packages():
        results = []
        for i in range(20):
            result = cache.get_package_path(f"sdist-package-{i}")
            results.append(result)
        return results

    result = benchmark(get_packages)
    assert len(result) == 20


@pytest.mark.benchmark(group="cache")
def test_benchmark_extract_dependencies_wheel(benchmark, benchmark_cache_dir):
    """Benchmark extracting dependencies from wheel files."""
    cache = LibraryCache(cache_dir=benchmark_cache_dir)

    def extract_deps():
        results = []
        for wheel_file in benchmark_cache_dir.glob("*.whl"):
            deps = cache._extract_dependencies_from_wheel(wheel_file)
            results.append(deps)
        return results

    result = benchmark(extract_deps)
    assert len(result) > 0


@pytest.mark.benchmark(group="cache")
def test_benchmark_extract_dependencies_sdist(benchmark, benchmark_cache_dir):
    """Benchmark extracting dependencies from sdist files."""
    cache = LibraryCache(cache_dir=benchmark_cache_dir)

    def extract_deps():
        results = []
        for sdist_file in benchmark_cache_dir.glob("*.tar.gz"):
            deps = cache._extract_dependencies_from_wheel(sdist_file)
            results.append(deps)
        return results

    result = benchmark(extract_deps)
    assert len(result) > 0


# ==================== Optimization Strategy Benchmarks ====================


@pytest.fixture(scope="module")
def benchmark_rules():
    """Create optimization rules for benchmarking."""
    return [
        OptimizationRule(
            library_name="numpy",
            exclude_patterns=[r".*\.so$", r".*\.pyd$", r"tests/.*"],
            include_patterns=[r".*\.py$"],
        ),
        OptimizationRule(
            library_name="scipy",
            exclude_patterns=[r".*\.so$", r"tests/.*"],
            include_patterns=[r".*\.py$"],
        ),
        OptimizationRule(
            library_name="pandas",
            exclude_patterns=[r".*\.so$", r".*\.pyd$", r".*/tests/.*"],
            include_patterns=[r".*\.py$"],
        ),
    ]


@pytest.mark.benchmark(group="optimization")
def test_benchmark_optimization_strategy_no_rules(benchmark):
    """Benchmark optimization strategy with no rules."""
    # Create strategy with empty rules dict (no rules for any library)
    strategy = SelectiveExtractionStrategy(rules=[])

    def check_files():
        # Use a library name that has no default rules (unknown_library)
        file_paths = [Path(f"package/{i}.py") for i in range(100)]
        return [strategy.should_extract_file("unknown_library", fp) for fp in file_paths]

    result = benchmark(check_files)
    # When no rules exist for a library, all files should be extracted
    assert all(result)


@pytest.mark.benchmark(group="optimization")
def test_benchmark_optimization_strategy_with_rules(benchmark, benchmark_rules):
    """Benchmark optimization strategy with rules."""
    strategy = SelectiveExtractionStrategy(rules=benchmark_rules)

    def check_files():
        file_paths = [
            Path("numpy/core/array.py"),
            Path("numpy/core.so"),
            Path("numpy/tests/test_core.py"),
            Path("scipy/stats.py"),
            Path("scipy/optimize.so"),
            Path("pandas/core/frame.py"),
            Path("pandas/io.so"),
        ] * 10
        return [strategy.should_extract_file("numpy", fp) for fp in file_paths]

    result = benchmark(check_files)
    assert len(result) == 70


@pytest.mark.benchmark(group="optimization")
def test_benchmark_optimization_strategy_large_fileset(benchmark, benchmark_rules):
    """Benchmark optimization strategy with large file set."""
    strategy = SelectiveExtractionStrategy(rules=benchmark_rules)

    def check_files():
        file_paths = []
        for i in range(1000):
            if i % 4 == 0:
                file_paths.append(Path(f"numpy/module{i}.py"))
            elif i % 4 == 1:
                file_paths.append(Path(f"numpy/module{i}.so"))
            elif i % 4 == 2:
                file_paths.append(Path(f"numpy/tests/test{i}.py"))
            else:
                file_paths.append(Path(f"scipy/module{i}.py"))
        return [strategy.should_extract_file("numpy", fp) for fp in file_paths]

    result = benchmark(check_files)
    assert len(result) == 1000


# ==================== Dependency Resolution Benchmarks ====================


@pytest.fixture(scope="module")
def benchmark_dependency_cache(tmp_path_factory):
    """Create cache with dependency chain for benchmarking."""
    cache_dir = tmp_path_factory.mktemp("benchmark_dep_cache")
    _cache = LibraryCache(cache_dir=cache_dir)

    # Create dependency chain: A -> B -> C -> D -> E
    # And independent packages: F, G, H
    packages = {
        "pkg_a": ["pkg_b"],
        "pkg_b": ["pkg_c"],
        "pkg_c": ["pkg_d"],
        "pkg_d": ["pkg_e"],
        "pkg_e": [],
        "pkg_f": [],
        "pkg_g": [],
        "pkg_h": [],
    }

    package_map = {}
    for pkg_name, deps in packages.items():
        wheel_file = cache_dir / f"{pkg_name}-1.0.0-py3-none-any.whl"
        with zipfile.ZipFile(wheel_file, "w") as zf:
            metadata = f"Metadata-Version: 2.1\nName: {pkg_name}\nVersion: 1.0.0\n"
            for dep in deps:
                metadata += f"Requires-Dist: {dep}\n"
            zf.writestr(f"{pkg_name}-1.0.0.dist-info/METADATA", metadata)
        package_map[pkg_name] = wheel_file

    return cache_dir, package_map


@pytest.mark.benchmark(group="dependency_resolution")
def test_benchmark_collect_dependencies_simple(benchmark, benchmark_dependency_cache):
    """Benchmark collecting simple dependency chain."""
    cache_dir, _ = benchmark_dependency_cache
    cache = LibraryCache(cache_dir=cache_dir)

    def collect():
        return cache.collect_dependencies_from_list(["pkg_a"])

    result = benchmark(collect)
    assert "pkg_a" in result


@pytest.mark.benchmark(group="dependency_resolution")
def test_benchmark_collect_dependencies_multiple_roots(benchmark, benchmark_dependency_cache):
    """Benchmark collecting dependencies from multiple root packages."""
    cache_dir, _ = benchmark_dependency_cache
    cache = LibraryCache(cache_dir=cache_dir)

    def collect():
        return cache.collect_dependencies_from_list(["pkg_a", "pkg_f", "pkg_g"])

    result = benchmark(collect)
    assert "pkg_a" in result
    assert "pkg_f" in result
    assert "pkg_g" in result


@pytest.mark.benchmark(group="dependency_resolution")
def test_benchmark_collect_dependencies_complex_graph(benchmark, tmp_path_factory):
    """Benchmark collecting dependencies from complex graph."""
    cache_dir = tmp_path_factory.mktemp("complex_dep_cache")
    LibraryCache(cache_dir=cache_dir)

    # Create complex dependency graph with multiple dependencies per package
    packages = {
        "app": ["requests", "flask", "numpy"],
        "requests": ["urllib3", "chardet"],
        "flask": ["werkzeug", "jinja2"],
        "numpy": [],
        "urllib3": [],
        "chardet": [],
        "werkzeug": [],
        "jinja2": ["markupsafe"],
        "markupsafe": [],
    }

    package_map = {}
    for pkg_name, deps in packages.items():
        wheel_file = cache_dir / f"{pkg_name}-1.0.0-py3-none-any.whl"
        with zipfile.ZipFile(wheel_file, "w") as zf:
            metadata = f"Metadata-Version: 2.1\nName: {pkg_name}\nVersion: 1.0.0\n"
            for dep in deps:
                metadata += f"Requires-Dist: {dep}\n"
            zf.writestr(f"{pkg_name}-1.0.0.dist-info/METADATA", metadata)
        package_map[pkg_name] = wheel_file

    def collect():
        cache = LibraryCache(cache_dir=cache_dir)
        return cache.collect_dependencies_from_list(["app"])

    result = benchmark(collect)
    assert "app" in result


# ==================== Package Extraction Benchmarks ====================


@pytest.fixture(scope="module")
def benchmark_wheel_files(tmp_path_factory):
    """Create wheel files for extraction benchmarking."""
    temp_dir = tmp_path_factory.mktemp("benchmark_wheels")

    wheel_files = []

    # Create wheel with many small Python files
    wheel_file = temp_dir / "many_files-1.0.0-py3-none-any.whl"
    with zipfile.ZipFile(wheel_file, "w") as zf:
        for i in range(200):
            zf.writestr(f"many_files/module{i}.py", f"# module {i}")
            zf.writestr(f"many_files/subdir{i}/file{i}.py", f"# file {i}")
        zf.writestr(
            "many_files-1.0.0.dist-info/METADATA",
            "Metadata-Version: 2.1\nName: many_files\n",
        )
    wheel_files.append(wheel_file)

    # Create wheel with mixed file types
    wheel_file = temp_dir / "mixed_files-1.0.0-py3-none-any.whl"
    with zipfile.ZipFile(wheel_file, "w") as zf:
        for i in range(100):
            zf.writestr(f"mixed_files/code{i}.py", f"# code {i}")
            zf.writestr(f"mixed_files/data{i}.dat", f"dummy data {i}")
            zf.writestr(f"mixed_files/config{i}.txt", f"config {i}")
        zf.writestr(
            "mixed_files-1.0.0.dist-info/METADATA",
            "Metadata-Version: 2.1\nName: mixed_files\n",
        )
    wheel_files.append(wheel_file)

    return wheel_files


@pytest.mark.benchmark(group="extraction")
def test_benchmark_extract_package_no_optimization(benchmark, benchmark_wheel_files, tmp_path_factory):
    """Benchmark package extraction without optimization."""
    cache_dir = tmp_path_factory.mktemp("extraction_cache")
    config = PyLibPackerConfig(cache_dir=cache_dir, optimize=False)
    packer = PyLibPacker(working_dir=Path("../../pylibpack/tests"), config=config)

    wheel_file = benchmark_wheel_files[0]
    _output_dir = tmp_path_factory.mktemp("extraction_output")

    def extract():
        with tempfile.TemporaryDirectory() as temp_dir:
            packer._extract_package(wheel_file, Path(temp_dir), "many_files")

    result = benchmark(extract)
    assert result is None


@pytest.mark.benchmark(group="extraction")
def test_benchmark_extract_package_with_optimization(benchmark, benchmark_wheel_files, tmp_path_factory):
    """Benchmark package extraction with optimization."""
    cache_dir = tmp_path_factory.mktemp("extraction_cache_opt")

    # Create optimization rule to exclude .dat and .txt files
    rules = [
        OptimizationRule(
            library_name="many_files",
            exclude_patterns=[r".*\.dat$", r".*\.txt$"],
            include_patterns=[],
        )
    ]
    strategy = SelectiveExtractionStrategy(rules=rules)
    config = PyLibPackerConfig(cache_dir=cache_dir, optimize=True)
    config._optimization_strategy = strategy  # Set the strategy
    packer = PyLibPacker(working_dir=Path("../../pylibpack/tests"), config=config)

    wheel_file = benchmark_wheel_files[1]
    _output_dir = tmp_path_factory.mktemp("extraction_output_opt")

    def extract():
        with tempfile.TemporaryDirectory() as temp_dir:
            packer._extract_package(wheel_file, Path(temp_dir), "mixed_files")

    result = benchmark(extract)
    assert result is None


# ==================== Large Scale Benchmarks ====================


@pytest.mark.benchmark(group="large_scale")
def test_benchmark_large_package_list_normalization(benchmark):
    """Benchmark normalization of large package list."""

    def normalize_large_list():
        # Simulate real-world package names with various formats
        names = []
        for i in range(1000):
            if i % 3 == 0:
                names.append(f"package-{i}")
            elif i % 3 == 1:
                names.append(f"package-{i}-extension")
            else:
                names.append(f"company-package-{i}-plugin")
        return [normalize_package_name(name) for name in names]

    result = benchmark(normalize_large_list)
    assert len(result) == 1000


@pytest.mark.benchmark(group="large_scale")
def test_benchmark_large_dependency_filtering(benchmark):
    """Benchmark filtering large dependency list."""

    def filter_large_list():
        deps = []
        for i in range(1000):
            if i % 4 == 0:
                deps.append(f"package-{i}")
            elif i % 4 == 1:
                deps.append(f"pytest-{i}")
            elif i % 4 == 2:
                deps.append(f"dev-{i}-tool")
            else:
                deps.append(f"test-{i}-utils")
        return [should_skip_dependency(dep) for dep in deps]

    result = benchmark(filter_large_list)
    assert len(result) == 1000


# ==================== Comparison Benchmarks ====================


@pytest.mark.benchmark(group="comparison")
def test_benchmark_optimization_vs_no_optimization(benchmark, benchmark_wheel_files, tmp_path_factory):
    """Compare optimization vs no optimization performance."""
    cache_dir = tmp_path_factory.mktemp("comparison_cache")

    # Test without optimization
    config_no_opt = PyLibPackerConfig(cache_dir=cache_dir / "no_opt", optimize=False)
    packer_no_opt = PyLibPacker(working_dir=Path("../../pylibpack/tests"), config=config_no_opt)

    # Test with optimization
    rules = [
        OptimizationRule(
            library_name="many_files",
            exclude_patterns=[r".*\.dat$", r".*\.txt$"],
            include_patterns=[],
        )
    ]
    strategy = SelectiveExtractionStrategy(rules=rules)
    config_with_opt = PyLibPackerConfig(cache_dir=cache_dir / "with_opt", optimize=True)
    config_with_opt._optimization_strategy = strategy
    _packer_with_opt = PyLibPacker(working_dir=Path("../../pylibpack/tests"), config=config_with_opt)

    wheel_file = benchmark_wheel_files[1]

    def extract_no_opt():
        with tempfile.TemporaryDirectory() as temp_dir:
            packer_no_opt._extract_package(wheel_file, Path(temp_dir), "mixed_files")

    result = benchmark(extract_no_opt)
    assert result is None


@pytest.mark.benchmark(group="comparison")
def test_benchmark_simple_vs_complex_rules(benchmark, tmp_path_factory):
    """Compare simple vs complex optimization rules."""
    _cache_dir = tmp_path_factory.mktemp("rules_cache")

    # Simple rule
    simple_rules = [OptimizationRule(library_name="test", exclude_patterns=[r".*\.so$"], include_patterns=[])]
    simple_strategy = SelectiveExtractionStrategy(rules=simple_rules)

    # Complex rule
    complex_rules = [
        OptimizationRule(
            library_name="test",
            exclude_patterns=[
                r".*\.so$",
                r".*\.pyd$",
                r"tests/.*",
                r".*/__pycache__/.*",
            ],
            include_patterns=[r".*\.py$"],
        )
    ]
    _complex_strategy = SelectiveExtractionStrategy(rules=complex_rules)

    file_paths = [
        Path("test/module.py"),
        Path("test/core.so"),
        Path("test/tests/test.py"),
        Path("test/__pycache__/module.pyc"),
    ] * 100

    def check_simple():
        return [simple_strategy.should_extract_file("test", fp) for fp in file_paths]

    result = benchmark(check_simple)
    assert len(result) == 400


# ==================== Memory Usage Benchmarks ====================


@pytest.mark.benchmark(group="memory")
def test_benchmark_cache_memory_usage(benchmark, tmp_path_factory):
    """Benchmark memory usage of cache operations."""
    cache_dir = tmp_path_factory.mktemp("memory_cache")
    cache = LibraryCache(cache_dir=cache_dir)

    # Create many wheel files
    for i in range(100):
        wheel_file = cache_dir / f"package{i}-1.0.0-py3-none-any.whl"
        with zipfile.ZipFile(wheel_file, "w") as zf:
            metadata = f"Metadata-Version: 2.1\nName: package{i}\nVersion: 1.0.0\n"
            for j in range(10):
                metadata += f"Requires-Dist: dep{j}\n"
            zf.writestr(f"package{i}-1.0.0.dist-info/METADATA", metadata)

    def get_and_extract():
        results = []
        for i in range(100):
            path = cache.get_package_path(f"package{i}")
            if path:
                deps = cache._extract_dependencies_from_wheel(path)
                results.append(deps)
        return results

    result = benchmark(get_and_extract)
    assert len(result) == 100


# ==================== End of Test File ====================
