#!/usr/bin/python3
"""Tests for slpkg.search.SearchPackage."""

import json
from unittest.mock import MagicMock

import pytest

from slpkg.search import SearchPackage


def make_search(
    packages: list | None = None,
    repository: str = "*",
    data: dict | None = None,
    option_no_case: bool = False,
    option_json: bool = False,
    option_quiet: bool = False,
    option_pkg_version: bool = False,
    option_pkg_description: bool = False,
    option_select: bool = False,
    option_dialog: bool = False,
) -> SearchPackage:
    """Construct SearchPackage bypassing __init__ (avoids config_load/Repositories)."""
    sp: SearchPackage = object.__new__(SearchPackage)
    sp.packages = list(packages or [])
    sp.repository = repository
    sp.data = dict(data or {})
    sp.repo_data = {}
    sp.all_data = {}
    sp.data_dict = {}
    sp.matching = 0
    sp.option_for_no_case = option_no_case
    sp.option_json = option_json
    sp.option_quiet = option_quiet
    sp.option_for_pkg_version = option_pkg_version
    sp.option_for_pkg_description = option_pkg_description
    sp.option_select = option_select
    sp.option_dialog = option_dialog
    sp.grey = ""
    sp.green = ""
    sp.yellow = ""
    sp.endc = ""
    sp.utils = MagicMock()
    sp.view_process = MagicMock()
    return sp


# ---------------------------------------------------------------------------
# _is_match()
# ---------------------------------------------------------------------------


def test_is_match_wildcard_matches_any_name() -> None:
    sp = make_search()
    assert sp._is_match("*", "ffmpeg") is True


def test_is_match_exact_name_matches() -> None:
    sp = make_search()
    assert sp._is_match("ffmpeg", "ffmpeg") is True


def test_is_match_substring_matches() -> None:
    sp = make_search()
    assert sp._is_match("mpeg", "ffmpeg") is True


def test_is_match_no_match_returns_false() -> None:
    sp = make_search()
    assert sp._is_match("vlc", "ffmpeg") is False


def test_is_match_case_insensitive_matches() -> None:
    sp = make_search(option_no_case=True)
    assert sp._is_match("FFMPEG", "ffmpeg") is True


def test_is_match_option_select_disables_substring() -> None:
    sp = make_search(option_select=True)
    assert sp._is_match("mpeg", "ffmpeg") is False


def test_is_match_option_dialog_disables_substring() -> None:
    sp = make_search(option_dialog=True)
    assert sp._is_match("mpeg", "ffmpeg") is False


def test_is_match_option_select_still_allows_exact() -> None:
    sp = make_search(option_select=True)
    assert sp._is_match("ffmpeg", "ffmpeg") is True


# ---------------------------------------------------------------------------
# _get_installed_status()
# ---------------------------------------------------------------------------

_PKG_DATA = {"version": "7.1", "package": "ffmpeg-7.1-x86_64-1.txz"}


def test_get_installed_status_not_installed_returns_empty() -> None:
    sp = make_search(repository="sbo", data={"ffmpeg": _PKG_DATA})
    sp.utils.is_package_installed.return_value = ""
    assert sp._get_installed_status("ffmpeg", _PKG_DATA) == ""


def test_get_installed_status_single_repo_exact_match() -> None:
    sp = make_search(repository="sbo", data={"ffmpeg": _PKG_DATA})
    sp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    assert sp._get_installed_status("ffmpeg", _PKG_DATA) != ""


def test_get_installed_status_single_repo_version_mismatch_returns_empty() -> None:
    sp = make_search(repository="sbo", data={"ffmpeg": _PKG_DATA})
    sp.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1"
    assert sp._get_installed_status("ffmpeg", _PKG_DATA) == ""


def test_get_installed_status_all_repos_exact_match() -> None:
    sp = make_search(repository="*")
    sp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    assert sp._get_installed_status("ffmpeg", _PKG_DATA) != ""


def test_get_installed_status_all_repos_version_mismatch_returns_empty() -> None:
    sp = make_search(repository="*")
    sp.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1"
    assert sp._get_installed_status("ffmpeg", _PKG_DATA) == ""


# ---------------------------------------------------------------------------
# is_not_case_sensitive()
# ---------------------------------------------------------------------------


def test_case_insensitive_exact_same_string_matches() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("ffmpeg", "ffmpeg") is True


def test_case_insensitive_uppercase_query_matches() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("FFMPEG", "ffmpeg") is True


def test_case_insensitive_mixed_case_query_matches() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("FfMpEg", "ffmpeg") is True


def test_case_insensitive_partial_match_returns_true() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("MPEG", "ffmpeg") is True


def test_case_insensitive_different_name_no_match() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("vlc", "ffmpeg") is False


def test_case_insensitive_completely_different_names() -> None:
    sp = make_search(option_no_case=True)
    assert sp.is_not_case_sensitive("BASH", "ffmpeg") is False


def test_case_sensitive_same_string_returns_false() -> None:
    sp = make_search(option_no_case=False)
    assert sp.is_not_case_sensitive("ffmpeg", "ffmpeg") is False


def test_case_sensitive_uppercase_query_returns_false() -> None:
    sp = make_search(option_no_case=False)
    assert sp.is_not_case_sensitive("FFMPEG", "ffmpeg") is False


def test_case_sensitive_different_names_returns_false() -> None:
    sp = make_search(option_no_case=False)
    assert sp.is_not_case_sensitive("vlc", "ffmpeg") is False


def test_return_type_is_bool_when_match() -> None:
    sp = make_search(option_no_case=True)
    assert isinstance(sp.is_not_case_sensitive("FFMPEG", "ffmpeg"), bool)


def test_return_type_is_bool_when_no_match() -> None:
    sp = make_search(option_no_case=True)
    assert isinstance(sp.is_not_case_sensitive("vlc", "ffmpeg"), bool)


# ---------------------------------------------------------------------------
# search_for_the_packages()
# ---------------------------------------------------------------------------

_REPO_DATA = {
    "ffmpeg": {"version": "7.1", "package": "ffmpeg-7.1-x86_64-1.txz"},
    "vlc": {"version": "3.0.21", "package": "vlc-3.0.21-x86_64-1.txz"},
    "libvpx": {"version": "1.13", "package": "libvpx-1.13-x86_64-1.txz"},
}


def test_search_for_packages_exact_match() -> None:
    """Exact name match increments matching and populates data_dict."""
    sp = make_search(packages=["ffmpeg"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 1
    assert sp.data_dict[1]["name"] == "ffmpeg"


def test_search_for_packages_wildcard_matches_all() -> None:
    """'*' as query matches every package in the repo."""
    sp = make_search(packages=["*"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == len(_REPO_DATA)


def test_search_for_packages_substring_match() -> None:
    """Query as substring of name is matched."""
    sp = make_search(packages=["vpx"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 1
    assert sp.data_dict[1]["name"] == "libvpx"


def test_search_for_packages_no_match() -> None:
    """Query that doesn't match anything leaves matching at 0."""
    sp = make_search(packages=["nonexistent"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 0
    assert sp.data_dict == {}


def test_search_for_packages_case_insensitive_match() -> None:
    """Uppercase query matches lowercase package name when option_no_case=True."""
    sp = make_search(packages=["FFMPEG"], repository="sbo", option_no_case=True)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 1


def test_search_for_packages_case_sensitive_no_match() -> None:
    """Uppercase query does NOT match when option_no_case=False."""
    sp = make_search(packages=["FFMPEG"], repository="sbo", option_no_case=False)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 0


def test_search_for_packages_installed_status_single_repo() -> None:
    """Installed package is flagged with installed_status in single repo mode."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["installed"] != ""


def test_search_for_packages_not_installed_status_empty() -> None:
    """Not-installed package has empty installed_status."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["installed"] == ""


def test_search_for_packages_installed_all_repos() -> None:
    """Installed status detected correctly in all-repos (*) mode."""
    all_data = {"sbo": _REPO_DATA}
    sp = make_search(packages=["ffmpeg"], repository="*", data=all_data)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = "ffmpeg-7.1-x86_64-1"
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["installed"] != ""


def test_search_for_packages_installed_version_mismatch() -> None:
    """Installed but different version → installed_status is empty."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = "ffmpeg-6.0-x86_64-1"
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["installed"] == ""


def test_search_for_packages_option_select_disables_substring() -> None:
    """With option_select=True, substring match is disabled (only exact)."""
    sp = make_search(packages=["vpx"], repository="sbo", option_select=True)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 0


def test_search_for_packages_option_dialog_disables_substring() -> None:
    """With option_dialog=True, substring match is disabled."""
    sp = make_search(packages=["vpx"], repository="sbo", option_dialog=True)
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 0


def test_search_for_packages_data_dict_has_repo_key() -> None:
    """data_dict entries contain the correct repository name."""
    sp = make_search(packages=["ffmpeg"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["repository"] == "sbo"


def test_search_for_packages_data_dict_has_version() -> None:
    """data_dict entries contain the correct version."""
    sp = make_search(packages=["ffmpeg"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.data_dict[1]["version"] == "7.1"


def test_search_for_packages_multiple_queries() -> None:
    """Multiple query terms each match their respective packages."""
    sp = make_search(packages=["ffmpeg", "vlc"], repository="sbo")
    sp.repo_data = _REPO_DATA
    sp.utils.is_package_installed.return_value = ""
    sp.search_for_the_packages("sbo")
    assert sp.matching == 2


# ---------------------------------------------------------------------------
# search_to_all_repositories()
# ---------------------------------------------------------------------------


def test_search_to_all_repositories_iterates_all_repos() -> None:
    """search_for_the_packages is called once per repo in all_data."""
    all_data = {
        "sbo": _REPO_DATA,
        "alien": {"handbrake": {"version": "1.7", "package": "handbrake-1.7-x86_64-1.txz"}},
    }
    sp = make_search(packages=["ffmpeg"], repository="*", data=all_data)
    sp.utils.is_package_installed.return_value = ""
    sp.search_to_all_repositories()
    # ffmpeg found in sbo only
    assert sp.matching == 1


def test_search_to_all_repositories_finds_across_repos() -> None:
    """Packages found in different repos are all counted."""
    all_data = {
        "sbo": {"ffmpeg": {"version": "7.1", "package": "ffmpeg-7.1-x86_64-1.txz"}},
        "alien": {"ffmpeg": {"version": "7.0", "package": "ffmpeg-7.0-x86_64-1.txz"}},
    }
    sp = make_search(packages=["ffmpeg"], repository="*", data=all_data)
    sp.utils.is_package_installed.return_value = ""
    sp.search_to_all_repositories()
    assert sp.matching == 2


def test_search_to_all_repositories_empty_data() -> None:
    """Empty data produces no matches."""
    sp = make_search(packages=["ffmpeg"], repository="*", data={})
    sp.search_to_all_repositories()
    assert sp.matching == 0


# ---------------------------------------------------------------------------
# search()
# ---------------------------------------------------------------------------


def test_search_specific_repo_calls_search_for_packages() -> None:
    """With a specific repo, search_for_the_packages is called."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA)
    sp.utils.is_package_installed.return_value = ""
    sp.search()
    assert sp.matching >= 0  # completed without error


def test_search_all_repos_calls_search_to_all_repositories() -> None:
    """With repository='*', search_to_all_repositories is invoked."""
    all_data = {"sbo": _REPO_DATA}
    sp = make_search(packages=["ffmpeg"], repository="*", data=all_data)
    sp.utils.is_package_installed.return_value = ""
    sp.search()
    assert sp.matching == 1


def test_search_quiet_skips_view_process(capsys) -> None:
    """With option_quiet=True, view_process.message and done are not called."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA,
                     option_quiet=True)
    sp.utils.is_package_installed.return_value = ""
    sp.search()
    sp.view_process.message.assert_not_called()
    sp.view_process.done.assert_not_called()


def test_search_not_quiet_calls_view_process() -> None:
    """Without quiet mode, view_process.message and done are called."""
    sp = make_search(packages=["ffmpeg"], repository="sbo", data=_REPO_DATA,
                     option_quiet=False)
    sp.utils.is_package_installed.return_value = ""
    sp.search()
    sp.view_process.message.assert_called_once()
    sp.view_process.done.assert_called_once()


def test_search_always_calls_summary(capsys) -> None:
    """summary_of_searching is always called regardless of options."""
    sp = make_search(packages=["nonexistent"], repository="sbo", data=_REPO_DATA)
    sp.utils.is_package_installed.return_value = ""
    sp.search()
    out = capsys.readouterr().out
    # "Does not match" or "Found" always appears
    assert "match" in out.lower() or "Found" in out


# ---------------------------------------------------------------------------
# summary_of_searching() — all branches
# ---------------------------------------------------------------------------


def _sp_with_results(
    installed: bool = False,
    repository: str = "*",
    option_json: bool = False,
    option_quiet: bool = False,
    option_pkg_version: bool = False,
    option_pkg_description: bool = False,
) -> SearchPackage:
    data = {
        "ffmpeg": {"version": "7.1", "package": "ffmpeg-7.1-x86_64-1.txz", "description": "FFmpeg multimedia library"},
        "vlc": {"version": "3.0.21", "package": "vlc-3.0.21-x86_64-1.txz", "description": "VLC media player"},
    }
    sp = make_search(
        repository=repository,
        data=data,
        option_json=option_json,
        option_quiet=option_quiet,
        option_pkg_version=option_pkg_version,
        option_pkg_description=option_pkg_description,
    )
    sp.matching = 2
    sp.data_dict = {
        1: {"repository": "sbo", "name": "ffmpeg", "version": "7.1", "installed": "[installed]" if installed else ""},
        2: {"repository": "alien", "name": "vlc", "version": "3.0.21", "installed": ""},
    }
    return sp


def test_json_output_is_valid_json(capsys) -> None:
    sp = _sp_with_results(option_json=True)
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    assert isinstance(data, list)


def test_json_output_contains_expected_keys(capsys) -> None:
    sp = _sp_with_results(option_json=True)
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    for item in data:
        assert {"repository", "name", "version", "installed"} <= item.keys()


def test_json_output_entry_count_matches(capsys) -> None:
    sp = _sp_with_results(option_json=True)
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    assert len(data) == 2


def test_json_installed_field_is_bool(capsys) -> None:
    sp = _sp_with_results(installed=True, option_json=True)
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    assert data[0]["installed"] is True
    assert data[1]["installed"] is False


def test_json_no_matches_outputs_empty_array(capsys) -> None:
    sp = make_search(option_json=True)
    sp.matching = 0
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    assert data == []


def test_json_values_match_data_dict(capsys) -> None:
    sp = _sp_with_results(option_json=True)
    sp.summary_of_searching()
    data = json.loads(capsys.readouterr().out)
    names = {item["name"] for item in data}
    assert names == {"ffmpeg", "vlc"}


def test_summary_no_match_prints_message(capsys) -> None:
    """Zero matches prints 'Does not match any package.'"""
    sp = make_search()
    sp.matching = 0
    sp.summary_of_searching()
    assert "Does not match any package." in capsys.readouterr().out


def test_summary_no_match_quiet_prints_nothing(capsys) -> None:
    """Zero matches + quiet → no output."""
    sp = make_search(option_quiet=True)
    sp.matching = 0
    sp.summary_of_searching()
    assert capsys.readouterr().out == ""


def test_summary_quiet_prints_names_only(capsys) -> None:
    """Quiet mode prints only package names, one per line."""
    sp = _sp_with_results(option_quiet=True)
    sp.summary_of_searching()
    out = capsys.readouterr().out
    lines = [l for l in out.splitlines() if l.strip()]
    assert "ffmpeg" in lines
    assert "vlc" in lines
    assert "Found" not in out


def test_summary_table_prints_found_line(capsys) -> None:
    """Normal table output ends with 'Found N packages.'"""
    sp = _sp_with_results()
    sp.summary_of_searching()
    assert "Found 2 packages." in capsys.readouterr().out


def test_summary_table_includes_package_names(capsys) -> None:
    """Package names appear in table output."""
    sp = _sp_with_results()
    sp.summary_of_searching()
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "vlc" in out


def test_summary_table_with_version(capsys) -> None:
    """With option_pkg_version, version numbers appear in output."""
    sp = _sp_with_results(option_pkg_version=True)
    sp.summary_of_searching()
    out = capsys.readouterr().out
    assert "7.1" in out
    assert "3.0.21" in out


def test_summary_table_without_version(capsys) -> None:
    """Without option_pkg_version, versions do NOT appear in output."""
    sp = _sp_with_results(option_pkg_version=False)
    sp.summary_of_searching()
    out = capsys.readouterr().out
    assert "7.1" not in out


def test_summary_table_all_repos_shows_repo_column(capsys) -> None:
    """With repository='*', repo name appears in each row."""
    sp = _sp_with_results(repository="*")
    sp.summary_of_searching()
    out = capsys.readouterr().out
    assert "sbo" in out
    assert "alien" in out


def test_summary_table_single_repo_no_repo_column(capsys) -> None:
    """With a specific repo, repo name does NOT appear in output."""
    sp = _sp_with_results(repository="sbo")
    # Override data_dict so both packages appear to be in "sbo"
    sp.data_dict = {
        1: {"repository": "sbo", "name": "ffmpeg", "version": "7.1", "installed": ""},
        2: {"repository": "sbo", "name": "vlc", "version": "3.0.21", "installed": ""},
    }
    sp.summary_of_searching()
    out = capsys.readouterr().out
    # repo column separator should not appear
    assert " : " not in out


def test_summary_table_description_single_repo(capsys) -> None:
    """With option_pkg_description in single-repo mode, description is printed."""
    sp = _sp_with_results(repository="sbo", option_pkg_description=True)
    sp.data_dict = {
        1: {"repository": "sbo", "name": "ffmpeg", "version": "7.1", "installed": ""},
    }
    sp.matching = 1
    sp.summary_of_searching()
    out = capsys.readouterr().out
    assert "FFmpeg" in out


def test_summary_table_description_skipped_for_all_repos(capsys) -> None:
    """With option_pkg_description and repository='*', description is NOT shown."""
    sp = _sp_with_results(repository="*", option_pkg_description=True)
    sp.summary_of_searching()
    out = capsys.readouterr().out
    # Description line starts with two spaces; should not appear
    assert "FFmpeg multimedia" not in out
