#!/usr/bin/python3
"""Tests for slpkg.reverse.Reverse pure data methods."""

import pytest
from unittest.mock import MagicMock

from slpkg.reverse import Reverse


def make_reverse(data: dict) -> Reverse:
    """Construct Reverse bypassing __init__ (avoids config_load/Utilities)."""
    dep: Reverse = object.__new__(Reverse)
    dep.data = data
    return dep


def make_reverse_full(data: dict, packages: list, options: dict) -> Reverse:
    """Construct Reverse with all attributes needed by find()."""
    rev: Reverse = object.__new__(Reverse)
    rev.data = data
    rev.packages = packages
    rev.options = options
    rev.bold = rev.grey = rev.yellow = rev.cyan = rev.endc = ""
    rev.unicode = True
    rev.utils = MagicMock()
    rev.utils.apply_package_pattern.side_effect = lambda d, pkgs: pkgs
    rev.option_for_full_reverse = options.get("option_full_reverse", False)
    rev.option_for_pkg_version = options.get("option_pkg_version", False)
    rev.option_quiet = options.get("option_quiet", False)
    return rev


# ---------------------------------------------------------------------------
# _make_label_fn()
# ---------------------------------------------------------------------------


def test_make_label_fn_returns_none_when_version_disabled(sample_data: dict) -> None:
    """With option_pkg_version=False, _make_label_fn returns None."""
    rev = make_reverse_full(sample_data, [], {"option_pkg_version": False})
    assert rev._make_label_fn() is None


def test_make_label_fn_returns_callable_when_version_enabled(sample_data: dict) -> None:
    """With option_pkg_version=True, _make_label_fn returns a callable."""
    rev = make_reverse_full(sample_data, [], {"option_pkg_version": True})
    assert callable(rev._make_label_fn())


def test_make_label_fn_callable_appends_version(sample_data: dict) -> None:
    """The returned callable appends the repo version when it is known."""
    rev = make_reverse_full(sample_data, [], {"option_pkg_version": True})
    fn = rev._make_label_fn()
    assert fn is not None
    label = fn("ffmpeg")
    assert "ffmpeg" in label
    assert "7.1" in label


def test_make_label_fn_callable_returns_name_only_when_no_version(sample_data: dict) -> None:
    """The returned callable returns just the name when version is unknown."""
    rev = make_reverse_full(sample_data, [], {"option_pkg_version": True})
    fn = rev._make_label_fn()
    assert fn is not None
    assert fn("nonexistent") == "nonexistent"


# ---------------------------------------------------------------------------
# set_the_package_version
# ---------------------------------------------------------------------------


def test_set_the_package_version_known_package(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    assert dep.set_the_package_version("ffmpeg") == "7.1"


def test_set_the_package_version_another_known_package(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    assert dep.set_the_package_version("bash") == "5.2.026"


def test_set_the_package_version_yasm(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    assert dep.set_the_package_version("yasm") == "1.3.0"


def test_set_the_package_version_unknown_returns_empty(sample_data: dict) -> None:
    """Unknown package → data.get() returns {} → .get('version', '') returns ''."""
    dep = make_reverse(sample_data)
    assert dep.set_the_package_version("nonexistent") == ""


def test_set_the_package_version_empty_data() -> None:
    dep = make_reverse({})
    assert dep.set_the_package_version("ffmpeg") == ""


# ---------------------------------------------------------------------------
# find_requires (generator)
# ---------------------------------------------------------------------------


def test_find_requires_yields_packages_that_depend_on_target(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    # ffmpeg requires libvpx; x264 does NOT require libvpx
    results = dict(dep.find_requires("libvpx"))
    assert "ffmpeg" in results


def test_find_requires_yields_correct_requires_list(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    results = dict(dep.find_requires("libvpx"))
    assert results["ffmpeg"] == ["libvpx", "x264"]


def test_find_requires_yasm_depended_on_by_x264(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    results = dict(dep.find_requires("yasm"))
    assert "x264" in results
    assert results["x264"] == ["yasm"]


def test_find_requires_no_dependees_yields_nothing(sample_data: dict) -> None:
    """perl has no other package depending on it."""
    dep = make_reverse(sample_data)
    results = list(dep.find_requires("perl"))
    assert results == []


def test_find_requires_returns_generator(sample_data: dict) -> None:
    import types

    dep = make_reverse(sample_data)
    gen = dep.find_requires("libvpx")
    assert isinstance(gen, types.GeneratorType)


def test_find_requires_yields_tuples_of_str_and_list(sample_data: dict) -> None:
    dep = make_reverse(sample_data)
    for name, requires in dep.find_requires("libvpx"):
        assert isinstance(name, str)
        assert isinstance(requires, list)


def test_find_requires_package_with_no_requires_key() -> None:
    """Packages without a 'requires' key must be safely skipped."""
    data = {
        "pkg_a": {"version": "1.0", "requires": ["target"]},
        "pkg_b": {"version": "1.0"},  # no 'requires' key
    }
    dep = make_reverse(data)
    results = dict(dep.find_requires("target"))
    assert "pkg_a" in results
    assert "pkg_b" not in results


def test_find_requires_requires_not_list_is_skipped() -> None:
    """Packages whose 'requires' value is not a list must be skipped."""
    data = {
        "pkg_a": {"version": "1.0", "requires": ["target"]},
        "pkg_bad": {"version": "1.0", "requires": "target"},  # str, not list
    }
    dep = make_reverse(data)
    results = dict(dep.find_requires("target"))
    assert "pkg_a" in results
    assert "pkg_bad" not in results


# ---------------------------------------------------------------------------
# _build_reverse_tree
# ---------------------------------------------------------------------------


def test_build_reverse_tree_no_dependees_returns_empty(sample_data: dict) -> None:
    """bash has no packages depending on it."""
    dep = make_reverse(sample_data)
    assert dep._build_reverse_tree("bash") == {}


def test_build_reverse_tree_single_direct_dependee(sample_data: dict) -> None:
    """yasm is required by x264 only."""
    dep = make_reverse(sample_data)
    tree = dep._build_reverse_tree("yasm")
    assert list(tree.keys()) == ["x264"]


def test_build_reverse_tree_two_level_chain(sample_data: dict) -> None:
    """yasm ← x264 ← ffmpeg: tree should be {"x264": {"ffmpeg": {}}}."""
    dep = make_reverse(sample_data)
    tree = dep._build_reverse_tree("yasm")
    assert "x264" in tree
    assert "ffmpeg" in tree["x264"]
    assert tree["x264"]["ffmpeg"] == {}


def test_build_reverse_tree_libvpx_direct_parent_ffmpeg(sample_data: dict) -> None:
    """libvpx ← ffmpeg; ffmpeg has no further reverse-dependees in sample_data."""
    dep = make_reverse(sample_data)
    tree = dep._build_reverse_tree("libvpx")
    assert tree == {"ffmpeg": {}}


def test_build_reverse_tree_cycle_terminates() -> None:
    """Pathological cycle: a requires b, b requires a — must not recurse infinitely."""
    data = {
        "a": {"requires": ["b"], "version": "1.0"},
        "b": {"requires": ["a"], "version": "1.0"},
    }
    dep = make_reverse(data)
    result = dep._build_reverse_tree("a")
    assert isinstance(result, dict)


def test_build_reverse_tree_missing_requires_key_skipped() -> None:
    """Packages without a 'requires' key must not appear as dependees."""
    data = {
        "with_req": {"requires": ["target"], "version": "1.0"},
        "no_req": {"version": "1.0"},
    }
    dep = make_reverse(data)
    tree = dep._build_reverse_tree("target")
    assert "with_req" in tree
    assert "no_req" not in tree


# ---------------------------------------------------------------------------
# find(): quiet mode
# ---------------------------------------------------------------------------


def test_find_quiet_flat_prints_direct_dependees(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Quiet + no full-reverse: prints only the names of direct reverse-deps."""
    rev = make_reverse_full(sample_data, ["libvpx"], {"option_quiet": True})
    rev.find()
    out = capsys.readouterr().out
    assert "ffmpeg" in out


def test_find_quiet_flat_no_dependees_prints_nothing(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Quiet + no full-reverse + no dependees: nothing printed."""
    rev = make_reverse_full(sample_data, ["perl"], {"option_quiet": True})
    rev.find()
    out = capsys.readouterr().out
    assert out.strip() == ""


def test_find_quiet_full_reverse_includes_transitive(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Quiet + full-reverse: prints both direct and transitive reverse-deps."""
    rev = make_reverse_full(sample_data, ["yasm"], {"option_quiet": True, "option_full_reverse": True})
    rev.find()
    out = capsys.readouterr().out
    assert "x264" in out
    assert "ffmpeg" in out


def test_find_quiet_full_reverse_no_dependees_prints_nothing(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Quiet + full-reverse + no dependees: nothing printed."""
    rev = make_reverse_full(sample_data, ["perl"], {"option_quiet": True, "option_full_reverse": True})
    rev.find()
    out = capsys.readouterr().out
    assert out.strip() == ""


# ---------------------------------------------------------------------------
# find(): normal (non-quiet) mode — flat list
# ---------------------------------------------------------------------------


def test_find_flat_prints_header(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Non-quiet flat mode prints the queried package name as a header."""
    rev = make_reverse_full(sample_data, ["libvpx"], {})
    rev.find()
    out = capsys.readouterr().out
    assert "libvpx" in out


def test_find_flat_with_dependees_prints_summary(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Non-quiet flat mode prints a 'reverse dependenc' summary line."""
    rev = make_reverse_full(sample_data, ["libvpx"], {})
    rev.find()
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "reverse dependenc" in out


def test_find_flat_no_dependees_prints_message(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Non-quiet flat mode with no dependees prints 'No reverse dependencies'."""
    rev = make_reverse_full(sample_data, ["perl"], {})
    rev.find()
    out = capsys.readouterr().out
    assert "No reverse dependencies" in out


def test_find_flat_single_dependee_singular_noun(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """When exactly one dependee exists the summary uses singular form."""
    rev = make_reverse_full(sample_data, ["libvpx"], {})
    rev.find()
    out = capsys.readouterr().out
    # ffmpeg is the only direct dependee of libvpx
    assert "reverse dependency" in out or "reverse dependencies" in out


# ---------------------------------------------------------------------------
# find(): normal (non-quiet) mode — full reverse tree
# ---------------------------------------------------------------------------


def test_find_full_reverse_with_dependees_prints_summary(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Full-reverse mode prints a 'direct' summary line."""
    rev = make_reverse_full(sample_data, ["yasm"], {"option_full_reverse": True})
    rev.find()
    out = capsys.readouterr().out
    assert "x264" in out
    assert "direct" in out


def test_find_full_reverse_no_dependees_prints_message(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Full-reverse mode with no dependees prints 'No reverse dependencies'."""
    rev = make_reverse_full(sample_data, ["perl"], {"option_full_reverse": True})
    rev.find()
    out = capsys.readouterr().out
    assert "No reverse dependencies" in out


def test_find_full_reverse_transitive_count_in_summary(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """Full-reverse on yasm should mention 'transitive' (ffmpeg is a transitive dep)."""
    rev = make_reverse_full(sample_data, ["yasm"], {"option_full_reverse": True})
    rev.find()
    out = capsys.readouterr().out
    assert "transitive" in out


# ---------------------------------------------------------------------------
# find(): pkg-version option
# ---------------------------------------------------------------------------


def test_find_pkg_version_shows_version_in_header(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """With option_pkg_version the queried package version appears in the header."""
    rev = make_reverse_full(sample_data, ["libvpx"], {"option_pkg_version": True})
    rev.find()
    out = capsys.readouterr().out
    assert "1.13.1" in out


def test_find_pkg_version_label_shown_for_dependees(sample_data: dict, capsys: pytest.CaptureFixture) -> None:
    """With option_pkg_version the label_fn is active; dependee version is visible."""
    rev = make_reverse_full(sample_data, ["libvpx"], {"option_pkg_version": True})
    rev.find()
    out = capsys.readouterr().out
    assert "ffmpeg" in out
    assert "7.1" in out


def test_find_pkg_version_unknown_package_no_version(capsys: pytest.CaptureFixture) -> None:
    """label_fn for a package with no version entry returns just the name."""
    data = {
        "parent": {"requires": ["target"], "version": ""},
        "target": {"version": ""},
    }
    rev = make_reverse_full(data, ["target"], {"option_pkg_version": True})
    rev.find()
    out = capsys.readouterr().out
    assert "target" in out
