#!/usr/bin/python3
"""Tests for slpkg.reinstall.Reinstall."""

from __future__ import annotations

from unittest.mock import MagicMock, call, patch

import pytest

from slpkg.reinstall import Reinstall


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

REPOS = {
    "sbo":   {"enable": True,  "repo_tag": "_SBo"},
    "ponce": {"enable": False, "repo_tag": "_SBo"},
    "alien": {"enable": True,  "repo_tag": "alien"},
    "slack": {"enable": True,  "repo_tag": ""},
}


def make_reinstall(packages: list[str] | None = None, options: dict | None = None) -> Reinstall:
    """Build a Reinstall instance bypassing real config/Repositories."""
    ri: Reinstall = object.__new__(Reinstall)
    ri.packages = packages or []
    ri.options = dict(options or {})
    ri.options["option_reinstall"] = True

    mock_repos = MagicMock()
    mock_repos.repositories = dict(REPOS)
    mock_repos.sbo_repo_name = "sbo"
    mock_repos.ponce_repo_name = "ponce"
    mock_repos.slack_repo_name = "slack"
    ri.repos = mock_repos

    ri.utils = MagicMock()
    ri.load_data = MagicMock()
    ri.pkg_validator = MagicMock()
    ri.sbo_repos = {"sbo", "ponce"}
    return ri


# ---------------------------------------------------------------------------
# _detect_repo()
# ---------------------------------------------------------------------------


def test_detect_repo_sbo_tag() -> None:
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1_SBo"
    ri.utils.split_package.return_value = {"tag": "_SBo"}
    assert ri._detect_repo("ffmpeg") == "sbo"


def test_detect_repo_alien_tag() -> None:
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = "vlc-3.0-x86_64-1alien"
    ri.utils.split_package.return_value = {"tag": "alien"}
    assert ri._detect_repo("vlc") == "alien"


def test_detect_repo_no_tag_returns_slack() -> None:
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = "bash-5.2-x86_64-1"
    ri.utils.split_package.return_value = {"tag": ""}
    assert ri._detect_repo("bash") == "slack"


def test_detect_repo_not_installed_returns_none() -> None:
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = ""
    assert ri._detect_repo("nonexistent") is None


def test_detect_repo_unknown_tag_returns_none() -> None:
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = "mypkg-1.0-x86_64-1_xyz"
    ri.utils.split_package.return_value = {"tag": "_xyz"}
    assert ri._detect_repo("mypkg") is None


def test_detect_repo_skips_disabled_repos() -> None:
    """ponce is disabled — _SBo tag should resolve to sbo (the enabled one)."""
    ri = make_reinstall()
    ri.utils.is_package_installed.return_value = "vmpk-0.8-x86_64-1_SBo"
    ri.utils.split_package.return_value = {"tag": "_SBo"}
    result = ri._detect_repo("vmpk")
    assert result == "sbo"


# ---------------------------------------------------------------------------
# _group_by_repo()
# ---------------------------------------------------------------------------


def test_group_by_repo_groups_correctly() -> None:
    ri = make_reinstall()

    def fake_detect(pkg: str) -> str | None:
        return {"ffmpeg": "sbo", "vlc": "alien", "bash": "slack"}.get(pkg)

    ri._detect_repo = fake_detect  # type: ignore[method-assign]
    result = ri._group_by_repo(["ffmpeg", "vlc", "bash"])
    assert result == {"sbo": ["ffmpeg"], "alien": ["vlc"], "slack": ["bash"]}


def test_group_by_repo_multiple_packages_same_repo() -> None:
    ri = make_reinstall()
    ri._detect_repo = lambda pkg: "sbo"  # type: ignore[method-assign]
    result = ri._group_by_repo(["ffmpeg", "vlc"])
    assert result == {"sbo": ["ffmpeg", "vlc"]}


def test_group_by_repo_skips_unknown(capsys: pytest.CaptureFixture[str]) -> None:
    ri = make_reinstall()
    ri._detect_repo = lambda pkg: None  # type: ignore[method-assign]
    result = ri._group_by_repo(["mypkg"])
    assert result == {}
    out = capsys.readouterr().out
    assert "mypkg" in out


def test_group_by_repo_preserves_insertion_order() -> None:
    ri = make_reinstall()
    mapping = {"a": "sbo", "b": "alien", "c": "sbo"}
    ri._detect_repo = lambda pkg: mapping.get(pkg)  # type: ignore[method-assign]
    result = ri._group_by_repo(["a", "b", "c"])
    assert list(result.keys()) == ["sbo", "alien"]
    assert result["sbo"] == ["a", "c"]


def test_group_by_repo_empty_input() -> None:
    ri = make_reinstall()
    assert ri._group_by_repo([]) == {}


# ---------------------------------------------------------------------------
# run() — explicit repository override
# ---------------------------------------------------------------------------


def test_run_explicit_repo_skips_detection() -> None:
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "alien"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    ri.load_data.load.assert_called_once_with("alien", message=True)
    mock_pkgs.assert_called_once()


def test_run_explicit_repo_uses_packages_for_binary() -> None:
    ri = make_reinstall(packages=["vlc"], options={"option_repository": "alien"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["vlc"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs, \
         patch("slpkg.reinstall.Slackbuilds") as mock_sbo:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    mock_pkgs.assert_called_once()
    mock_sbo.assert_not_called()


def test_run_explicit_repo_uses_slackbuilds_for_sbo() -> None:
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "sbo"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Slackbuilds") as mock_sbo, \
         patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_sbo.return_value.execute = MagicMock()
        ri.run()

    mock_sbo.assert_called_once()
    mock_pkgs.assert_not_called()


# ---------------------------------------------------------------------------
# run() — auto-detection path
# ---------------------------------------------------------------------------


def test_run_auto_detect_calls_detect_per_package() -> None:
    ri = make_reinstall(packages=["ffmpeg", "vlc"])
    ri.utils.case_insensitive_pattern_matching = MagicMock(side_effect=lambda pkgs, *a, **kw: pkgs)
    ri._detect_repo = MagicMock(return_value="alien")  # type: ignore[method-assign]

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    assert ri._detect_repo.call_count == 2


def test_run_option_reinstall_always_true() -> None:
    """run() must always set option_reinstall=True regardless of input options."""
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "alien"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    _, _, kwargs = mock_pkgs.mock_calls[0]
    passed_options: dict = mock_pkgs.call_args[0][3]
    assert passed_options.get("option_reinstall") is True


def test_run_calls_pkg_validator_installed() -> None:
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "alien"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    ri.pkg_validator.is_package_installed.assert_called_once_with(["ffmpeg"])


def test_run_calls_pkg_validator_exists() -> None:
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "alien"})
    data = {"ffmpeg": {"version": "7.1"}}
    ri.load_data.load.return_value = data
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    ri.pkg_validator.is_package_exists.assert_called_once_with(["ffmpeg"], data)


def test_run_passes_mode_install() -> None:
    ri = make_reinstall(packages=["ffmpeg"], options={"option_repository": "alien"})
    ri.utils.case_insensitive_pattern_matching = MagicMock(return_value=["ffmpeg"])

    with patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    assert mock_pkgs.call_args[1]["mode"] == "install"


def test_run_multi_repo_executes_each(capsys: pytest.CaptureFixture[str]) -> None:
    ri = make_reinstall(packages=["ffmpeg", "vlc"])
    mapping = {"ffmpeg": "sbo", "vlc": "alien"}
    ri._detect_repo = lambda pkg: mapping.get(pkg)  # type: ignore[method-assign]
    ri.utils.case_insensitive_pattern_matching = MagicMock(side_effect=lambda pkgs, *a, **kw: pkgs)

    with patch("slpkg.reinstall.Slackbuilds") as mock_sbo, \
         patch("slpkg.reinstall.Packages") as mock_pkgs:
        mock_sbo.return_value.execute = MagicMock()
        mock_pkgs.return_value.execute = MagicMock()
        ri.run()

    mock_sbo.assert_called_once()
    mock_pkgs.assert_called_once()
