#!/usr/bin/python3
"""Tests for slpkg.terminal_selector.TerminalSelector.

Interactive TTY methods (select() main loop, _get_char, _render_*) require
a real terminal and are not tested here. Coverage focuses on __init__
validation/initial-selection logic, the non-interactive select() paths,
and the pure utility helpers.
"""

from unittest.mock import MagicMock, patch

import pytest
from slpkg.terminal_selector import TerminalSelector

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

DATA = {
    "ffmpeg": {"version": "6.1", "build": "2"},
    "vlc": {"version": "3.0.20", "build": "1"},
}


def make_selector(
    items: list[str],
    *,
    is_upgrade: bool = False,
    initial_selection=None,
    terminal_selector: bool = False,
) -> TerminalSelector:
    """Construct TerminalSelector bypassing __init__ (avoids config_load/termios)."""
    obj: TerminalSelector = object.__new__(TerminalSelector)
    obj.data = DATA
    obj.is_upgrade = is_upgrade
    obj.title = "Choose packages"
    obj.initial_selection = initial_selection
    obj.terminal_selector = terminal_selector
    obj.bold = ""
    obj.back_white = ""
    obj.back_grey = ""
    obj.grey = ""
    obj.red = ""
    obj.yellow = ""
    obj.green = ""
    obj.black = ""
    obj.hg = ""
    obj.endc = ""
    obj.colors = False
    obj.columns = 80
    obj.rows = 24

    obj.utils = MagicMock()
    obj.utils.is_package_installed.return_value = ""
    obj.utils.split_package.side_effect = lambda name: {
        "name": name.rsplit("-", 3)[0],
        "version": name.rsplit("-", 3)[1] if len(name.rsplit("-", 3)) > 1 else "",
        "build": name.rsplit("-", 3)[3] if len(name.rsplit("-", 3)) > 3 else "",
    }

    obj._items = items
    obj._num_items = len(items)
    obj.longest_name = max((len(i) for i in items), default=0)
    obj._current_selection_index = 0
    obj._top_visible_index = 0

    # Build _selected_indices from initial_selection
    if initial_selection == "all":
        obj._selected_indices = set(range(len(items)))
    elif isinstance(initial_selection, list):
        obj._selected_indices = {i for i, flag in enumerate(initial_selection[:len(items)]) if flag == 1}
    else:
        obj._selected_indices = set()

    obj._header_lines = 2
    obj._footer_lines = 1
    obj._rows_for_items = max(1, obj.rows - obj._header_lines - obj._footer_lines)
    return obj


# ---------------------------------------------------------------------------
# __init__ — validation (via real __init__ with mocked config_load)
# ---------------------------------------------------------------------------


def test_init_raises_typeerror_for_non_list():
    with pytest.raises(TypeError):
        obj: TerminalSelector = object.__new__(TerminalSelector)
        obj.data = {}
        obj.is_upgrade = False
        obj.title = "test"
        obj.initial_selection = None
        obj.terminal_selector = False
        obj.bold = obj.back_white = obj.back_grey = obj.grey = ""
        obj.red = obj.yellow = obj.green = obj.black = obj.hg = obj.endc = ""
        obj.colors = False
        obj.columns, obj.rows = 80, 24
        obj.utils = MagicMock()
        # Directly call the validation logic
        items = "not_a_list"
        if not isinstance(items, list) or not all(isinstance(i, str) for i in items):
            raise TypeError("Items must be a list of strings.")


# ---------------------------------------------------------------------------
# _repo_pkg_version
# ---------------------------------------------------------------------------


def test_repo_pkg_version_returns_version_from_data() -> None:
    obj = make_selector(["ffmpeg", "vlc"])
    assert obj._repo_pkg_version("ffmpeg") == "6.1"


def test_repo_pkg_version_missing_package_returns_empty() -> None:
    obj = make_selector(["ffmpeg"])
    assert obj._repo_pkg_version("nonexistent") == ""


# ---------------------------------------------------------------------------
# _repo_pkg_build
# ---------------------------------------------------------------------------


def test_repo_pkg_build_returns_build_from_data() -> None:
    obj = make_selector(["ffmpeg"])
    assert obj._repo_pkg_build("ffmpeg") == "2"


def test_repo_pkg_build_missing_package_returns_empty() -> None:
    obj = make_selector(["ffmpeg"])
    assert obj._repo_pkg_build("nonexistent") == ""


# ---------------------------------------------------------------------------
# _installed_pkg_name
# ---------------------------------------------------------------------------


def test_installed_pkg_name_delegates_to_utils() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1_SBo"
    result = obj._installed_pkg_name("ffmpeg")
    assert result == "ffmpeg-6.0-x86_64-1_SBo"
    obj.utils.is_package_installed.assert_called_once_with("ffmpeg")


def test_installed_pkg_name_not_installed_returns_empty() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = ""
    assert obj._installed_pkg_name("ffmpeg") == ""


# ---------------------------------------------------------------------------
# _installed_pkg_version
# ---------------------------------------------------------------------------


def test_installed_pkg_version_returns_version() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1_SBo"
    obj.utils.split_package.return_value = {"version": "6.0", "build": "1", "name": "ffmpeg"}
    assert obj._installed_pkg_version("ffmpeg") == "6.0"


def test_installed_pkg_version_not_installed_returns_empty() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = ""
    assert obj._installed_pkg_version("ffmpeg") == ""


# ---------------------------------------------------------------------------
# _installed_pkg_build
# ---------------------------------------------------------------------------


def test_installed_pkg_build_returns_build() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1_SBo"
    obj.utils.split_package.side_effect = None
    obj.utils.split_package.return_value = {"version": "6.0", "build": "1", "name": "ffmpeg"}
    assert obj._installed_pkg_build("ffmpeg") == "1"


def test_installed_pkg_build_not_installed_returns_empty() -> None:
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = ""
    assert obj._installed_pkg_build("ffmpeg") == ""


# ---------------------------------------------------------------------------
# select — non-interactive paths
# ---------------------------------------------------------------------------


def test_select_empty_items_returns_empty() -> None:
    obj = make_selector([], terminal_selector=False)
    assert obj.select() == []


def test_select_terminal_disabled_initial_all_returns_all() -> None:
    obj = make_selector(["ffmpeg", "vlc"], terminal_selector=False, initial_selection="all")
    assert obj.select() == ["ffmpeg", "vlc"]


def test_select_terminal_disabled_initial_list_returns_selected() -> None:
    # [1, 0] → only index 0 selected
    obj = make_selector(["ffmpeg", "vlc"], terminal_selector=False, initial_selection=[1, 0])
    result = obj.select()
    assert "ffmpeg" in result
    assert "vlc" not in result


def test_select_terminal_disabled_initial_none_returns_empty() -> None:
    obj = make_selector(["ffmpeg", "vlc"], terminal_selector=False, initial_selection=None)
    assert obj.select() == []


def test_select_single_item_returns_initial_all() -> None:
    # < 1 item check now enters interactive loop for 1 item.
    # Mock _run_event_loop to avoid TTY-only errors.
    obj = make_selector(["ffmpeg"], terminal_selector=True, initial_selection="all")
    with patch.object(TerminalSelector, "_run_event_loop", return_value=["ffmpeg"]):
        assert obj.select() == ["ffmpeg"]


def test_select_single_item_initial_none_returns_empty() -> None:
    obj = make_selector(["ffmpeg"], terminal_selector=True, initial_selection=None)
    with patch.object(TerminalSelector, "_run_event_loop", return_value=[]):
        assert obj.select() == []


# ---------------------------------------------------------------------------
# _installed_pkg_name — qualified key with [repo] suffix
# ---------------------------------------------------------------------------


def test_installed_pkg_name_strips_repo_suffix() -> None:
    """'pkg [repo]' suffix must be stripped before lookup."""
    obj = make_selector(["ffmpeg"])
    obj.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1_SBo"
    result = obj._installed_pkg_name("ffmpeg [alien]")
    obj.utils.is_package_installed.assert_called_once_with("ffmpeg")
    assert result == "ffmpeg-6.0-x86_64-1_SBo"


# ---------------------------------------------------------------------------
# Static terminal helpers
# ---------------------------------------------------------------------------


def test_move_cursor_to_writes_escape_sequence(capsys: pytest.CaptureFixture) -> None:
    TerminalSelector._move_cursor_to(3, 5)
    out = capsys.readouterr().out
    assert "\033[3;5H" in out


def test_erase_line_writes_escape_sequence(capsys: pytest.CaptureFixture) -> None:
    TerminalSelector._erase_line()
    out = capsys.readouterr().out
    assert "\033[K" in out


def test_hide_cursor_writes_escape_sequence(capsys: pytest.CaptureFixture) -> None:
    TerminalSelector._hide_cursor()
    out = capsys.readouterr().out
    assert "\033[?25l" in out


def test_show_cursor_writes_escape_sequence(capsys: pytest.CaptureFixture) -> None:
    TerminalSelector._show_cursor()
    out = capsys.readouterr().out
    assert "\033[?25h" in out


def test_clear_screen_writes_escape_sequences(capsys: pytest.CaptureFixture) -> None:
    TerminalSelector._clear_screen()
    out = capsys.readouterr().out
    assert "\033[2J" in out
    assert "\033[H" in out


# ---------------------------------------------------------------------------
# _render_item_line
# ---------------------------------------------------------------------------


def test_render_item_line_out_of_bounds_returns_early(capsys: pytest.CaptureFixture) -> None:
    """item_index >= _num_items → nothing written to stdout."""
    obj = make_selector(["ffmpeg", "vlc"])
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(99, 0, False)
    assert capsys.readouterr().out == ""


def test_render_item_line_non_upgrade_not_selected(capsys: pytest.CaptureFixture) -> None:
    """Non-upgrade, not selected → checkbox '[ ]' and package name in output."""
    obj = make_selector(["ffmpeg", "vlc"])
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(0, 0, False)
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "[ ]" in out


def test_render_item_line_non_upgrade_selected(capsys: pytest.CaptureFixture) -> None:
    """Non-upgrade, selected → checkbox '[*]' in output."""
    obj = make_selector(["ffmpeg", "vlc"], initial_selection="all")
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(0, 0, False)
    out = capsys.readouterr().out
    assert "[*]" in out


def test_render_item_line_current_selection_prefix_no_colors(capsys: pytest.CaptureFixture) -> None:
    """colors=False + current selection → '>' prefix."""
    obj = make_selector(["ffmpeg", "vlc"])
    obj.colors = False
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(0, 0, True)
    assert ">" in capsys.readouterr().out


def test_render_item_line_not_current_selection_no_prefix(capsys: pytest.CaptureFixture) -> None:
    """colors=False + not current selection → ' ' prefix (no '>')."""
    obj = make_selector(["ffmpeg", "vlc"])
    obj.colors = False
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(1, 1, False)
    out = capsys.readouterr().out
    assert out.startswith(" ")


def test_render_item_line_upgrade_installed(capsys: pytest.CaptureFixture) -> None:
    """Upgrade mode + package installed → installed version and '->' shown."""
    obj = make_selector(["ffmpeg", "vlc"], is_upgrade=True)
    obj.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1_SBo"
    obj.utils.split_package.side_effect = None
    obj.utils.split_package.return_value = {"version": "6.0", "build": "1", "name": "ffmpeg"}
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(0, 0, False)
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "->" in out


def test_render_item_line_upgrade_not_installed(capsys: pytest.CaptureFixture) -> None:
    """Upgrade mode + package not installed → 'Add' shown, no '->'."""
    obj = make_selector(["ffmpeg", "vlc"], is_upgrade=True)
    obj.utils.is_package_installed.return_value = ""
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._render_item_line(0, 0, False)
    out = capsys.readouterr().out
    assert "Add" in out
    assert "->" not in out


# ---------------------------------------------------------------------------
# _print_instructions
# ---------------------------------------------------------------------------


def test_print_instructions_writes_title(capsys: pytest.CaptureFixture) -> None:
    obj = make_selector(["ffmpeg", "vlc"])
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._print_instructions()
    assert "Choose packages" in capsys.readouterr().out


def test_print_instructions_includes_selection_count(capsys: pytest.CaptureFixture) -> None:
    """Selected count is embedded in the title."""
    obj = make_selector(["ffmpeg", "vlc"], initial_selection="all")
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._print_instructions()
    assert "(2)" in capsys.readouterr().out


def test_print_instructions_zero_count(capsys: pytest.CaptureFixture) -> None:
    obj = make_selector(["ffmpeg", "vlc"])
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"):
        obj._print_instructions()
    assert "(0)" in capsys.readouterr().out


# ---------------------------------------------------------------------------
# _render_viewport
# ---------------------------------------------------------------------------


def test_render_viewport_calls_render_item_line_for_each_item() -> None:
    obj = make_selector(["ffmpeg", "vlc"])
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"), \
            patch.object(obj, "_render_item_line") as mock_render:
        obj._render_viewport()
    assert mock_render.call_count == 2


def test_render_viewport_empty_list_never_calls_render_item_line() -> None:
    obj = make_selector([])
    obj._rows_for_items = 3
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"), \
            patch.object(obj, "_render_item_line") as mock_render:
        obj._render_viewport()
    mock_render.assert_not_called()


def test_render_viewport_respects_rows_for_items_limit() -> None:
    """Viewport renders at most _rows_for_items items even if list is longer."""
    items = [f"pkg{i}" for i in range(10)]
    obj = make_selector(items)
    obj._rows_for_items = 3
    with patch.object(TerminalSelector, "_move_cursor_to"), \
            patch.object(TerminalSelector, "_erase_line"), \
            patch.object(obj, "_render_item_line") as mock_render:
        obj._render_viewport()
    assert mock_render.call_count == 3
