#!/usr/bin/python3
"""Tests for slpkg.upgrade.Upgrade."""

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

import pytest
from slpkg.upgrade import Upgrade
from slpkg.utilities import Utilities


def make_upgrade(
    data: dict,
    package_method: bool = False,
    downgrade_packages: bool = False,
    kernel_version: bool = False,
    kernel_ver: str = "",
    repository: str = "alien",
    pinned: set | None = None,
    repos: MagicMock | None = None,
) -> Upgrade:
    """Construct an Upgrade instance bypassing __init__ (avoids config_load/Repositories)."""
    upg: Upgrade = object.__new__(Upgrade)
    upg.data = data
    upg.package_method = package_method
    upg.downgrade_packages = downgrade_packages
    upg.kernel_version = kernel_version
    upg.kernel_ver = kernel_ver
    upg.repository = repository
    upg.pinned = pinned if pinned is not None else set()
    upg.log_packages = Path("/var/log/packages")
    upg.grey = upg.yellow = upg.red = upg.green = upg.endc = ""
    upg.id = 0
    upg.sum_upgrade = 0
    upg.sum_removed = 0
    upg.sum_added = 0
    upg.sum_pinned = 0
    upg.installed_packages = []
    upg.columns = 80
    upg.rows = 24
    # Wire split_package through to the real static implementation.
    upg.utils = MagicMock()
    upg.utils.split_package.side_effect = Utilities.split_package
    upg.load_data = MagicMock()
    if repos is not None:
        upg.repos = repos
    else:
        upg.repos = MagicMock()
        upg.repos.slack_repo_name = "slackware64"
        upg.repos.slack_extra_repo_name = "slackware64-extra"
        upg.repos.data_json = "data.json"
        upg.repos.remove_packages = []
        upg.repos.new_packages = []
        upg.option_for_install_new = False
        upg.repos.repositories = {
            "alien": {"repo_tag": "alien", "enable": True},
            "slackware64": {"repo_tag": "", "enable": True, "path": "/tmp"},
            "slackware64-extra": {"enable": False, "path": "/tmp"},
        }
    return upg


# ---------------------------------------------------------------------------
# Helpers: minimal repo data entries for upgrade tests
# ---------------------------------------------------------------------------


def repo_entry(version: str, build: str = "1", package: str = "") -> dict:
    pkg = package or f"ffmpeg-{version}-x86_64-{build}alien.txz"
    return {"version": version, "build": build, "package": pkg}


# ---------------------------------------------------------------------------
# Version comparison (default method: packaging.version.parse)
# ---------------------------------------------------------------------------


def test_same_version_same_build_not_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.1", "1")}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is False


def test_newer_repo_version_is_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is True


def test_older_repo_version_not_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.0", "1")}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is False


def test_same_version_newer_build_is_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.1", "2")}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is True


def test_same_version_older_build_not_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.1", "1")}
    upg = make_upgrade(data)
    # installed has build 2, repo has build 1 → not upgradeable
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-2alien") is False


def test_package_not_in_data_not_upgradeable() -> None:
    upg = make_upgrade({"bash": repo_entry("5.2", "1")})
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is False


# ---------------------------------------------------------------------------
# Downgrade
# ---------------------------------------------------------------------------


def test_downgrade_enabled_older_repo_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.0", "1")}
    upg = make_upgrade(data, downgrade_packages=True)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is True


def test_downgrade_disabled_older_repo_not_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.0", "1")}
    upg = make_upgrade(data, downgrade_packages=False)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is False


# ---------------------------------------------------------------------------
# Package method (full-name comparison)
# ---------------------------------------------------------------------------


def test_package_method_same_name_not_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.1", "1", "ffmpeg-7.1-x86_64-1alien.txz")}
    upg = make_upgrade(data, package_method=True)
    # installed name (without ext) matches repo package name (without ext)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is False


def test_package_method_different_name_is_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.2", "1", "ffmpeg-7.2-x86_64-1alien.txz")}
    upg = make_upgrade(data, package_method=True)
    assert upg.is_package_upgradeable("ffmpeg-7.1-x86_64-1alien") is True


# ---------------------------------------------------------------------------
# Invalid version string → string comparison fallback
# ---------------------------------------------------------------------------


def test_invalid_version_fallback_string_compare_upgradeable() -> None:
    # Use version strings that packaging.version.parse cannot handle
    data = {"ffmpeg": {"version": "r200", "build": "1", "package": "ffmpeg-r200-x86_64-1alien.txz"}}
    upg = make_upgrade(data)
    # 'r200' > 'r100' alphabetically
    assert upg.is_package_upgradeable("ffmpeg-r100-x86_64-1alien") is True


def test_invalid_version_fallback_string_compare_same_not_upgradeable() -> None:
    data = {"ffmpeg": {"version": "r200", "build": "1", "package": "ffmpeg-r200-x86_64-1alien.txz"}}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-r200-x86_64-1alien") is False


# ---------------------------------------------------------------------------
# Kernel version adjustment
# ---------------------------------------------------------------------------


def test_kernel_version_flag_strips_kernel_suffix() -> None:
    # Package installed with kernel version suffix; repo has stripped version.
    # If kernel_version=True and version ends with _<kernel_ver>, it's stripped before compare.
    kernel = "5.15.0"
    data = {"mymodule": repo_entry("1.0", "1", "mymodule-1.0-x86_64-1.txz")}
    upg = make_upgrade(data, kernel_version=True, kernel_ver=kernel)
    # Installed version '1.0_5.15.0' → stripped to '1.0' → same as repo → not upgradeable
    assert upg.is_package_upgradeable(f"mymodule-1.0_{kernel}-x86_64-1") is False


def test_kernel_version_flag_newer_repo_upgradeable() -> None:
    kernel = "5.15.0"
    data = {"mymodule": repo_entry("2.0", "1", "mymodule-2.0-x86_64-1.txz")}
    upg = make_upgrade(data, kernel_version=True, kernel_ver=kernel)
    # Installed version '1.0_5.15.0' → stripped to '1.0'; repo is '2.0' → upgradeable
    assert upg.is_package_upgradeable(f"mymodule-1.0_{kernel}-x86_64-1") is True


# ---------------------------------------------------------------------------
# Invalid version string → string comparison fallback: same version newer build
# ---------------------------------------------------------------------------


def test_invalid_version_fallback_same_version_newer_build_upgradeable() -> None:
    data = {"ffmpeg": {"version": "r200", "build": "2", "package": "ffmpeg-r200-x86_64-2alien.txz"}}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-r200-x86_64-1alien") is True


def test_invalid_version_fallback_same_version_older_build_not_upgradeable() -> None:
    data = {"ffmpeg": {"version": "r200", "build": "1", "package": "ffmpeg-r200-x86_64-1alien.txz"}}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-r200-x86_64-2alien") is False


def test_invalid_version_fallback_non_integer_build_not_upgradeable() -> None:
    data = {"ffmpeg": {"version": "r200", "build": "abc", "package": "ffmpeg-r200-x86_64-abcalien.txz"}}
    upg = make_upgrade(data)
    assert upg.is_package_upgradeable("ffmpeg-r200-x86_64-xyzalien") is False


# ---------------------------------------------------------------------------
# _results(): output + SystemExit
# ---------------------------------------------------------------------------


def test_results_empty_prints_up_to_date(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    with pytest.raises(SystemExit) as exc:
        upg._results({})
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "up-to-date" in captured.out


def test_results_with_packages_prints_table(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({"ffmpeg": repo_entry("7.2", "1")})
    upg.sum_upgrade = 1
    packages = {
        1: {
            "name": "ffmpeg",
            "inst_version": "7.1",
            "inst_build": "1",
            "repo_version": "7.2",
            "repo_build": "1",
            "repo": "alien",
            "type": "upgrade",
        }
    }
    with pytest.raises(SystemExit) as exc:
        upg._results(packages)
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "ffmpeg" in captured.out
    assert "7.2" in captured.out
    assert "Total packages" in captured.out


def test_results_remove_type_printed(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    upg.sum_removed = 1
    packages = {
        1: {
            "name": "oldpkg",
            "inst_version": "1.0",
            "inst_build": "1",
            "repo_version": "",
            "repo_build": "",
            "repo": "slackware64",
            "type": "remove",
        }
    }
    with pytest.raises(SystemExit) as exc:
        upg._results(packages)
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "oldpkg" in captured.out


def test_results_add_type_printed(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    upg.sum_added = 1
    packages = {
        1: {
            "name": "newpkg",
            "inst_version": "",
            "inst_build": "",
            "repo_version": "2.0",
            "repo_build": "1",
            "repo": "alien",
            "type": "add",
        }
    }
    with pytest.raises(SystemExit) as exc:
        upg._results(packages)
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "newpkg" in captured.out


def test_results_pinned_type_printed(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    upg.sum_pinned = 1
    packages = {
        1: {
            "name": "pinnedpkg",
            "inst_version": "1.0",
            "inst_build": "1",
            "repo_version": "2.0",
            "repo_build": "1",
            "repo": "alien",
            "type": "pinned",
        }
    }
    with pytest.raises(SystemExit) as exc:
        upg._results(packages)
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "pinnedpkg" in captured.out
    assert "pinned" in captured.out


def test_results_long_name_truncated(capsys: pytest.CaptureFixture) -> None:
    long_name = "a" * 40
    upg = make_upgrade({})
    packages = {
        1: {
            "name": long_name,
            "inst_version": "1.0",
            "inst_build": "1",
            "repo_version": "2.0",
            "repo_build": "1",
            "repo": "alien",
            "type": "upgrade",
        }
    }
    with pytest.raises(SystemExit):
        upg._results(packages)
    captured = capsys.readouterr()
    assert "..." in captured.out


def test_results_long_version_truncated(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    packages = {
        1: {
            "name": "pkg",
            "inst_version": "1.2.3.4.5.6.7.8.9.0",
            "inst_build": "1",
            "repo_version": "2.3.4.5.6.7.8.9.0.1",
            "repo_build": "1",
            "repo": "alien",
            "type": "upgrade",
        }
    }
    with pytest.raises(SystemExit):
        upg._results(packages)
    captured = capsys.readouterr()
    assert "..." in captured.out


def test_results_pinned_note_when_sum_pinned_zero(capsys: pytest.CaptureFixture) -> None:
    upg = make_upgrade({})
    upg.sum_pinned = 0
    packages = {
        1: {
            "name": "pkg",
            "inst_version": "1.0",
            "inst_build": "1",
            "repo_version": "2.0",
            "repo_build": "1",
            "repo": "alien",
            "type": "upgrade",
        }
    }
    with pytest.raises(SystemExit):
        upg._results(packages)
    captured = capsys.readouterr()
    assert "pinned" not in captured.out


# ---------------------------------------------------------------------------
# packages() generator
# ---------------------------------------------------------------------------


def _make_upgrade_for_packages(
    data: dict,
    installed_packages: list,
    remove_packages: list | None = None,
    new_packages: list | None = None,
    pinned: set | None = None,
    repository: str = "alien",
) -> Upgrade:
    """Helper to build Upgrade with mocked _load_installed_packages."""
    upg = make_upgrade(data, repository=repository, pinned=pinned)
    upg.repos.remove_packages = remove_packages or []
    upg.repos.new_packages = new_packages or []
    upg.installed_packages = [Path(p) for p in installed_packages]
    # Prevent _load_installed_packages from touching filesystem
    upg._load_installed_packages = MagicMock()  # type: ignore[method-assign]
    return upg


def test_packages_generator_yields_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = _make_upgrade_for_packages(data, ["ffmpeg-7.1-x86_64-1alien"])
    result = list(upg.packages())
    assert "ffmpeg" in result


def test_packages_generator_skips_non_upgradeable() -> None:
    data = {"ffmpeg": repo_entry("7.1", "1")}
    upg = _make_upgrade_for_packages(data, ["ffmpeg-7.1-x86_64-1alien"])
    result = list(upg.packages())
    assert "ffmpeg" not in result


def test_packages_generator_skips_pinned() -> None:
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = _make_upgrade_for_packages(data, ["ffmpeg-7.1-x86_64-1alien"], pinned={"ffmpeg"})
    result = list(upg.packages())
    assert "ffmpeg" not in result


def test_packages_generator_yields_removed() -> None:
    # Package installed but not in repo data → Removed
    data: dict = {}
    upg = _make_upgrade_for_packages(
        data,
        ["oldpkg-1.0-x86_64-1"],
        remove_packages=["alien"],
        repository="alien",
    )
    result = list(upg.packages())
    assert "oldpkg_Removed." in result


def test_packages_generator_yields_added() -> None:
    data = {"newpkg": repo_entry("1.0", "1")}
    upg = _make_upgrade_for_packages(
        data,
        [],
        new_packages=["alien"],
        repository="alien",
    )
    upg.utils.all_installed.return_value = {}  # nothing installed
    result = list(upg.packages())
    assert "newpkg_Added." in result


def test_packages_generator_no_added_when_installed() -> None:
    data = {"newpkg": repo_entry("1.0", "1")}
    upg = _make_upgrade_for_packages(
        data,
        [],
        new_packages=["alien"],
        repository="alien",
    )
    upg.utils.all_installed.return_value = {"newpkg": "newpkg-1.0-x86_64-1alien"}
    result = list(upg.packages())
    assert "newpkg_Added." not in result


# ---------------------------------------------------------------------------
# check_packages() — single repo
# ---------------------------------------------------------------------------


def _make_upgrade_for_check(
    data: dict,
    installed_packages: list,
    remove_packages: list | None = None,
    new_packages: list | None = None,
    pinned: set | None = None,
    repository: str = "alien",
) -> Upgrade:
    """Upgrade with _load_installed_packages side-effecting installed_packages."""
    upg = make_upgrade(data, repository=repository, pinned=pinned)
    upg.repos.remove_packages = remove_packages or []
    upg.repos.new_packages = new_packages or []
    paths = [Path(p) for p in installed_packages]

    def _load_side_effect(repo: str) -> None:
        upg.installed_packages = paths

    upg._load_installed_packages = _load_side_effect  # type: ignore[method-assign]
    upg.utils.all_installed.return_value = {
        Utilities.split_package(p)["name"]: p for p in installed_packages
    }
    return upg


def test_check_packages_upgradeable_raises_system_exit(capsys: pytest.CaptureFixture) -> None:
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = _make_upgrade_for_check(data, ["ffmpeg-7.1-x86_64-1alien"])
    with pytest.raises(SystemExit) as exc:
        upg.check_packages()
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "ffmpeg" in captured.out


def test_check_packages_up_to_date_raises_system_exit(capsys: pytest.CaptureFixture) -> None:
    data = {"ffmpeg": repo_entry("7.1", "1")}
    upg = _make_upgrade_for_check(data, ["ffmpeg-7.1-x86_64-1alien"])
    with pytest.raises(SystemExit) as exc:
        upg.check_packages()
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "up-to-date" in captured.out


def test_check_packages_pinned_counted(capsys: pytest.CaptureFixture) -> None:
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = _make_upgrade_for_check(data, ["ffmpeg-7.1-x86_64-1alien"], pinned={"ffmpeg"})
    with pytest.raises(SystemExit):
        upg.check_packages()
    assert upg.sum_pinned == 1
    assert upg.sum_upgrade == 0


def test_check_packages_remove_candidate(capsys: pytest.CaptureFixture) -> None:
    data: dict = {}
    upg = _make_upgrade_for_check(
        data,
        ["oldpkg-1.0-x86_64-1"],
        remove_packages=["alien"],
        repository="alien",
    )
    with pytest.raises(SystemExit):
        upg.check_packages()
    assert upg.sum_removed == 1


def test_check_packages_add_candidate(capsys: pytest.CaptureFixture) -> None:
    data = {"newpkg": repo_entry("1.0", "1")}
    upg = _make_upgrade_for_check(
        data,
        [],
        new_packages=["alien"],
        repository="alien",
    )
    upg.utils.all_installed.return_value = {}
    with pytest.raises(SystemExit):
        upg.check_packages()
    assert upg.sum_added == 1


def test_check_packages_wildcard_uses_all_repos(capsys: pytest.CaptureFixture) -> None:
    # When repository == "*", self.data is treated as multi-repo dict
    repo_data = {
        "alien": {"ffmpeg": repo_entry("7.2", "1")},
    }
    upg = make_upgrade(repo_data, repository="*")
    upg.repos.remove_packages = []
    upg.repos.new_packages = []
    paths = [Path("ffmpeg-7.1-x86_64-1alien")]

    def _load_side_effect(repo: str) -> None:
        upg.installed_packages = paths

    upg._load_installed_packages = _load_side_effect  # type: ignore[method-assign]
    upg.utils.all_installed.return_value = {"ffmpeg": "ffmpeg-7.1-x86_64-1alien"}
    with pytest.raises(SystemExit) as exc:
        upg.check_packages()
    assert exc.value.code == 0
    captured = capsys.readouterr()
    assert "ffmpeg" in captured.out


# ---------------------------------------------------------------------------
# ignore_tag mode: _load_installed_packages()
# ---------------------------------------------------------------------------


def test_load_installed_packages_ignore_tag_loads_all(tmp_path: Path) -> None:
    """When option_for_ignore_tag=True, all files in log_packages are loaded regardless of tag."""
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = make_upgrade(data, repository="alien")
    upg.option_for_ignore_tag = True
    upg.log_packages = tmp_path
    (tmp_path / "ffmpeg-7.1-x86_64-1_SBo").touch()
    (tmp_path / "bash-5.2-x86_64-1").touch()
    upg._load_installed_packages("alien")
    names = {p.name for p in upg.installed_packages}
    assert "ffmpeg-7.1-x86_64-1_SBo" in names
    assert "bash-5.2-x86_64-1" in names


def test_load_installed_packages_ignore_tag_false_uses_tag(tmp_path: Path) -> None:
    """When option_for_ignore_tag=False (default), tag-based filtering is used."""
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = make_upgrade(data, repository="alien")
    upg.option_for_ignore_tag = False
    # No filesystem access needed — just verify the tag path is taken (not the ignore_tag path).
    # With a real log_packages dir having no files the result is [].
    upg.log_packages = tmp_path
    upg._load_installed_packages("alien")
    assert upg.installed_packages == []


def test_packages_generator_ignore_tag_yields_cross_repo_package() -> None:
    """With option_for_ignore_tag=True, packages() yields a package installed from a different repo."""
    data = {"xsimd": {"version": "14.1.0", "build": "1", "package": "xsimd-14.1.0-x86_64-1alien.txz"}}
    upg = _make_upgrade_for_packages(data, ["xsimd-13.2.0-x86_64-1_SBo"])
    upg.option_for_ignore_tag = True
    result = list(upg.packages())
    assert "xsimd" in result


def test_load_installed_packages_ignore_tag_true_for_slack_repo(tmp_path: Path) -> None:
    """With option_for_ignore_tag=True, the slack repo branch is also bypassed and glob('*') is used."""
    data = {"ffmpeg": repo_entry("7.2", "1")}
    upg = make_upgrade(data, repository="slackware64")
    upg.option_for_ignore_tag = True
    upg.log_packages = tmp_path
    (tmp_path / "ffmpeg-7.1-x86_64-1_SBo").touch()
    (tmp_path / "bash-5.2-x86_64-1").touch()
    upg._load_installed_packages("slackware64")
    names = {p.name for p in upg.installed_packages}
    assert "ffmpeg-7.1-x86_64-1_SBo" in names
    assert "bash-5.2-x86_64-1" in names
