#!/usr/bin/python3
"""Tests for slpkg.orphans.Orphans.find()."""

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

from slpkg.orphans import Orphans


def make_orphans(deps_log_file: Path, installed: dict[str, str]) -> Orphans:
    """Construct Orphans bypassing __init__ (avoids config_load/Utilities)."""
    obj: Orphans = object.__new__(Orphans)
    obj.deps_log_file = deps_log_file
    # Point marks_log_file to a nonexistent path so _read_marks_log returns empty set.
    obj.marks_log_file = deps_log_file.parent / "marks.log"
    obj.utils = MagicMock()
    obj.utils.all_installed.return_value = installed
    obj.bold = ""
    obj.green = ""
    obj.grey = ""
    obj.cyan = ""
    obj.endc = ""
    obj.quiet = False
    return obj


# ---------------------------------------------------------------------------
# Empty / missing deps.log → no orphans
# ---------------------------------------------------------------------------


def test_empty_deps_log_returns_no_orphans(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    f.write_text(json.dumps({}), encoding="utf-8")
    obj = make_orphans(f, {"python3": "python3-3.11-x86_64-1"})
    assert obj.find() == []


def test_missing_deps_log_returns_no_orphans(tmp_path: Path) -> None:
    f = tmp_path / "nonexistent_deps.log"
    obj = make_orphans(f, {"python3": "python3-3.11-x86_64-1"})
    assert obj.find() == []


def test_invalid_json_returns_no_orphans(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    f.write_text("not valid json", encoding="utf-8")
    obj = make_orphans(f, {"python3": "python3-3.11-x86_64-1"})
    assert obj.find() == []


def test_non_dict_json_returns_no_orphans(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    f.write_text(json.dumps(["polychromatic", "yasm"]), encoding="utf-8")
    obj = make_orphans(f, {"python3": "python3-3.11-x86_64-1"})
    assert obj.find() == []


# ---------------------------------------------------------------------------
# All deps still needed → no orphans
# ---------------------------------------------------------------------------


def test_all_deps_still_needed_no_orphans(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_orphans(f, installed)
    assert obj.find() == []


# ---------------------------------------------------------------------------
# Parent removed → dep becomes an orphan
# ---------------------------------------------------------------------------


def test_parent_removed_dep_becomes_orphan(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")
    # polychromatic was removed; its deps are still installed
    installed = {
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "openrazer-kernel": "openrazer-kernel-1.3-x86_64-1",
    }
    obj = make_orphans(f, installed)
    assert obj.find() == ["openrazer-daemon", "openrazer-kernel"]


def test_only_one_parent_removed(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {
        "polychromatic": ["openrazer-daemon", "openrazer-kernel"],
        "ranger": ["python3"],
    }
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    # ranger removed, polychromatic still installed
    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",
        "python3": "python3-3.11-x86_64-1",
    }
    obj = make_orphans(f, installed)
    # python3 is now orphaned (ranger removed), but openrazer-* are still needed
    assert obj.find() == ["python3"]


# ---------------------------------------------------------------------------
# Dep explicitly reinstalled → NOT an orphan (it's also a top-level key)
# ---------------------------------------------------------------------------


def test_dep_explicitly_reinstalled_not_orphan(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    # python3 appears as both a key (explicitly installed) and a dep of ranger
    deps_log = {
        "polychromatic": ["openrazer-daemon"],
        "ranger": ["python3"],
        "python3": [],  # explicitly installed
    }
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    # ranger removed, so python3 has no active parent — but it's in keys → explicit
    installed = {
        "polychromatic": "polychromatic-1.0-x86_64-1",
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "python3": "python3-3.11-x86_64-1",
    }
    obj = make_orphans(f, installed)
    # python3 is explicit → not an orphan; openrazer-daemon is still needed
    assert obj.find() == []


# ---------------------------------------------------------------------------
# Dep not installed → not in candidates
# ---------------------------------------------------------------------------


def test_dep_not_installed_not_in_orphans(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; openrazer-daemon also removed
    installed: dict[str, str] = {}
    obj = make_orphans(f, installed)
    assert obj.find() == []


# ---------------------------------------------------------------------------
# Shared dependency — needed by at least one remaining parent
# ---------------------------------------------------------------------------


def test_shared_dep_needed_by_one_remaining_parent_not_orphan(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {
        "app-a": ["shared-lib"],
        "app-b": ["shared-lib"],
    }
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    # app-a removed, app-b still installed → shared-lib still needed
    installed = {
        "app-b": "app-b-2.0-x86_64-1",
        "shared-lib": "shared-lib-1.0-x86_64-1",
    }
    obj = make_orphans(f, installed)
    assert obj.find() == []


# ---------------------------------------------------------------------------
# Result is sorted
# ---------------------------------------------------------------------------


def test_orphans_returned_sorted(tmp_path: Path) -> None:
    f = tmp_path / "deps.log"
    deps_log = {"app": ["zebra", "alpha", "mango"]}
    f.write_text(json.dumps(deps_log), encoding="utf-8")
    installed = {
        "zebra": "zebra-1.0-x86_64-1",
        "alpha": "alpha-1.0-x86_64-1",
        "mango": "mango-1.0-x86_64-1",
    }
    obj = make_orphans(f, installed)
    assert obj.find() == ["alpha", "mango", "zebra"]
