#!/usr/bin/python3
"""Tests for slpkg.mark.Mark and orphans integration with marks."""

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

from slpkg.mark import Mark
from slpkg.orphans import Orphans

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


def make_mark(marks_log_file: Path, packages: list[str], *, unmark: bool = False, quiet: bool = False) -> Mark:
    """Construct Mark bypassing __init__ (avoids config_load/Utilities)."""
    obj: Mark = object.__new__(Mark)
    obj.marks_log_file = marks_log_file
    obj.packages = packages
    obj.unmark = unmark
    obj.quiet = quiet
    obj.bold = ""
    obj.grey = ""
    obj.cyan = ""
    obj.green = ""
    obj.endc = ""
    obj.utils = MagicMock()
    return obj


def make_orphans(deps_log_file: Path, marks_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
    obj.marks_log_file = marks_log_file
    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


# ---------------------------------------------------------------------------
# _read_marks_log
# ---------------------------------------------------------------------------


def test_read_marks_log_missing_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, [])
    assert obj._read_marks_log() == set()


def test_read_marks_log_invalid_json_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "marks.log"
    f.write_text("not valid json", encoding="utf-8")
    obj = make_mark(f, [])
    assert obj._read_marks_log() == set()


def test_read_marks_log_non_list_json_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps({"pkg": True}), encoding="utf-8")
    obj = make_mark(f, [])
    assert obj._read_marks_log() == set()


def test_read_marks_log_valid_returns_set(tmp_path: Path) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["python-daemonize", "some-pkg"]), encoding="utf-8")
    obj = make_mark(f, [])
    assert obj._read_marks_log() == {"python-daemonize", "some-pkg"}


# ---------------------------------------------------------------------------
# _write_marks_log
# ---------------------------------------------------------------------------


def test_write_marks_log_writes_sorted_json(tmp_path: Path) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, [])
    obj._write_marks_log({"zebra", "alpha", "mango"})
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == ["alpha", "mango", "zebra"]


# ---------------------------------------------------------------------------
# _mark (add)
# ---------------------------------------------------------------------------


def test_mark_adds_package_to_log(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, ["python-daemonize"])
    obj._mark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "python-daemonize" in data


def test_mark_idempotent_already_marked(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["python-daemonize"]), encoding="utf-8")
    obj = make_mark(f, ["python-daemonize"])
    obj._mark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data.count("python-daemonize") == 1


def test_mark_multiple_packages(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, ["pkg-a", "pkg-b"])
    obj._mark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "pkg-a" in data
    assert "pkg-b" in data


def test_mark_preserves_existing_entries(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["existing-pkg"]), encoding="utf-8")
    obj = make_mark(f, ["new-pkg"])
    obj._mark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "existing-pkg" in data
    assert "new-pkg" in data


# ---------------------------------------------------------------------------
# _unmark (--auto)
# ---------------------------------------------------------------------------


def test_unmark_removes_package(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["python-daemonize", "other-pkg"]), encoding="utf-8")
    obj = make_mark(f, ["python-daemonize"], unmark=True)
    obj._unmark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "python-daemonize" not in data
    assert "other-pkg" in data


def test_unmark_not_in_list_is_silent_noop(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["other-pkg"]), encoding="utf-8")
    obj = make_mark(f, ["nonexistent"], unmark=True)
    obj._unmark()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == ["other-pkg"]


def test_unmark_missing_log_is_silent_noop(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, ["nonexistent"], unmark=True)
    obj._unmark()  # Must not raise
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == []


# ---------------------------------------------------------------------------
# view() dispatch
# ---------------------------------------------------------------------------


def test_view_no_packages_calls_list(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["pkg-a"]), encoding="utf-8")
    obj = make_mark(f, [])
    obj.view()
    out = capsys.readouterr().out
    assert "pkg-a" in out


def test_view_with_packages_calls_mark(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, ["new-pkg"])
    obj.view()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "new-pkg" in data


def test_view_auto_calls_unmark(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["target-pkg"]), encoding="utf-8")
    obj = make_mark(f, ["target-pkg"], unmark=True)
    obj.view()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "target-pkg" not in data


# ---------------------------------------------------------------------------
# _list output format
# ---------------------------------------------------------------------------


def test_list_empty_shows_no_marked_message(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    obj = make_mark(f, [])
    obj._list()
    out = capsys.readouterr().out
    assert "No manually marked packages found" in out


def test_list_shows_count(tmp_path: Path, capsys) -> None:
    f = tmp_path / "marks.log"
    f.write_text(json.dumps(["pkg-a", "pkg-b"]), encoding="utf-8")
    obj = make_mark(f, [])
    obj._list()
    out = capsys.readouterr().out
    assert "2" in out


# ---------------------------------------------------------------------------
# Orphans integration: marked package excluded from orphans
# ---------------------------------------------------------------------------


def test_marked_package_excluded_from_orphans(tmp_path: Path) -> None:
    deps_log_file = tmp_path / "deps.log"
    marks_log_file = tmp_path / "marks.log"

    deps_log = {"polychromatic": ["openrazer-daemon", "python-daemonize"]}
    deps_log_file.write_text(json.dumps(deps_log), encoding="utf-8")

    # python-daemonize is manually marked
    marks_log_file.write_text(json.dumps(["python-daemonize"]), encoding="utf-8")

    # polychromatic removed; its deps still installed
    installed = {
        "openrazer-daemon": "openrazer-daemon-1.3-x86_64-1",
        "python-daemonize": "python-daemonize-2.5-x86_64-1",
    }
    obj = make_orphans(deps_log_file, marks_log_file, installed)
    orphans = obj.find()

    # python-daemonize is marked → not an orphan; openrazer-daemon is an orphan
    assert "python-daemonize" not in orphans
    assert "openrazer-daemon" in orphans


def test_unmarked_package_appears_in_orphans(tmp_path: Path) -> None:
    deps_log_file = tmp_path / "deps.log"
    marks_log_file = tmp_path / "marks.log"

    deps_log = {"polychromatic": ["openrazer-daemon"]}
    deps_log_file.write_text(json.dumps(deps_log), encoding="utf-8")

    # No marks
    marks_log_file.write_text(json.dumps([]), encoding="utf-8")

    installed = {"openrazer-daemon": "openrazer-daemon-1.3-x86_64-1"}
    obj = make_orphans(deps_log_file, marks_log_file, installed)
    orphans = obj.find()

    assert "openrazer-daemon" in orphans


def test_marked_but_not_installed_ignored(tmp_path: Path) -> None:
    deps_log_file = tmp_path / "deps.log"
    marks_log_file = tmp_path / "marks.log"

    deps_log = {"polychromatic": ["openrazer-daemon"]}
    deps_log_file.write_text(json.dumps(deps_log), encoding="utf-8")

    # Mark a package that's not installed
    marks_log_file.write_text(json.dumps(["not-installed-pkg"]), encoding="utf-8")

    installed = {"openrazer-daemon": "openrazer-daemon-1.3-x86_64-1"}
    obj = make_orphans(deps_log_file, marks_log_file, installed)
    orphans = obj.find()

    # openrazer-daemon is still an orphan (marking a non-installed pkg has no effect)
    assert "openrazer-daemon" in orphans
    assert "not-installed-pkg" not in orphans


def test_missing_marks_log_no_effect_on_orphans(tmp_path: Path) -> None:
    deps_log_file = tmp_path / "deps.log"
    marks_log_file = tmp_path / "nonexistent_marks.log"

    deps_log = {"polychromatic": ["openrazer-daemon"]}
    deps_log_file.write_text(json.dumps(deps_log), encoding="utf-8")

    installed = {"openrazer-daemon": "openrazer-daemon-1.3-x86_64-1"}
    obj = make_orphans(deps_log_file, marks_log_file, installed)
    orphans = obj.find()

    assert "openrazer-daemon" in orphans
