#!/usr/bin/python3
"""Tests for slpkg.why.Why._read_deps_log() and the find logic."""

import json
from pathlib import Path
from unittest.mock import MagicMock

from slpkg.why import Why


def make_why(packages: list[str], deps_log_file: Path, installed: dict[str, str]) -> Why:
    """Construct Why bypassing __init__ (avoids config_load/Utilities)."""
    obj: Why = object.__new__(Why)
    obj.packages = packages
    obj.deps_log_file = deps_log_file
    obj.utils = MagicMock()
    obj.utils.all_installed.return_value = installed
    obj.bold = ""
    obj.green = ""
    obj.grey = ""
    obj.cyan = ""
    obj.yellow = ""
    obj.endc = ""
    obj.quiet = False
    obj.transitive = False
    obj.unicode = True
    obj.option_for_pkg_version = False
    return obj


def needed_by(obj: Why, package: str) -> list[str]:
    """Helper: return the sorted list of installed parents that need package."""
    deps_log = obj._read_deps_log()
    installed = set(obj.utils.all_installed().keys())
    return sorted(key for key, deps in deps_log.items() if package in deps and key in installed)


# ---------------------------------------------------------------------------
# _read_deps_log: missing / invalid files
# ---------------------------------------------------------------------------


def test_read_deps_log_missing_file_returns_empty(tmp_path: Path) -> None:
    f = tmp_path / "nonexistent.log"
    obj = make_why([], f, {})
    assert obj._read_deps_log() == {}


def test_read_deps_log_invalid_json_returns_empty(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    f.write_text("not valid json", encoding="utf-8")
    obj = make_why([], f, {})
    assert obj._read_deps_log() == {}


def test_read_deps_log_non_dict_json_returns_empty(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    f.write_text(json.dumps(["a", "b"]), encoding="utf-8")
    obj = make_why([], f, {})
    assert obj._read_deps_log() == {}


def test_read_deps_log_valid_file_returns_data(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    data = {"polychromatic": ["openrazer-daemon", "openrazer-kernel"]}
    f.write_text(json.dumps(data), encoding="utf-8")
    obj = make_why([], f, {})
    assert obj._read_deps_log() == data


# ---------------------------------------------------------------------------
# Find logic: package found in deps
# ---------------------------------------------------------------------------


def test_package_needed_by_one_parent(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon", "openrazer-kernel"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "openrazer-kernel": "openrazer-kernel-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    assert needed_by(obj, "openrazer-daemon") == ["polychromatic"]


def test_package_needed_by_multiple_parents(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {
        "polychromatic": ["python3", "openrazer-daemon"],
        "yt-dlp": ["python3"],
    }
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "yt-dlp": "yt-dlp-2024-x86_64-1",
        "python3": "python3-3.11-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["python3"], f, installed)
    assert needed_by(obj, "python3") == ["polychromatic", "yt-dlp"]


# ---------------------------------------------------------------------------
# Find logic: package not in any deps
# ---------------------------------------------------------------------------


def test_package_not_in_any_dep_returns_empty(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "bash": "bash-5.2-x86_64-1",
    }
    obj = make_why(["bash"], f, installed)
    assert needed_by(obj, "bash") == []


# ---------------------------------------------------------------------------
# Find logic: parent listed in deps.log but no longer installed
# ---------------------------------------------------------------------------


def test_removed_parent_not_counted(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    # polychromatic removed
    installed = {"openrazer-daemon": "openrazer-daemon-1.3-x86_64-1"}
    obj = make_why(["openrazer-daemon"], f, installed)
    assert needed_by(obj, "openrazer-daemon") == []


# ---------------------------------------------------------------------------
# Missing deps.log → graceful empty result
# ---------------------------------------------------------------------------


def test_missing_deps_log_returns_empty_needed_by(tmp_path: Path) -> None:
    f = tmp_path / "nonexistent.log"
    installed = {"python3": "python3-3.11-x86_64-1"}
    obj = make_why(["python3"], f, installed)
    assert needed_by(obj, "python3") == []


# ---------------------------------------------------------------------------
# Multiple packages queried at once
# ---------------------------------------------------------------------------


def test_multiple_packages_each_resolved_independently(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {
        "polychromatic": ["openrazer-daemon", "openrazer-kernel"],
        "yt-dlp": ["python3"],
    }
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "yt-dlp": "yt-dlp-2024-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "openrazer-kernel": "openrazer-kernel-1.3-x86_64-1",
        "python3": "python3-3.11-x86_64-1",
    }
    obj = make_why(["openrazer-kernel", "python3"], f, installed)
    assert needed_by(obj, "openrazer-kernel") == ["polychromatic"]
    assert needed_by(obj, "python3") == ["yt-dlp"]
    assert needed_by(obj, "bash") == []


# ---------------------------------------------------------------------------
# _find_transitive
# ---------------------------------------------------------------------------


def make_why_plain(deps_log_data: dict, installed: dict[str, str]) -> Why:
    """Minimal Why instance for testing _find_transitive directly."""
    from pathlib import Path
    from unittest.mock import MagicMock

    obj: Why = object.__new__(Why)
    obj.packages = []
    obj.deps_log_file = Path("/nonexistent")
    obj.utils = MagicMock()
    obj.utils.all_installed.return_value = installed
    obj.bold = obj.green = obj.grey = obj.cyan = obj.endc = ""
    obj.quiet = False
    obj.transitive = True
    obj.unicode = True
    return obj


def ft(obj: Why, package: str, deps_log: dict, installed: dict) -> list[tuple[int, str]]:
    """Shorthand: call _find_transitive with a set of installed names."""
    return obj._find_transitive(package, deps_log, set(installed.keys()))


def test_transitive_no_parents_returns_empty() -> None:
    deps_log = {"app": ["libfoo"]}
    installed = {"libfoo": "libfoo-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    # libfoo has app as direct parent; bash has no parents
    assert ft(obj, "bash", deps_log, installed) == []


def test_transitive_single_direct_parent() -> None:
    deps_log = {"app": ["libfoo"]}
    installed = {"app": "app-1.0-x86_64-1", "libfoo": "libfoo-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    assert ft(obj, "libfoo", deps_log, installed) == [(0, "app")]


def test_transitive_two_level_chain() -> None:
    # top-app → middle → libfoo
    deps_log = {
        "top-app": ["middle"],
        "middle": ["libfoo"],
    }
    installed = {
        "top-app": "top-app-1.0-x86_64-1",
        "middle": "middle-1.0-x86_64-1",
        "libfoo": "libfoo-1.0-x86_64-1",
    }
    obj = make_why_plain(deps_log, installed)
    result = ft(obj, "libfoo", deps_log, installed)
    # middle is at depth 0, top-app is at depth 1
    assert result == [(0, "middle"), (1, "top-app")]


def test_transitive_multiple_direct_parents_sorted() -> None:
    deps_log = {
        "zebra": ["libfoo"],
        "alpha": ["libfoo"],
    }
    installed = {
        "zebra": "zebra-1.0-x86_64-1",
        "alpha": "alpha-1.0-x86_64-1",
        "libfoo": "libfoo-1.0-x86_64-1",
    }
    obj = make_why_plain(deps_log, installed)
    result = ft(obj, "libfoo", deps_log, installed)
    assert result == [(0, "alpha"), (0, "zebra")]


def test_transitive_diamond_dependency() -> None:
    # Both ffmpeg and libwebp need libfoo; polychromatic needs both
    deps_log = {
        "polychromatic": ["ffmpeg", "libwebp"],
        "ffmpeg": ["libfoo"],
        "libwebp": ["libfoo"],
    }
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "ffmpeg": "ffmpeg-1.0-x86_64-1",
        "libwebp": "libwebp-1.0-x86_64-1",
        "libfoo": "libfoo-1.0-x86_64-1",
    }
    obj = make_why_plain(deps_log, installed)
    result = ft(obj, "libfoo", deps_log, installed)
    names = [name for _, name in result]
    # Both ffmpeg and libwebp are direct parents
    assert "ffmpeg" in names
    assert "libwebp" in names
    # polychromatic appears as ancestor of both (cycle-safe, no infinite loop)
    assert names.count("polychromatic") >= 1


def test_transitive_cycle_does_not_loop() -> None:
    # Pathological case: a ↔ b cycle
    deps_log = {
        "a": ["b"],
        "b": ["a"],
    }
    installed = {"a": "a-1.0-x86_64-1", "b": "b-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    # Should terminate without RecursionError
    result = ft(obj, "a", deps_log, installed)
    assert isinstance(result, list)


def test_transitive_uninstalled_parent_excluded() -> None:
    deps_log = {
        "top-app": ["middle"],
        "middle": ["libfoo"],
    }
    # top-app not installed
    installed = {"middle": "middle-1.0-x86_64-1", "libfoo": "libfoo-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    result = ft(obj, "libfoo", deps_log, installed)
    # middle is a direct parent; top-app is excluded (not installed)
    assert result == [(0, "middle")]


# ---------------------------------------------------------------------------
# _build_tree
# ---------------------------------------------------------------------------


def bt(obj: Why, package: str, deps_log: dict, installed: dict) -> dict:
    """Shorthand: call _build_tree with a set of installed names."""
    return obj._build_tree(package, deps_log, set(installed.keys()))


def test_build_tree_no_parents_returns_empty_dict() -> None:
    deps_log = {"app": ["libfoo"]}
    installed = {"libfoo": "libfoo-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    assert bt(obj, "bash", deps_log, installed) == {}


def test_build_tree_single_direct_parent() -> None:
    deps_log = {"app": ["libfoo"]}
    installed = {"app": "app-1.0-x86_64-1", "libfoo": "libfoo-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    assert bt(obj, "libfoo", deps_log, installed) == {"app": {}}


def test_build_tree_two_level_chain() -> None:
    deps_log = {"top": ["middle"], "middle": ["libfoo"]}
    installed = {
        "top": "top-1.0-x86_64-1",
        "middle": "middle-1.0-x86_64-1",
        "libfoo": "libfoo-1.0-x86_64-1",
    }
    obj = make_why_plain(deps_log, installed)
    assert bt(obj, "libfoo", deps_log, installed) == {"middle": {"top": {}}}


def test_build_tree_cycle_terminates() -> None:
    deps_log = {"a": ["b"], "b": ["a"]}
    installed = {"a": "a-1.0-x86_64-1", "b": "b-1.0-x86_64-1"}
    obj = make_why_plain(deps_log, installed)
    result = bt(obj, "a", deps_log, installed)
    assert isinstance(result, dict)


# ---------------------------------------------------------------------------
# _render_tree
# ---------------------------------------------------------------------------


def test_render_tree_single_node_unicode() -> None:
    deps_log = {}
    installed: dict = {}
    obj = make_why_plain(deps_log, installed)
    obj.unicode = True
    tree = {"app": {}}
    lines = obj._render_tree(tree, direct_parents={"app"})
    assert lines == ["\u2514\u2500 app"]


def test_render_tree_single_node_ascii() -> None:
    deps_log = {}
    installed: dict = {}
    obj = make_why_plain(deps_log, installed)
    obj.unicode = False
    tree = {"app": {}}
    lines = obj._render_tree(tree, direct_parents=set())
    assert lines == ["\\- app"]


def test_render_tree_also_direct_annotation() -> None:
    deps_log = {}
    installed: dict = {}
    obj = make_why_plain(deps_log, installed)
    obj.unicode = True
    # polychromatic is at depth 1 and also a direct parent
    tree = {"openrazer-daemon": {"polychromatic": {}}, "polychromatic": {}}
    lines = obj._render_tree(tree, direct_parents={"openrazer-daemon", "polychromatic"})
    # The annotation should appear on the depth-1 polychromatic line
    annotated = [ln for ln in lines if "polychromatic" in ln and "also direct" in ln]
    assert len(annotated) == 1


def test_render_tree_two_siblings_use_branch_and_last() -> None:
    deps_log = {}
    installed: dict = {}
    obj = make_why_plain(deps_log, installed)
    obj.unicode = True
    tree = {"alpha": {}, "zebra": {}}
    lines = obj._render_tree(tree, direct_parents=set())
    assert lines[0].startswith("├─ ")
    assert lines[1].startswith("└─ ")


# ---------------------------------------------------------------------------
# _collect_transitive_names
# ---------------------------------------------------------------------------


def test_collect_transitive_names_empty_tree() -> None:
    assert Why._collect_transitive_names({}) == set()


def test_collect_transitive_names_flat_tree() -> None:
    # All nodes at depth 0 — nothing transitive
    tree = {"a": {}, "b": {}}
    assert Why._collect_transitive_names(tree) == set()


def test_collect_transitive_names_two_level() -> None:
    # middle at depth 0, top at depth 1
    tree = {"middle": {"top": {}}}
    assert Why._collect_transitive_names(tree) == {"top"}


def test_collect_transitive_names_diamond() -> None:
    tree = {
        "spice": {"spice-gtk": {}, "spice-vdagent": {}},
        "spice-gtk": {},
        "spice-vdagent": {},
    }
    assert Why._collect_transitive_names(tree) == {"spice-gtk", "spice-vdagent"}


# ---------------------------------------------------------------------------
# _pkg_version
# ---------------------------------------------------------------------------


def test_pkg_version_returns_version_when_installed() -> None:
    obj = make_why_plain({}, {})
    obj.utils.split_package.return_value = {"version": "3.11.0"}
    result = obj._pkg_version("python3", {"python3": "python3-3.11.0-x86_64-1"})
    assert result == "3.11.0"


def test_pkg_version_returns_empty_when_not_in_installed() -> None:
    obj = make_why_plain({}, {})
    result = obj._pkg_version("python3", {})
    assert result == ""


def test_pkg_version_returns_empty_when_split_has_no_version_key() -> None:
    obj = make_why_plain({}, {})
    obj.utils.split_package.return_value = {}
    result = obj._pkg_version("python3", {"python3": "python3-3.11.0-x86_64-1"})
    assert result == ""


# ---------------------------------------------------------------------------
# view() — quiet mode
# ---------------------------------------------------------------------------


def test_view_quiet_non_transitive_prints_parent(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = True
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    assert "polychromatic" in capsys.readouterr().out


def test_view_quiet_transitive_prints_chain(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"top-app": ["middle"], "middle": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "top-app": "top-app-1.0-x86_64-1",
        "middle": "middle-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = True
    obj.transitive = True
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert "middle" in out


def test_view_quiet_no_parents_prints_nothing(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "bash": "bash-5.2-x86_64-1",
    }
    obj = make_why(["bash"], f, installed)
    obj.quiet = True
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    assert capsys.readouterr().out == ""


def test_view_quiet_transitive_deduplicates_output(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    # Two paths both lead to "middle"
    deps_log = {"top-app": ["middle"], "other-app": ["middle"], "middle": ["libfoo"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "top-app": "top-app-1.0-x86_64-1",
        "other-app": "other-app-1.0-x86_64-1",
        "middle": "middle-1.0-x86_64-1",
        "libfoo": "libfoo-1.0-x86_64-1",
    }
    obj = make_why(["libfoo"], f, installed)
    obj.quiet = True
    obj.transitive = True
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    # "middle" should appear exactly once (dedup via 'seen' set)
    assert out.count("middle") == 1


# ---------------------------------------------------------------------------
# view() — non-quiet (verbose) mode
# ---------------------------------------------------------------------------


def test_view_non_quiet_prints_header(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    f.write_text("{}", encoding="utf-8")
    installed = {"bash": "bash-5.2-x86_64-1"}
    obj = make_why(["bash"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    assert "Dependency lookup:" in capsys.readouterr().out


def test_view_non_quiet_transitive_header(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    f.write_text("{}", encoding="utf-8")
    installed = {"bash": "bash-5.2-x86_64-1"}
    obj = make_why(["bash"], f, installed)
    obj.quiet = False
    obj.transitive = True
    obj.option_for_pkg_version = False
    obj.view()
    assert "transitive" in capsys.readouterr().out.lower()


def test_view_empty_deps_log_prints_not_tracked(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    f.write_text("{}", encoding="utf-8")
    installed = {"python3": "python3-3.11-x86_64-1"}
    obj = make_why(["python3"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    assert "Not tracked" in capsys.readouterr().out


def test_view_non_quiet_found_parents_shows_count(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"], "yt-dlp": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "yt-dlp": "yt-dlp-2024-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert "2" in out
    assert "packages" in out.lower()


def test_view_non_quiet_single_parent_uses_singular(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert "package" in out.lower()


def test_view_wildcard_expands_all_installed(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    f.write_text("{}", encoding="utf-8")
    installed = {"pkgA": "pkgA-1.0-x86_64-1", "pkgB": "pkgB-1.0-x86_64-1"}
    obj = make_why(["*"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert "pkgA" in out
    assert "pkgB" in out


def test_view_pkg_version_option_shows_version(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = True
    obj.utils.split_package.return_value = {"version": "1.3"}
    obj.view()
    assert "1.3" in capsys.readouterr().out


def test_view_transitive_non_quiet_shows_tree_and_summary(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why(["openrazer-daemon"], f, installed)
    obj.quiet = False
    obj.transitive = True
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert "polychromatic" in out
    assert "direct" in out.lower()


def test_view_transitive_no_parents_prints_not_tracked(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "bash": "bash-5.2-x86_64-1",
    }
    obj = make_why(["bash"], f, installed)
    obj.quiet = False
    obj.transitive = True
    obj.option_for_pkg_version = False
    obj.view()
    assert "Not tracked" in capsys.readouterr().out


def test_view_prints_deps_log_path_at_end(tmp_path: Path, capsys) -> None:
    f = tmp_path / "deps.log"
    f.write_text("{}", encoding="utf-8")
    installed = {"bash": "bash-5.2-x86_64-1"}
    obj = make_why(["bash"], f, installed)
    obj.quiet = False
    obj.transitive = False
    obj.option_for_pkg_version = False
    obj.view()
    out = capsys.readouterr().out
    assert str(f) in out


# ---------------------------------------------------------------------------
# _resolve_packages
# ---------------------------------------------------------------------------


def test_resolve_packages_expands_wildcard(tmp_path: Path) -> None:
    installed = {"pkgA": "pkgA-1.0-x86_64-1", "pkgB": "pkgB-1.0-x86_64-1"}
    obj = make_why(["*"], tmp_path / "deps.log", installed)
    obj._resolve_packages(set(installed.keys()))
    assert sorted(obj.packages) == ["pkgA", "pkgB"]


def test_resolve_packages_wildcard_with_explicit_package(tmp_path: Path) -> None:
    installed = {"pkgA": "pkgA-1.0-x86_64-1", "pkgB": "pkgB-1.0-x86_64-1"}
    obj = make_why(["explicit", "*"], tmp_path / "deps.log", installed)
    obj._resolve_packages(set(installed.keys()))
    assert "explicit" in obj.packages
    assert "pkgA" in obj.packages
    assert "*" not in obj.packages


def test_resolve_packages_no_wildcard_unchanged(tmp_path: Path) -> None:
    installed = {"pkgA": "pkgA-1.0-x86_64-1"}
    obj = make_why(["bash", "python3"], tmp_path / "deps.log", installed)
    obj._resolve_packages(set(installed.keys()))
    assert obj.packages == ["bash", "python3"]


# ---------------------------------------------------------------------------
# _make_label_fn
# ---------------------------------------------------------------------------


def test_make_label_fn_without_version_returns_name(tmp_path: Path) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj.option_for_pkg_version = False
    label_fn = obj._make_label_fn({})
    assert label_fn("python3") == "python3"


def test_make_label_fn_with_version_appends_version(tmp_path: Path) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj.option_for_pkg_version = True
    obj.utils.split_package.return_value = {"version": "3.11"}
    label_fn = obj._make_label_fn({"python3": "python3-3.11-x86_64-1"})
    result = label_fn("python3")
    assert "python3" in result
    assert "3.11" in result


def test_make_label_fn_with_version_missing_package_returns_name(tmp_path: Path) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj.option_for_pkg_version = True
    label_fn = obj._make_label_fn({})
    assert label_fn("python3") == "python3"


# ---------------------------------------------------------------------------
# _view_package_header
# ---------------------------------------------------------------------------


def test_view_package_header_prints_package_name(tmp_path: Path, capsys) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_header("openrazer-daemon", {})
    assert "openrazer-daemon" in capsys.readouterr().out


def test_view_package_header_with_version(tmp_path: Path, capsys) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj.option_for_pkg_version = True
    obj.utils.split_package.return_value = {"version": "1.3"}
    obj._view_package_header("openrazer-daemon", {"openrazer-daemon": "openrazer-daemon-1.3-x86_64-1"})
    out = capsys.readouterr().out
    assert "openrazer-daemon" in out
    assert "1.3" in out


# ---------------------------------------------------------------------------
# _view_package_flat
# ---------------------------------------------------------------------------


def test_view_package_flat_prints_parents(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"], "yt-dlp": ["openrazer-daemon"]}
    installed = {"polychromatic", "yt-dlp", "openrazer-daemon"}
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_flat("openrazer-daemon", deps_log, installed, lambda n: n)
    out = capsys.readouterr().out
    assert "polychromatic" in out
    assert "yt-dlp" in out


def test_view_package_flat_prints_count(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"], "yt-dlp": ["openrazer-daemon"]}
    installed = {"polychromatic", "yt-dlp", "openrazer-daemon"}
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_flat("openrazer-daemon", deps_log, installed, lambda n: n)
    out = capsys.readouterr().out
    assert "2" in out
    assert "packages" in out


def test_view_package_flat_no_parents_prints_not_tracked(tmp_path: Path, capsys) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_flat("bash", {}, set(), lambda n: n)
    assert "Not tracked" in capsys.readouterr().out


def test_view_package_flat_single_parent_singular_noun(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    installed = {"polychromatic", "openrazer-daemon"}
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_flat("openrazer-daemon", deps_log, installed, lambda n: n)
    out = capsys.readouterr().out
    assert "1 package" in out or "package" in out


# ---------------------------------------------------------------------------
# _view_package_transitive
# ---------------------------------------------------------------------------


def test_view_package_transitive_prints_parent(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    installed = {"polychromatic", "openrazer-daemon"}
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_transitive("openrazer-daemon", deps_log, installed, lambda n: n)
    out = capsys.readouterr().out
    assert "polychromatic" in out
    assert "direct" in out


def test_view_package_transitive_no_parents_prints_not_tracked(tmp_path: Path, capsys) -> None:
    obj = make_why([], tmp_path / "deps.log", {})
    obj._view_package_transitive("bash", {}, set(), lambda n: n)
    assert "Not tracked" in capsys.readouterr().out


# ---------------------------------------------------------------------------
# _view_package
# ---------------------------------------------------------------------------


def test_view_package_empty_deps_log_prints_not_tracked(tmp_path: Path, capsys) -> None:
    installed = {"bash": "bash-5.2-x86_64-1"}
    obj = make_why([], tmp_path / "deps.log", installed)
    obj._view_package("bash", {}, {"bash"}, {"bash": "bash-5.2-x86_64-1"}, lambda n: n)
    assert "Not tracked" in capsys.readouterr().out


def test_view_package_non_transitive_delegates_to_flat(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    installed_dict = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why([], tmp_path / "deps.log", installed_dict)
    obj.transitive = False
    obj._view_package("openrazer-daemon", deps_log, set(installed_dict), installed_dict, lambda n: n)
    out = capsys.readouterr().out
    assert "polychromatic" in out


def test_view_package_transitive_delegates_to_tree(tmp_path: Path, capsys) -> None:
    deps_log = {"polychromatic": ["openrazer-daemon"]}
    installed_dict = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
    }
    obj = make_why([], tmp_path / "deps.log", installed_dict)
    obj.transitive = True
    obj._view_package("openrazer-daemon", deps_log, set(installed_dict), installed_dict, lambda n: n)
    out = capsys.readouterr().out
    assert "polychromatic" in out
    assert "direct" in out
