#!/usr/bin/env python3
"""Tests for compare_control_field method."""

import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import importlib.util
from importlib.machinery import SourceFileLoader

import pytest

# Load the script as a module (it doesn't have .py extension)
script_path = str(
    Path(__file__).parent.parent
    / "files"
    / "usr"
    / "libexec"
    / "cuda_test_compare_packaging"
)


def import_from_file(module_name, file_path):
    loader = SourceFileLoader(module_name, file_path)
    spec = importlib.util.spec_from_loader(module_name, loader)

    if spec is None:
        raise ImportError(f"Could not load module {module_name} from {file_path}")
    module = importlib.util.module_from_spec(spec)

    sys.modules[module_name] = module
    if spec.loader is None:
        raise ImportError(f"No loader found for module {module_name}")
    spec.loader.exec_module(module)
    return module


ctcp = import_from_file("cuda_test_compare_packaging", script_path)


@pytest.fixture
def setup():
    """Setup test environment."""
    tmpdir = Path(tempfile.mkdtemp())
    config = ctcp.Config(work_dir=tmpdir / "test")
    config.setup_directories()

    our_deb = tmpdir / "our.deb"
    nvidia_deb = tmpdir / "nvidia.deb"
    our_deb.touch()
    nvidia_deb.touch()

    return config, MagicMock(), our_deb, nvidia_deb


@pytest.fixture
def extract_dirs_with_maintainer_scripts(setup):
    """Create extraction directories with DEBIAN maintainer scripts."""
    config, logger, our_deb, nvidia_deb = setup

    nvidia_extract = config.extract_dir / "test-package_nvidia"
    our_extract = config.extract_dir / "test-package_ours"

    nvidia_extract.mkdir(parents=True, exist_ok=True)
    our_extract.mkdir(parents=True, exist_ok=True)

    (nvidia_extract / "DEBIAN").mkdir(parents=True, exist_ok=True)
    (our_extract / "DEBIAN").mkdir(parents=True, exist_ok=True)

    # Create control file
    control_content = """Package: test-package
Version: 1.0-1
Architecture: amd64
Maintainer: Test <test@example.com>
Depends: libc6 (>= 2.34)
Description: Test package
 This is a test package.
"""
    (nvidia_extract / "DEBIAN" / "control").write_text(control_content)
    # Different version for our package
    (our_extract / "DEBIAN" / "control").write_text(
        control_content.replace("1.0-1", "1.0-0ubuntu7")
    )

    # Create postinst script
    postinst_content = """#!/bin/sh
set -e

if [ "$1" = "configure" ]; then
    ldconfig
fi

#DEBHELPER#

exit 0
"""
    (nvidia_extract / "DEBIAN" / "postinst").write_text(postinst_content)
    (our_extract / "DEBIAN" / "postinst").write_text(postinst_content)
    (nvidia_extract / "DEBIAN" / "postinst").chmod(0o755)
    (our_extract / "DEBIAN" / "postinst").chmod(0o755)

    # Create preinst script
    preinst_content = """#!/bin/sh
set -e

#DEBHELPER#

exit 0
"""
    (nvidia_extract / "DEBIAN" / "preinst").write_text(preinst_content)
    (our_extract / "DEBIAN" / "preinst").write_text(preinst_content)
    (nvidia_extract / "DEBIAN" / "preinst").chmod(0o755)
    (our_extract / "DEBIAN" / "preinst").chmod(0o755)

    # Create postrm script
    postrm_content = """#!/bin/sh
set -e

if [ "$1" = "remove" ]; then
    ldconfig
fi

#DEBHELPER#

exit 0
"""
    (nvidia_extract / "DEBIAN" / "postrm").write_text(postrm_content)
    (our_extract / "DEBIAN" / "postrm").write_text(postrm_content)
    (nvidia_extract / "DEBIAN" / "postrm").chmod(0o755)
    (our_extract / "DEBIAN" / "postrm").chmod(0o755)

    # Create prerm script
    prerm_content = """#!/bin/sh
set -e

#DEBHELPER#

exit 0
"""
    (nvidia_extract / "DEBIAN" / "prerm").write_text(prerm_content)
    (our_extract / "DEBIAN" / "prerm").write_text(prerm_content)
    (nvidia_extract / "DEBIAN" / "prerm").chmod(0o755)
    (our_extract / "DEBIAN" / "prerm").chmod(0o755)

    (nvidia_extract / "file1.txt").write_text("content")
    (our_extract / "file1.txt").write_text("content")

    (nvidia_extract / "usr" / "lib").mkdir(parents=True)
    (our_extract / "usr" / "lib").mkdir(parents=True)

    (nvidia_extract / "usr" / "lib" / "file.txt").write_text("more content")
    (our_extract / "usr" / "lib" / "file.txt").write_text("more content")

    (nvidia_extract / "usr" / "share" / "lintian" / "overrides").mkdir(parents=True)
    (our_extract / "usr" / "share" / "lintian" / "overrides").mkdir(parents=True)

    (nvidia_extract / "usr" / "share" / "lintian" / "overrides" / "pkgname").write_text(
        "valid override"
    )
    (our_extract / "usr" / "share" / "lintian" / "overrides" / "pkgname").write_text(
        "valid override"
    )

    return config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract


@patch.object(ctcp.PackageManager, "get_deb_field")
def test_identical_depends(mock_field, setup):
    """Test identical dependencies."""
    config, logger, our_deb, nvidia_deb = setup
    deps = {}
    deps["nvidia"] = (
        "libmount1, cuda-toolkit-config-common | cuda-cudart-11-2, cuda-toolkit-13-config-common | cuda-cudart-11-2, cuda-toolkit-13-1-config-common"
    )
    deps["ours"] = (
        "libmount1, cuda-toolkit-config-common | cuda-cudart-11-2, cuda-toolkit-13-config-common | cuda-cudart-11-2, cuda-toolkit-13-1-config-common"
    )
    mock_field.side_effect = [deps["nvidia"], deps["ours"]]

    comparator = ctcp.PackageComparator(config, logger)
    result = comparator.compare_control_field("Depends", our_deb, nvidia_deb)

    assert not result.has_failures()
    assert not result.missing_items
    assert not result.extra_items
    assert not result.version_mismatches


@patch.object(ctcp.PackageManager, "get_deb_field")
def test_missing_dependency(mock_field, setup):
    """Test missing dependency."""
    config, logger, our_deb, nvidia_deb = setup
    deps = {}
    deps["nvidia"] = (
        "libmount1, libstdc++6, libudev1, libgcc1, cuda-toolkit-config-common | cuda-cudart-11-2"
    )
    deps["ours"] = "libmount1, libudev1, cuda-toolkit-config-common | cuda-cudart-11-2"
    mock_field.side_effect = [deps["nvidia"], deps["ours"]]

    comparator = ctcp.PackageComparator(config, logger)
    result = comparator.compare_control_field("Depends", our_deb, nvidia_deb)

    assert result.has_failures()
    assert result.missing_items
    assert "libstdc++6" in result.missing_items
    assert "libgcc1" in result.missing_items
    assert not result.extra_items
    assert not result.version_mismatches


@patch.object(ctcp.PackageManager, "get_deb_field")
def test_extra_dependency(mock_field, setup):
    """Test missing dependency."""
    config, logger, our_deb, nvidia_deb = setup
    deps = {}
    deps["nvidia"] = "libmount1, cuda-toolkit-13-1-config-common"
    deps["ours"] = (
        "libmount1, cuda-toolkit-13-1-config-common, libibverbs1 (>= 1.1.6), librdmacm1t64 (>= 1.0.15), libstdc++6 (>= 5.2)"
    )
    mock_field.side_effect = [deps["nvidia"], deps["ours"]]

    comparator = ctcp.PackageComparator(config, logger)
    result = comparator.compare_control_field("Depends", our_deb, nvidia_deb)

    assert not result.has_failures()
    assert not result.missing_items
    assert result.extra_items
    assert "libibverbs1" in result.extra_items
    assert "librdmacm1t64" in result.extra_items
    assert "libstdc++6" in result.extra_items
    assert not result.version_mismatches


# deps = {}
# deps["nvidia"] = "libmount1, libstdc++6, libudev1, libgcc1, cuda-toolkit-config-common | cuda-cudart-11-2, cuda-toolkit-13-config-common | cuda-cudart-11-2, cuda-toolkit-13-1-config-common"
# deps["ours"] = "libmount1, libudev1, cuda-toolkit-config-common | cuda-cudart-11-2, cuda-toolkit-13-config-common | cuda-cudart-11-2, cuda-toolkit-13-1-config-common, ibverbs-providers (>= 13), libc6 (>= 2.34), libgcc-s1 (>= 3.0), libibverbs1 (>= 1.1.6), librdmacm1t64 (>= 1.0.15), libstdc++6 (>= 5.2)"


@patch.object(ctcp.PackageManager, "get_deb_field")
def test_version_mismatch(mock_field, setup):
    """Test version mismatch."""
    config, logger, our_deb, nvidia_deb = setup

    deps = {}
    deps["nvidia"] = (
        "libmount1, libc6 (>= 2.34), libgcc-s1 (>= 3.0), libibverbs1 (>= 1.1.6), librdmacm1t64 (>= 1.0.15), libstdc++6 (>= 5.2)"
    )
    deps["ours"] = (
        "libmount1, libc6 (>= 2.35), libgcc-s1 (>= 3.0), libibverbs1 (>= 1.1.6), librdmacm1t64 (>= 1.0.15), libstdc++6 (>= 5.2)"
    )
    mock_field.side_effect = [deps["nvidia"], deps["ours"]]

    comparator = ctcp.PackageComparator(config, logger)
    result = comparator.compare_control_field("Depends", our_deb, nvidia_deb)

    assert result.has_failures()
    assert not result.missing_items
    assert not result.extra_items
    assert "libc6" in result.version_mismatches


def test_compare_package_identical_folders(extract_dirs_with_maintainer_scripts):
    """Test compare_package with two identical folder structures."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        result = comparator.compare_package("test-package", our_deb, nvidia_deb)

    assert result is True


def test_compare_package_different_folders(extract_dirs_with_maintainer_scripts):
    """Test compare_package with different folder contents."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )
    (our_extract / "usr" / "lib" / "file.txt").write_text("Something new")

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        # catch the exception raised by the file comparison
        with pytest.raises(
            RuntimeError, match="File or permission differences found, fatal error."
        ):
            _ = comparator.compare_package("test-package", our_deb, nvidia_deb)


def test_compare_package_filter(extract_dirs_with_maintainer_scripts):
    """Test compare_package with filtered differences (lintian overrides)."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )

    (nvidia_extract / "usr" / "share" / "lintian" / "overrides" / "pkgname").write_text(
        "This is different but should be ignored"
    )

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        result = comparator.compare_package("test-package", our_deb, nvidia_deb)

    assert result is True


def test_compare_package_DEBIAN_missing(extract_dirs_with_maintainer_scripts):
    """Test compare_package with missing DEBIAN folder."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )

    # remove the nvidia DEBIAN folder to trigger the error
    for root, dirs, files in (nvidia_extract / "DEBIAN").walk(top_down=False):
        for name in files:
            (root / name).unlink()
        for name in dirs:
            (root / name).rmdir()
    (nvidia_extract / "DEBIAN").rmdir()

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        with pytest.raises(RuntimeError, match="DEBIAN folder not found"):
            _ = comparator.compare_package("test-package", our_deb, nvidia_deb)


def test_compare_package_DEBIAN_identical(extract_dirs_with_maintainer_scripts):
    """Test compare_package with identical DEBIAN maintainer scripts."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        result = comparator.compare_package("test-package", our_deb, nvidia_deb)

    assert result is True


def test_compare_package_DEBIAN_difference(extract_dirs_with_maintainer_scripts):
    """Test compare_package with different DEBIAN maintainer scripts."""
    config, logger, our_deb, nvidia_deb, nvidia_extract, our_extract = (
        extract_dirs_with_maintainer_scripts
    )

    # Modify postinst to be different
    (our_extract / "DEBIAN" / "postinst").write_text("""#!/bin/sh
set -eu

if [ "$1" = "configure" ]; then
    ldconfig
fi

#DEBHELPER#

exit 0
""")

    # Mock extract_deb to do nothing (we already created the folders)
    with patch.object(ctcp.PackageManager, "extract_deb"):
        comparator = ctcp.PackageComparator(config, logger)
        with pytest.raises(
            RuntimeError, match="File or permission differences found, fatal error."
        ):
            _ = comparator.compare_package("test-package", our_deb, nvidia_deb)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
