#!/usr/bin/python3
"""Tests for slpkg.remove_packages.RemovePackages pure methods."""

import json
from pathlib import Path
from unittest.mock import MagicMock, call, mock_open, patch

import pytest

from slpkg.remove_packages import RemovePackages


def make_remove_packages(
    packages: list | None = None,
    dependencies: list | None = None,
    packages_for_remove: list | None = None,
    deps_log: dict | None = None,
    option_for_resolve_off: bool = False,
    options: dict | None = None,
) -> RemovePackages:
    """Construct RemovePackages bypassing __init__ (avoids config_load)."""
    rp: RemovePackages = object.__new__(RemovePackages)
    rp.packages = list(packages or [])
    rp.dependencies = list(dependencies or [])
    rp.packages_for_remove = list(packages_for_remove or [])
    rp.deps_log = dict(deps_log or {})
    rp.options = dict(options or {})
    rp.option_for_resolve_off = option_for_resolve_off
    rp.red = ""
    rp.grey = ""
    rp.endc = ""
    rp.ask_question = True
    rp.answer_yes = False
    rp.removepkg = "removepkg"
    rp.process_log_file = MagicMock(spec=Path)
    rp.deps_log_file = MagicMock(spec=Path)
    rp.found_dependent_packages = {}
    rp.utils = MagicMock()
    rp.view = MagicMock()
    rp.multi_proc = MagicMock()
    rp.history_logger = MagicMock()
    rp.dialogbox = MagicMock()
    rp.choose_packages = MagicMock()
    rp.dialog = False
    return rp


# ---------------------------------------------------------------------------
# remove_doubles_dependencies()
# ---------------------------------------------------------------------------


def test_remove_doubles_deduplicates() -> None:
    rp = make_remove_packages(dependencies=["yasm", "bash", "yasm", "libvpx"])
    rp.remove_doubles_dependencies()
    assert sorted(rp.dependencies) == sorted(["yasm", "bash", "libvpx"])


def test_remove_doubles_no_duplicates_unchanged() -> None:
    rp = make_remove_packages(dependencies=["yasm", "bash", "libvpx"])
    rp.remove_doubles_dependencies()
    assert sorted(rp.dependencies) == sorted(["yasm", "bash", "libvpx"])


def test_remove_doubles_empty_list_stays_empty() -> None:
    rp = make_remove_packages(dependencies=[])
    rp.remove_doubles_dependencies()
    assert rp.dependencies == []


def test_remove_doubles_all_same_element() -> None:
    rp = make_remove_packages(dependencies=["yasm", "yasm", "yasm"])
    rp.remove_doubles_dependencies()
    assert rp.dependencies == ["yasm"]


def test_remove_doubles_single_element_unchanged() -> None:
    rp = make_remove_packages(dependencies=["yasm"])
    rp.remove_doubles_dependencies()
    assert rp.dependencies == ["yasm"]


def test_remove_doubles_result_is_list() -> None:
    rp = make_remove_packages(dependencies=["a", "b", "a"])
    rp.remove_doubles_dependencies()
    assert isinstance(rp.dependencies, list)


def test_remove_doubles_many_duplicates() -> None:
    rp = make_remove_packages(dependencies=["a", "b", "c", "a", "b", "c", "a", "b", "c"])
    rp.remove_doubles_dependencies()
    assert sorted(rp.dependencies) == ["a", "b", "c"]
    assert len(rp.dependencies) == 3


# ---------------------------------------------------------------------------
# add_packages_for_remove()
# ---------------------------------------------------------------------------


def test_add_packages_installed_package_added() -> None:
    """Installed package is appended to packages_for_remove."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=True)
    rp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    rp.add_packages_for_remove()
    assert "ffmpeg-7.1-x86_64-1" in rp.packages_for_remove


def test_add_packages_not_installed_skipped() -> None:
    """Package not installed is not added to packages_for_remove."""
    rp = make_remove_packages(packages=["ghost"], option_for_resolve_off=True)
    rp.utils.is_package_installed.return_value = ""
    rp.add_packages_for_remove()
    assert rp.packages_for_remove == []


def test_add_packages_deps_added_to_dependencies() -> None:
    """Direct deps from deps_log are appended to self.dependencies."""
    rp = make_remove_packages(
        packages=["ffmpeg"],
        deps_log={"ffmpeg": ["libvpx", "x264"]},
    )
    rp.utils.is_package_installed.side_effect = lambda p: f"{p}-1.0-x86_64-1" if p != "ffmpeg" else "ffmpeg-7.1-x86_64-1"
    rp.add_packages_for_remove()
    assert "libvpx" in rp.dependencies
    assert "x264" in rp.dependencies


def test_add_packages_dep_already_in_packages_skipped() -> None:
    """Dep already in self.packages is not added to dependencies."""
    rp = make_remove_packages(
        packages=["ffmpeg", "libvpx"],
        deps_log={"ffmpeg": ["libvpx"]},
    )
    rp.utils.is_package_installed.side_effect = lambda p: f"{p}-1.0-x86_64-1"
    rp.add_packages_for_remove()
    assert "libvpx" not in rp.dependencies


def test_add_packages_dep_not_installed_skipped() -> None:
    """Dep in deps_log but not installed is not added to dependencies."""
    rp = make_remove_packages(
        packages=["ffmpeg"],
        deps_log={"ffmpeg": ["libvpx"]},
    )
    # ffmpeg is installed, libvpx is not
    rp.utils.is_package_installed.side_effect = lambda p: "ffmpeg-7.1-x86_64-1" if p == "ffmpeg" else ""
    rp.add_packages_for_remove()
    assert "libvpx" not in rp.dependencies


def test_add_packages_resolve_off_skips_deps() -> None:
    """With option_for_resolve_off=True, no deps are added."""
    rp = make_remove_packages(
        packages=["ffmpeg"],
        deps_log={"ffmpeg": ["libvpx"]},
        option_for_resolve_off=True,
    )
    rp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    rp.add_packages_for_remove()
    assert rp.dependencies == []


def test_add_packages_no_deps_log_entry() -> None:
    """Package not in deps_log — no dependencies added."""
    rp = make_remove_packages(packages=["ffmpeg"], deps_log={})
    rp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    rp.add_packages_for_remove()
    assert rp.dependencies == []


# ---------------------------------------------------------------------------
# add_installed_dependencies_to_remove()
# ---------------------------------------------------------------------------


def test_add_installed_deps_adds_installed() -> None:
    """Installed dependency is appended to packages_for_remove."""
    rp = make_remove_packages(dependencies=["libvpx"])
    rp.utils.is_package_installed.return_value = "libvpx-1.13-x86_64-1"
    rp.add_installed_dependencies_to_remove()
    assert "libvpx-1.13-x86_64-1" in rp.packages_for_remove


def test_add_installed_deps_skips_not_installed() -> None:
    """Dependency not installed is skipped."""
    rp = make_remove_packages(dependencies=["ghost"])
    rp.utils.is_package_installed.return_value = ""
    rp.add_installed_dependencies_to_remove()
    assert rp.packages_for_remove == []


def test_add_installed_deps_no_duplicate() -> None:
    """Already-present package is not added twice."""
    rp = make_remove_packages(
        dependencies=["libvpx"],
        packages_for_remove=["libvpx-1.13-x86_64-1"],
    )
    rp.utils.is_package_installed.return_value = "libvpx-1.13-x86_64-1"
    rp.add_installed_dependencies_to_remove()
    assert rp.packages_for_remove.count("libvpx-1.13-x86_64-1") == 1


def test_add_installed_deps_multiple() -> None:
    """Multiple dependencies are all added."""
    rp = make_remove_packages(dependencies=["libvpx", "x264"])
    rp.utils.is_package_installed.side_effect = lambda p: f"{p}-1.0-x86_64-1"
    rp.add_installed_dependencies_to_remove()
    assert "libvpx-1.0-x86_64-1" in rp.packages_for_remove
    assert "x264-1.0-x86_64-1" in rp.packages_for_remove


def test_add_installed_deps_empty_dependencies() -> None:
    """Empty dependencies list — packages_for_remove unchanged."""
    rp = make_remove_packages(dependencies=[])
    rp.add_installed_dependencies_to_remove()
    assert rp.packages_for_remove == []


# ---------------------------------------------------------------------------
# find_dependent()
# ---------------------------------------------------------------------------


def test_find_dependent_detects_dependent_package(capsys) -> None:
    """Package that depends on the removed package is reported."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"vlc": ["ffmpeg", "libvpx"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.utils.is_package_installed.return_value = "vlc-3.0-x86_64-1"

    rp.find_dependent()

    assert "vlc" in rp.found_dependent_packages
    out = capsys.readouterr().out
    assert "Warning" in out


def test_find_dependent_dependent_not_installed_skipped() -> None:
    """Dependent package in deps_log but not installed is ignored."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"vlc": ["ffmpeg"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.utils.is_package_installed.return_value = ""

    rp.find_dependent()

    assert rp.found_dependent_packages == {}


def test_find_dependent_dependent_already_in_packages_skipped() -> None:
    """Dependent already in self.packages is not reported."""
    rp = make_remove_packages(
        packages=["vlc"],
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"vlc": ["ffmpeg"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.utils.is_package_installed.return_value = "vlc-3.0-x86_64-1"

    rp.find_dependent()

    assert "vlc" not in rp.found_dependent_packages


def test_find_dependent_dependent_already_in_dependencies_skipped() -> None:
    """Dependent already in self.dependencies is not reported."""
    rp = make_remove_packages(
        dependencies=["vlc"],
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"vlc": ["ffmpeg"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.utils.is_package_installed.return_value = "vlc-3.0-x86_64-1"

    rp.find_dependent()

    assert "vlc" not in rp.found_dependent_packages


def test_find_dependent_no_deps_log_no_warning(capsys) -> None:
    """Empty deps_log — no warning printed."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}

    rp.find_dependent()

    assert rp.found_dependent_packages == {}
    out = capsys.readouterr().out
    assert "Warning" not in out


def test_find_dependent_multiple_dependents(capsys) -> None:
    """Multiple dependent packages are all reported."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"vlc": ["ffmpeg"], "handbrake": ["ffmpeg"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.utils.is_package_installed.side_effect = lambda p: f"{p}-1.0-x86_64-1"

    rp.find_dependent()

    assert "vlc" in rp.found_dependent_packages
    assert "handbrake" in rp.found_dependent_packages


# ---------------------------------------------------------------------------
# remove_question()
# ---------------------------------------------------------------------------


def test_remove_question_answer_yes_config_returns_y() -> None:
    """When answer_yes=True, returns 'y' without user input."""
    rp = make_remove_packages()
    rp.answer_yes = True
    assert rp.remove_question() == "y"


def test_remove_question_ask_disabled_returns_n() -> None:
    """When ask_question=False, returns 'n' without prompting."""
    rp = make_remove_packages()
    rp.ask_question = False
    assert rp.remove_question() == "n"


def test_remove_question_user_input_y() -> None:
    """User types 'y' — returned as-is."""
    rp = make_remove_packages()
    rp.answer_yes = False
    with patch("builtins.input", return_value="y"):
        assert rp.remove_question() == "y"


def test_remove_question_user_input_n() -> None:
    """User types 'n' — returned as-is."""
    rp = make_remove_packages()
    rp.answer_yes = False
    with patch("builtins.input", return_value="n"):
        assert rp.remove_question() == "n"


def test_remove_question_keyboard_interrupt_raises_systemexit() -> None:
    """KeyboardInterrupt during input raises SystemExit(1)."""
    rp = make_remove_packages()
    rp.answer_yes = False
    with patch("builtins.input", side_effect=KeyboardInterrupt):
        with pytest.raises(SystemExit):
            rp.remove_question()


def test_remove_question_eoferror_raises_systemexit() -> None:
    """EOFError during input raises SystemExit(1)."""
    rp = make_remove_packages()
    rp.answer_yes = False
    with patch("builtins.input", side_effect=EOFError):
        with pytest.raises(SystemExit):
            rp.remove_question()


# ---------------------------------------------------------------------------
# remove_packages()
# ---------------------------------------------------------------------------


def test_remove_packages_calls_process_and_log() -> None:
    """process_and_log is called once per package."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1", "libvpx-1.13-x86_64-1"],
        deps_log={},
    )
    rp.utils.split_package.side_effect = lambda p: {"name": p.rsplit("-", 3)[0], "version": "1.0"}
    rp.multi_proc.process_and_log.return_value = True
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    assert rp.multi_proc.process_and_log.call_count == 2


def test_remove_packages_success_logs_history() -> None:
    """Successful removal writes to history_logger."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = True
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    rp.history_logger.write.assert_called_once_with("remove", "ffmpeg", "ffmpeg-7.1-x86_64-1", "local")


def test_remove_packages_failure_skips_history() -> None:
    """Failed removal does NOT write to history_logger."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = False
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    rp.history_logger.write.assert_not_called()


def test_remove_packages_removes_from_deps_log() -> None:
    """Successfully removed package is purged from deps_log."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"ffmpeg": ["libvpx"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = True
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    assert "ffmpeg" not in rp.deps_log


def test_remove_packages_failure_keeps_deps_log_entry() -> None:
    """Failed removal leaves deps_log entry intact."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"ffmpeg": ["libvpx"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = False
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    assert "ffmpeg" in rp.deps_log


def test_remove_packages_writes_updated_deps_log() -> None:
    """Updated deps_log is written back to file as JSON."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={"ffmpeg": ["libvpx"], "vlc": ["ffmpeg"]},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = True
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    written = rp.deps_log_file.write_text.call_args[0][0]
    data = json.loads(written)
    assert "ffmpeg" not in data
    assert "vlc" in data


def test_remove_packages_unlinks_process_log() -> None:
    """Old process log file is removed at the start."""
    rp = make_remove_packages(
        packages_for_remove=["ffmpeg-7.1-x86_64-1"],
        deps_log={},
    )
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    rp.multi_proc.process_and_log.return_value = True
    rp.deps_log_file.write_text = MagicMock()

    rp.remove_packages()

    rp.process_log_file.unlink.assert_called_once_with(missing_ok=True)


# ---------------------------------------------------------------------------
# remove() — additional branches
# ---------------------------------------------------------------------------


def test_remove_empty_packages_does_nothing() -> None:
    """Empty packages list — no action taken."""
    rp = make_remove_packages(packages=[])
    rp.add_packages_for_remove = MagicMock()

    rp.remove()

    rp.add_packages_for_remove.assert_not_called()


def test_remove_loads_deps_log_when_resolve_on() -> None:
    """With option_for_resolve_off=False, deps_log is loaded via read_json_file."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=False)
    rp.utils.read_json_file.return_value = {"ffmpeg": ["libvpx"]}
    rp.add_packages_for_remove = MagicMock()
    rp.remove_doubles_dependencies = MagicMock()
    rp.choose_packages_for_remove = MagicMock(return_value=[])
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.find_dependent = MagicMock()

    rp.remove()

    rp.utils.read_json_file.assert_called_once()


def test_remove_skips_deps_log_when_resolve_off() -> None:
    """With option_for_resolve_off=True, read_json_file is NOT called."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=True)
    rp.add_packages_for_remove = MagicMock()
    rp.remove_doubles_dependencies = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.find_dependent = MagicMock()

    rp.remove()

    rp.utils.read_json_file.assert_not_called()


def test_remove_upgrade_true_calls_choose_packages_for_remove() -> None:
    """With upgrade=True, choose_packages_for_remove is called for self.packages."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=True)
    rp.choose_packages_for_remove = MagicMock(return_value=["ffmpeg"])
    rp.add_packages_for_remove = MagicMock()
    rp.remove_doubles_dependencies = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.find_dependent = MagicMock()
    rp.remove_question = MagicMock(return_value="n")

    rp.remove(upgrade=True)

    rp.choose_packages_for_remove.assert_called_once_with(["ffmpeg"], True)


def test_remove_upgrade_answer_n_skips_remove_packages() -> None:
    """With upgrade=True and answer='n', remove_packages() is not called."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=True)
    rp.choose_packages_for_remove = MagicMock(return_value=["ffmpeg"])
    rp.add_packages_for_remove = MagicMock()
    rp.remove_doubles_dependencies = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.find_dependent = MagicMock()
    rp.remove_question = MagicMock(return_value="n")
    remove_mock = MagicMock()
    rp.remove_packages = remove_mock

    rp.remove(upgrade=True)

    remove_mock.assert_not_called()


def test_remove_upgrade_answer_y_calls_remove_packages() -> None:
    """With upgrade=True and answer='y', remove_packages() is called."""
    rp = make_remove_packages(packages=["ffmpeg"], option_for_resolve_off=True)
    rp.choose_packages_for_remove = MagicMock(return_value=["ffmpeg"])
    rp.add_packages_for_remove = MagicMock()
    rp.remove_doubles_dependencies = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.find_dependent = MagicMock()
    rp.remove_question = MagicMock(return_value="y")
    remove_mock = MagicMock()
    rp.remove_packages = remove_mock
    rp.utils.finished_time = MagicMock()

    rp.remove(upgrade=True)

    remove_mock.assert_called_once()


# ---------------------------------------------------------------------------
# remove() dry-run (option_check / --dry-run)
# ---------------------------------------------------------------------------


def test_remove_dry_run_skips_remove_packages() -> None:
    """--dry-run: view is shown, find_dependent() is called, but remove_packages() is not."""
    rp = make_remove_packages(
        packages=["pkg1"],
        options={"option_check": True},
        option_for_resolve_off=True,
    )
    rp.find_dependent = MagicMock()
    rp.add_packages_for_remove = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    remove_mock = MagicMock()
    rp.remove_packages = remove_mock

    rp.remove()

    rp.view.remove_packages.assert_called_once()
    rp.find_dependent.assert_called_once()
    remove_mock.assert_not_called()


def test_remove_dry_run_does_not_call_question() -> None:
    """--dry-run: view.question() is never called."""
    rp = make_remove_packages(
        packages=["pkg1"],
        options={"option_check": True},
        option_for_resolve_off=True,
    )
    rp.find_dependent = MagicMock()
    rp.add_packages_for_remove = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    rp.remove_packages = MagicMock()

    rp.remove()

    rp.view.question.assert_not_called()


def test_remove_normal_calls_question_and_remove_packages() -> None:
    """Without --dry-run, remove() calls view.question() and remove_packages()."""
    rp = make_remove_packages(
        packages=["pkg1"],
        options={},
        option_for_resolve_off=True,
    )
    rp.find_dependent = MagicMock()
    rp.add_packages_for_remove = MagicMock()
    rp.add_installed_dependencies_to_remove = MagicMock()
    remove_mock = MagicMock()
    rp.remove_packages = remove_mock

    rp.remove()

    rp.view.question.assert_called_once()
    remove_mock.assert_called_once()


# ---------------------------------------------------------------------------
# _load_deps_log()
# ---------------------------------------------------------------------------


def test_load_deps_log_resolve_off_skips_read() -> None:
    """With option_for_resolve_off=True, read_json_file is never called."""
    rp = make_remove_packages(option_for_resolve_off=True)
    rp._load_deps_log()
    rp.utils.read_json_file.assert_not_called()


def test_load_deps_log_file_exists_loads_it() -> None:
    """With resolve on and file present, deps_log is populated."""
    rp = make_remove_packages(option_for_resolve_off=False)
    rp.deps_log_file.is_file.return_value = True
    rp.utils.read_json_file.return_value = {"ffmpeg": ["libvpx"]}
    rp._load_deps_log()
    assert rp.deps_log == {"ffmpeg": ["libvpx"]}


def test_load_deps_log_file_missing_leaves_empty() -> None:
    """With resolve on but file absent, deps_log stays empty."""
    rp = make_remove_packages(option_for_resolve_off=False)
    rp.deps_log_file.is_file.return_value = False
    rp._load_deps_log()
    assert rp.deps_log == {}
    rp.utils.read_json_file.assert_not_called()


# ---------------------------------------------------------------------------
# _get_confirmation()
# ---------------------------------------------------------------------------


def test_get_confirmation_upgrade_delegates_to_remove_question() -> None:
    """upgrade=True calls remove_question() and returns its result."""
    rp = make_remove_packages()
    rp.remove_question = MagicMock(return_value="n")
    assert rp._get_confirmation(upgrade=True) == "n"
    rp.remove_question.assert_called_once()


def test_get_confirmation_non_upgrade_calls_view_question_returns_y() -> None:
    """upgrade=False calls view.question() and always returns 'y'."""
    rp = make_remove_packages()
    assert rp._get_confirmation(upgrade=False) == "y"
    rp.view.question.assert_called_once()


# ---------------------------------------------------------------------------
# _build_choices()
# ---------------------------------------------------------------------------


def test_build_choices_installed_package_added() -> None:
    """Installed package produces a choice tuple."""
    rp = make_remove_packages()
    rp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    rp.utils.split_package.return_value = {"name": "ffmpeg", "version": "7.1"}
    choices = rp._build_choices(["ffmpeg"])
    assert len(choices) == 1
    assert choices[0][0] == "ffmpeg"
    assert choices[0][1] == "7.1"
    assert choices[0][2] is True


def test_build_choices_not_installed_skipped() -> None:
    """Package not installed produces no choice tuple."""
    rp = make_remove_packages()
    rp.utils.is_package_installed.return_value = ""
    choices = rp._build_choices(["ghost"])
    assert choices == []


def test_build_choices_mixed_packages() -> None:
    """Only installed packages appear in the result."""
    rp = make_remove_packages()
    rp.utils.is_package_installed.side_effect = lambda p: f"{p}-1.0-x86_64-1" if p != "ghost" else ""
    rp.utils.split_package.side_effect = lambda p: {"name": p.rsplit("-", 3)[0], "version": "1.0"}
    choices = rp._build_choices(["ffmpeg", "ghost", "libvpx"])
    names = [c[0] for c in choices]
    assert "ffmpeg" in names
    assert "libvpx" in names
    assert "ghost" not in names


# ---------------------------------------------------------------------------
# _select_via_dialog()
# ---------------------------------------------------------------------------


def test_select_via_dialog_single_package_returned_as_is() -> None:
    """Less than 2 packages bypasses the dialog and returns the list unchanged."""
    rp = make_remove_packages()
    result = rp._select_via_dialog([], "text", "title", ["ffmpeg"])
    assert result == ["ffmpeg"]
    rp.dialogbox.checklist.assert_not_called()


def test_select_via_dialog_ok_returns_selection() -> None:
    """code='ok' returns the selected package list."""
    rp = make_remove_packages()
    rp.clear_screen = ""
    rp.dialogbox.checklist.return_value = ("ok", ["ffmpeg", "libvpx"])
    choices: list = [("ffmpeg", "7.1", True, ""), ("libvpx", "1.13", True, "")]
    result = rp._select_via_dialog(choices, "text", "title", ["ffmpeg", "libvpx"])
    assert result == ["ffmpeg", "libvpx"]


def test_select_via_dialog_cancel_returns_empty() -> None:
    """Non-ok code returns an empty list."""
    rp = make_remove_packages()
    rp.clear_screen = ""
    rp.dialogbox.checklist.return_value = ("cancel", [])
    choices: list = [("ffmpeg", "7.1", True, ""), ("libvpx", "1.13", True, "")]
    result = rp._select_via_dialog(choices, "text", "title", ["ffmpeg", "libvpx"])
    assert result == []
