#!/usr/bin/python3
"""Tests for slpkg/sbo/slackbuild.py using pytest."""

import os
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
from slpkg.sbo.slackbuild import (DependencyManager, PackageBuilder,
                                  PackageInstaller, PackageMoverAndCleaner,
                                  Slackbuilds, SourcePreparer,
                                  _find_package_files, _select_sources)


@pytest.fixture
def mock_config():
    """Mock config_load for Slackbuilds."""
    with patch("slpkg.sbo.slackbuild.config_load") as mock:
        mock.build_path = Path("/tmp/slpkg/build")
        mock.tmp_slpkg = Path("/tmp/slpkg")
        mock.tmp_path = Path("/tmp")
        mock.prog_name = "slpkg"
        mock.makeflags = "-j4"
        mock.is_64bit.return_value = True
        mock.gpg_verification = False
        mock.delete_sources = False
        mock.quiet = False
        mock.installpkg = "installpkg"
        mock.reinstall = "upgradepkg --reinstall"
        mock.deps_log_file = Path("/var/log/slpkg/deps.log")
        mock.editor = "vim"
        mock.sep_line = "-"
        yield mock


# ---------------------------------------------------------------------------
# _find_package_files
# ---------------------------------------------------------------------------


def test_find_package_files_returns_sorted(tmp_path):
    (tmp_path / "pkg-2.0-x86_64-1.txz").touch()
    (tmp_path / "pkg-1.0-x86_64-1.txz").touch()
    result = _find_package_files(tmp_path)
    assert result == ["pkg-1.0-x86_64-1.txz", "pkg-2.0-x86_64-1.txz"]


def test_find_package_files_empty_dir(tmp_path):
    assert _find_package_files(tmp_path) == []


def test_find_package_files_ignores_non_package_files(tmp_path):
    (tmp_path / "README.txt").touch()
    (tmp_path / "pkg-1.0-x86_64-1.txz").touch()
    result = _find_package_files(tmp_path)
    assert result == ["pkg-1.0-x86_64-1.txz"]


# ---------------------------------------------------------------------------
# _select_sources
# ---------------------------------------------------------------------------

_DATA = {
    "sbo": {
        "download": ["http://src1", "http://src2"],
        "md5sum": ["aaa", "bbb"],
        "download64": ["http://src64"],
        "md5sum64": ["ccc"],
    }
}


def test_select_sources_64bit_returns_64bit_variants():
    sources, checksums = _select_sources("sbo", _DATA, is_64bit=True)
    assert sources == ["http://src64"]
    assert checksums == ["ccc"]


def test_select_sources_default_when_not_64bit():
    sources, checksums = _select_sources("sbo", _DATA, is_64bit=False)
    assert sources == ["http://src1", "http://src2"]
    assert checksums == ["aaa", "bbb"]


def test_select_sources_falls_back_when_no_download64():
    data = {"sbo": {"download": ["http://src1"], "md5sum": ["aaa"]}}
    sources, checksums = _select_sources("sbo", data, is_64bit=True)
    assert sources == ["http://src1"]
    assert checksums == ["aaa"]


def test_select_sources_missing_md5sum_returns_empty_list():
    data = {"sbo": {"download": ["http://src1"]}}
    sources, checksums = _select_sources("sbo", data, is_64bit=False)
    assert sources == ["http://src1"]
    assert checksums == []


# ---------------------------------------------------------------------------
# Tests for DependencyManager
# ---------------------------------------------------------------------------

def test_dependency_manager_resolve_and_select():
    data = {"pkg1": {"requires": "dep1"}}
    options = {}
    mock_choose_deps = MagicMock()
    mock_choose_deps.choose.return_value = ["dep1"]
    mock_view_proc = MagicMock()
    mock_utils = MagicMock()

    with patch("slpkg.sbo.slackbuild.Requires") as mock_requires_cls:
        mock_requires_cls.return_value.resolve.return_value = ("dep1",)

        dm = DependencyManager(data, options, mock_choose_deps, mock_view_proc, mock_utils)
        selected = dm.resolve_and_select(["pkg1"])

        assert selected == ["dep1"]
        mock_choose_deps.choose.assert_called_once()


def test_dependency_manager_manage_build_order():
    dm = DependencyManager({}, {}, MagicMock(), MagicMock(), MagicMock())
    dm.utils.is_package_installed.side_effect = lambda x: x == "installed_pkg"

    initial = ["pkg1", "pkg2"]
    selected_deps = ["dep1", "installed_pkg"]

    # Without skip-installed
    order, skipped = dm.manage_build_order(initial, selected_deps, False)
    assert order == ["dep1", "installed_pkg", "pkg1", "pkg2"]
    assert skipped == []

    # With skip-installed
    order, skipped = dm.manage_build_order(initial, selected_deps, True)
    assert "installed_pkg" in skipped
    assert "installed_pkg" not in order
    assert order == ["dep1", "pkg1", "pkg2"]


# --- Tests for SourcePreparer ---

def test_source_preparer_prepare(tmp_path):
    repos = MagicMock()
    repos.repositories = {"slack": {"path": tmp_path / "repo"}}
    utils = MagicMock()
    view_proc = MagicMock()

    sp = SourcePreparer(tmp_path / "build", tmp_path / "tmp", "slpkg", True, False, repos, utils, view_proc)

    data = {
        "sbo1": {
            "location": "system",
            "download": ["http://url1"],
            "download64": ["http://url64"],
        }
    }

    with patch("shutil.copytree"), patch("os.chmod"):
        sources, asc, count, locations = sp.prepare(["sbo1"], data, "slack", ".tar.gz")

        assert "sbo1" in sources
        assert sources["sbo1"][0] == ["http://url64"]  # Since is_64bit is True
        assert count == 1
        assert locations["sbo1"] == "system"


def test_collect_asc_file_appends_when_exists(tmp_path):
    """asc_file is built via MagicMock path arithmetic; is_file() returns truthy by default."""
    repos = MagicMock()
    sp = SourcePreparer(tmp_path / "build", tmp_path, "slpkg", False, True, repos, MagicMock(), MagicMock())

    asc_files: list = []
    sp._collect_asc_file("sbo1", "system", ".tar.gz", asc_files)

    assert len(asc_files) == 1


def test_collect_asc_file_skips_when_missing(tmp_path):
    repos = MagicMock()
    # Make the final path object report is_file() → False
    mock_asc = MagicMock()
    mock_asc.is_file.return_value = False
    repos.repositories_path.__truediv__.return_value.__truediv__.return_value.__truediv__.return_value = mock_asc

    sp = SourcePreparer(tmp_path / "build", tmp_path, "slpkg", False, True, repos, MagicMock(), MagicMock())

    asc_files: list = []
    sp._collect_asc_file("sbo1", "system", ".tar.gz", asc_files)

    assert asc_files == []


# ---------------------------------------------------------------------------
# PackageBuilder._edit_slackbuild
# ---------------------------------------------------------------------------


def _make_package_builder(tmp_path):
    multi_proc = MagicMock()
    utils = MagicMock()
    view = MagicMock()
    with patch("slpkg.sbo.slackbuild.config_load"):
        pb = PackageBuilder({}, tmp_path / "build", tmp_path / "tmp", "slpkg", "-j4", "vim", multi_proc, utils, view)
    return pb


def test_edit_slackbuild_exact_name_match(tmp_path):
    """option_sbo_edit == name → opens the editor."""
    pb = _make_package_builder(tmp_path)
    pb.options = {"option_sbo_edit": "mysbo"}
    folder = tmp_path / "mysbo"
    folder.mkdir()
    sbo_file = folder / "mysbo.SlackBuild"
    sbo_file.write_text("# script\n")

    pb._edit_slackbuild("mysbo", folder)
    pb.multi_proc.process.assert_called_once()


def test_edit_slackbuild_wildcard_match(tmp_path):
    """option_sbo_edit == '*' → opens the editor for current sbo."""
    pb = _make_package_builder(tmp_path)
    pb.options = {"option_sbo_edit": "*"}
    folder = tmp_path / "other"
    folder.mkdir()
    sbo_file = folder / "other.SlackBuild"
    sbo_file.write_text("# script\n")

    pb._edit_slackbuild("other", folder)
    pb.multi_proc.process.assert_called_once()


def test_edit_slackbuild_no_match_skips_editor(tmp_path):
    """option_sbo_edit does not match → editor is never called."""
    pb = _make_package_builder(tmp_path)
    pb.options = {"option_sbo_edit": "different"}

    pb._edit_slackbuild("mysbo", tmp_path)
    pb.multi_proc.process.assert_not_called()


def test_edit_slackbuild_none_option_skips_editor(tmp_path):
    """option_sbo_edit is None (flag not given) → editor is never called."""
    pb = _make_package_builder(tmp_path)
    pb.options = {}

    pb._edit_slackbuild("mysbo", tmp_path)
    pb.multi_proc.process.assert_not_called()


# --- Tests for PackageBuilder ---

def test_package_builder_build(mock_config, tmp_path):
    multi_proc = MagicMock()
    utils = MagicMock()
    view = MagicMock()

    pb = PackageBuilder({}, tmp_path / "build", tmp_path / "tmp", "slpkg", "-j4", "vim", multi_proc, utils, view)

    with patch("tempfile.mkdtemp", return_value=str(tmp_path / "out")), \
            patch.dict(os.environ, {}):
        pb.build("mysbo")
        assert os.environ["OUTPUT"] == str(tmp_path / "out")
        assert os.environ["MAKEFLAGS"] == "-j4"
        multi_proc.process_and_log.assert_called_once()


# --- Tests for PackageInstaller ---

def _make_mock_output_env(pkg_name: str = "pkg-1.0-x86_64-1.txz"):
    mock_output_env = MagicMock(spec=Path)
    mock_pkg_file = MagicMock(spec=Path)
    mock_pkg_file.name = pkg_name
    mock_pkg_file.is_file.return_value = True
    mock_output_env.glob.return_value = [mock_pkg_file]
    return mock_output_env


def test_package_installer_install():
    multi_proc = MagicMock()
    utils = MagicMock()
    multi_proc.process_and_log.return_value = True

    pi = PackageInstaller("installpkg", "upgradepkg", multi_proc, {}, "install", {}, Path("/deps.log"), utils)

    with patch("slpkg.sbo.slackbuild.Requires") as mock_req:
        mock_req.return_value.resolve.return_value = ()
        success = pi.install("mysbo", _make_mock_output_env())
        assert success is True
        cmd = multi_proc.process_and_log.call_args[0][0]
        assert cmd.startswith("installpkg ")


def test_package_installer_install_uses_reinstall_tool():
    """When option_reinstall is set the reinstall tool is used."""
    multi_proc = MagicMock()
    multi_proc.process_and_log.return_value = True

    pi = PackageInstaller(
        "installpkg", "upgradepkg --reinstall", multi_proc,
        {"option_reinstall": True}, "install", {}, Path("/deps.log"), MagicMock()
    )

    with patch("slpkg.sbo.slackbuild.Requires") as mock_req:
        mock_req.return_value.resolve.return_value = ()
        pi.install("mysbo", _make_mock_output_env())
        cmd = multi_proc.process_and_log.call_args[0][0]
        assert cmd.startswith("upgradepkg --reinstall ")


# --- Tests for Slackbuilds (Orchestrator) ---

def test_slackbuilds_initialization(mock_config):
    repository = "slack"
    data = {"pkg1": {"location": "sys", "download": []}}
    slackbuilds_list = ["pkg1"]
    options = {}

    with patch("slpkg.sbo.slackbuild.Repositories"), \
            patch("slpkg.sbo.slackbuild.MultiProcess"), \
            patch("slpkg.sbo.slackbuild.View"), \
            patch("slpkg.sbo.slackbuild.ViewProcess"), \
            patch("slpkg.sbo.slackbuild.Md5sum"), \
            patch("slpkg.sbo.slackbuild.Downloader"), \
            patch("slpkg.sbo.slackbuild.GPGVerify"), \
            patch("slpkg.sbo.slackbuild.ChooseDependencies"), \
            patch("slpkg.sbo.slackbuild.Utilities") as mock_utils_cls:

        # Mock apply_package_pattern
        mock_utils_cls.return_value.apply_package_pattern.return_value = ["pkg1"]

        sb = Slackbuilds(repository, data, slackbuilds_list, options, "install")
        assert sb.repository == "slack"
        assert sb.slackbuilds_initial == ["pkg1"]
        assert isinstance(sb.dependency_manager, DependencyManager)
