#!/usr/bin/python3
"""Tests for slpkg.file_search.FileSearch."""

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

from slpkg.file_search import FileSearch


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


def make_file_search(
    *,
    search_terms: list[str] | None = None,
    repository: str = "",
    no_case: bool = False,
    quiet: bool = False,
    local: bool = False,
    option_repository: bool = False,
    log_packages: Path | None = None,
) -> FileSearch:
    """Construct FileSearch bypassing __init__ (avoids config_load/Repositories)."""
    obj: FileSearch = object.__new__(FileSearch)
    obj.search_terms = search_terms or ["ffmpeg"]
    obj.repository = repository
    obj.grey = ""
    obj.green = ""
    obj.red = ""
    obj.yellow = ""
    obj.endc = ""
    obj.log_packages = log_packages or Path("/nonexistent")
    obj.matching_count = 0
    obj.results = {}
    obj.option_for_no_case = no_case
    obj.option_quiet = quiet
    obj.option_local = local
    obj.option_repository = option_repository

    obj.utils = MagicMock()
    obj.utils.split_package.side_effect = lambda name: {"name": name.rsplit("-", 3)[0]}
    obj.utils.is_package_installed.return_value = ""

    obj.repos = MagicMock()
    obj.view_process = MagicMock()
    return obj


def write_gzip_index(repo_path: Path, data: dict[str, list[str]]) -> None:
    """Write a gzip-compressed JSON index to repo_path/filelist.json.gz."""
    repo_path.mkdir(parents=True, exist_ok=True)
    gz_file = repo_path / "filelist.json.gz"
    with gzip.open(gz_file, "wt", encoding="utf-8") as f:
        json.dump(data, f)


def write_pkg_log(log_dir: Path, pkg_name: str, content: str) -> None:
    log_dir.mkdir(parents=True, exist_ok=True)
    (log_dir / pkg_name).write_text(content, encoding="utf-8")


# ---------------------------------------------------------------------------
# search() — repository index
# ---------------------------------------------------------------------------


def test_search_finds_term_in_index(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg", "/usr/lib64/libav.so"]})
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert "sbo" in obj.results
    assert "ffmpeg-6.1-x86_64-1_SBo" in obj.results["sbo"]
    assert "/usr/bin/ffmpeg" in obj.results["sbo"]["ffmpeg-6.1-x86_64-1_SBo"]


def test_search_no_match_returns_empty_results(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {"vlc-3.0.20-x86_64-1_SBo": ["/usr/bin/vlc"]})
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert obj.results == {}


def test_search_skips_missing_index(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    repo_path.mkdir()
    # No filelist.json.gz created
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert obj.results == {}


def test_search_case_sensitive_no_match(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]})
    obj = make_file_search(search_terms=["FFMPEG"], no_case=False)
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert obj.results == {}


def test_search_case_insensitive_finds_match(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]})
    obj = make_file_search(search_terms=["FFMPEG"], no_case=True)
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert "sbo" in obj.results


def test_search_option_repository_filters_other_repos(tmp_path: Path) -> None:
    sbo_path = tmp_path / "sbo"
    alien_path = tmp_path / "alien"
    write_gzip_index(sbo_path, {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]})
    write_gzip_index(alien_path, {"ffmpeg-alien": ["/usr/bin/ffmpeg"]})
    obj = make_file_search(search_terms=["ffmpeg"], repository="sbo", option_repository=True)
    obj.repos.repositories = {
        "sbo": {"path": sbo_path, "enable": True},
        "alien": {"path": alien_path, "enable": True},
    }
    obj.search()
    assert "sbo" in obj.results
    assert "alien" not in obj.results


def test_search_multiple_terms_collects_all_matches(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {
        "ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg", "/usr/share/ffmpeg/"],
        "vlc-3.0.20-x86_64-1_SBo": ["/usr/bin/vlc"],
    })
    obj = make_file_search(search_terms=["ffmpeg", "vlc"])
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert "ffmpeg-6.1-x86_64-1_SBo" in obj.results["sbo"]
    assert "vlc-3.0.20-x86_64-1_SBo" in obj.results["sbo"]


def test_search_updates_matching_count(tmp_path: Path) -> None:
    repo_path = tmp_path / "sbo"
    write_gzip_index(repo_path, {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg", "/usr/lib64/ffmpeg.so"]})
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.repos.repositories = {"sbo": {"path": repo_path, "enable": True}}
    obj.search()
    assert obj.matching_count == 2


# ---------------------------------------------------------------------------
# search_local() — installed packages
# ---------------------------------------------------------------------------

LOG_CONTENT = """\
PACKAGE NAME:  ffmpeg-6.1-x86_64-1_SBo
FILE LIST:
./
install/slack-desc
./usr/bin/ffmpeg
./usr/lib64/libavcodec.so
"""


def test_search_local_finds_term_in_log(tmp_path: Path) -> None:
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["ffmpeg"], log_packages=tmp_path)
    obj.search_local()
    assert "Installed (Local)" in obj.results
    assert "ffmpeg-6.1-x86_64-1_SBo" in obj.results["Installed (Local)"]


def test_search_local_no_file_list_section_skips_package(tmp_path: Path) -> None:
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", "PACKAGE NAME: ffmpeg\nno file list here\n")
    obj = make_file_search(search_terms=["ffmpeg"], log_packages=tmp_path)
    obj.search_local()
    assert obj.results == {}


def test_search_local_case_sensitive_no_match(tmp_path: Path) -> None:
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["FFMPEG"], no_case=False, log_packages=tmp_path)
    obj.search_local()
    assert obj.results == {}


def test_search_local_case_insensitive_finds_match(tmp_path: Path) -> None:
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["FFMPEG"], no_case=True, log_packages=tmp_path)
    obj.search_local()
    assert "Installed (Local)" in obj.results


def test_search_local_nonexistent_dir_returns_empty(tmp_path: Path) -> None:
    obj = make_file_search(search_terms=["ffmpeg"], log_packages=tmp_path / "nonexistent")
    obj.search_local()
    assert obj.results == {}


# ---------------------------------------------------------------------------
# display_results()
# ---------------------------------------------------------------------------


def test_display_results_no_results_prints_message(capsys) -> None:
    obj = make_file_search()
    obj.results = {}
    obj.display_results()
    out = capsys.readouterr().out
    assert "Does not match any file" in out


def test_display_results_shows_repo_and_package(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "Repository: sbo" in out
    assert "ffmpeg-6.1-x86_64-1_SBo" in out


def test_display_results_normal_mode_shows_file_paths(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"], quiet=False)
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "/usr/bin/ffmpeg" in out


def test_display_results_quiet_mode_hides_file_paths(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"], quiet=True)
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "ffmpeg-6.1-x86_64-1_SBo" in out
    assert "/usr/bin/ffmpeg" not in out


def test_display_results_shows_installed_status(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.utils.is_package_installed.return_value = "ffmpeg-6.1-x86_64-1_SBo"
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "installed" in out


def test_display_results_shows_uninstalled_status(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.utils.is_package_installed.return_value = ""
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "uninstalled" in out


def test_display_results_shows_total_count(capsys) -> None:
    obj = make_file_search(search_terms=["ffmpeg"])
    obj.matching_count = 7
    obj.results = {"sbo": {"ffmpeg-6.1-x86_64-1_SBo": ["/usr/bin/ffmpeg"]}}
    obj.display_results()
    out = capsys.readouterr().out
    assert "7 matches" in out


# ---------------------------------------------------------------------------
# run() — dispatch
# ---------------------------------------------------------------------------


def test_run_calls_search_when_not_local(tmp_path: Path) -> None:
    obj = make_file_search(local=False)
    obj.repos.repositories = {}
    obj.run()
    # No local search was triggered — results are empty (no index files)
    assert obj.results == {}


def test_run_calls_search_local_when_local(tmp_path: Path) -> None:
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["ffmpeg"], local=True, log_packages=tmp_path)
    obj.run()
    assert "Installed (Local)" in obj.results


def test_run_calls_view_process_message_and_done() -> None:
    obj = make_file_search(local=False)
    obj.repos.repositories = {}
    obj.run()
    obj.view_process.message.assert_called_once()
    obj.view_process.done.assert_called_once()


# ---------------------------------------------------------------------------
# search_local() — exact path matching (like rpm -qf / dpkg -S)
# ---------------------------------------------------------------------------


def test_search_local_exact_path_finds_match(tmp_path: Path) -> None:
    """Absolute search term '/usr/bin/ffmpeg' matches './usr/bin/ffmpeg' entry."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["/usr/bin/ffmpeg"], log_packages=tmp_path)
    obj.search_local()
    assert "Installed (Local)" in obj.results
    assert "ffmpeg-6.1-x86_64-1_SBo" in obj.results["Installed (Local)"]


def test_search_local_exact_path_no_match(tmp_path: Path) -> None:
    """Absolute search term for a non-existent path returns no results."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["/usr/bin/nonexistent"], log_packages=tmp_path)
    obj.search_local()
    assert obj.results == {}


def test_search_local_exact_path_no_partial_match(tmp_path: Path) -> None:
    """Absolute search term must match the full path, not a substring."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    # '/usr/bin' is a prefix of './usr/bin/ffmpeg' but NOT an exact match
    obj = make_file_search(search_terms=["/usr/bin"], log_packages=tmp_path)
    obj.search_local()
    assert obj.results == {}


def test_search_local_exact_path_case_insensitive(tmp_path: Path) -> None:
    """Absolute path with --no-case matches regardless of case."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["/USR/BIN/FFMPEG"], no_case=True, log_packages=tmp_path)
    obj.search_local()
    assert "Installed (Local)" in obj.results


def test_search_local_exact_path_case_sensitive_no_match(tmp_path: Path) -> None:
    """Absolute path without --no-case does NOT match on different case."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["/USR/BIN/FFMPEG"], no_case=False, log_packages=tmp_path)
    obj.search_local()
    assert obj.results == {}


def test_search_local_substring_still_works(tmp_path: Path) -> None:
    """Non-absolute terms still do substring matching (regression guard)."""
    write_pkg_log(tmp_path, "ffmpeg-6.1-x86_64-1_SBo", LOG_CONTENT)
    obj = make_file_search(search_terms=["libav"], log_packages=tmp_path)
    obj.search_local()
    assert "Installed (Local)" in obj.results
    assert "./usr/lib64/libavcodec.so" in obj.results["Installed (Local)"]["ffmpeg-6.1-x86_64-1_SBo"]
