#!/usr/bin/python3
"""Tests for slpkg.pin.Pin."""

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

from slpkg.pin import Pin


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


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


# ---------------------------------------------------------------------------
# read_pins
# ---------------------------------------------------------------------------


def test_read_pins_missing_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, [])
    assert obj.read_pins() == set()


def test_read_pins_invalid_json_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "pins.log"
    f.write_text("not valid json", encoding="utf-8")
    obj = make_pin(f, [])
    assert obj.read_pins() == set()


def test_read_pins_non_list_json_returns_empty_set(tmp_path: Path) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps({"pkg": True}), encoding="utf-8")
    obj = make_pin(f, [])
    assert obj.read_pins() == set()


def test_read_pins_valid_returns_set(tmp_path: Path) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, [])
    assert obj.read_pins() == {"rust", "llvm"}


# ---------------------------------------------------------------------------
# _write_pins
# ---------------------------------------------------------------------------


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


# ---------------------------------------------------------------------------
# _pin (add)
# ---------------------------------------------------------------------------


def test_pin_adds_package_to_log(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, ["rust"])
    obj._pin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "rust" in data


def test_pin_idempotent_already_pinned(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust"]), encoding="utf-8")
    obj = make_pin(f, ["rust"])
    obj._pin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data.count("rust") == 1


def test_pin_multiple_packages(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, ["rust", "llvm"])
    obj._pin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "rust" in data
    assert "llvm" in data


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


# ---------------------------------------------------------------------------
# _unpin
# ---------------------------------------------------------------------------


def test_unpin_removes_package(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, ["rust"], unpin=True)
    obj._unpin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "rust" not in data
    assert "llvm" in data


def test_unpin_not_in_list_is_silent_noop(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["llvm"]), encoding="utf-8")
    obj = make_pin(f, ["nonexistent"], unpin=True)
    obj._unpin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == ["llvm"]


def test_unpin_missing_log_is_silent_noop(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, ["nonexistent"], unpin=True)
    obj._unpin()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == []


# ---------------------------------------------------------------------------
# _unpin_all
# ---------------------------------------------------------------------------


def test_unpin_all_removes_all_pins(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, [], unpin=True)
    obj._unpin_all()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == []


def test_unpin_all_empty_log_shows_message(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, [], unpin=True)
    obj._unpin_all()
    out = capsys.readouterr().out
    assert "No pinned packages found" in out


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


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


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


def test_view_unpin_with_packages_calls_unpin(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust"]), encoding="utf-8")
    obj = make_pin(f, ["rust"], unpin=True)
    obj.view()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert "rust" not in data


def test_view_unpin_no_packages_calls_unpin_all(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, [], unpin=True)
    obj.view()
    data = json.loads(f.read_text(encoding="utf-8"))
    assert data == []


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


def test_list_empty_shows_no_pinned_message(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    obj = make_pin(f, [])
    obj._list()
    out = capsys.readouterr().out
    assert "No pinned packages found" in out


def test_list_shows_count(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, [])
    obj._list()
    out = capsys.readouterr().out
    assert "2" in out


def test_list_quiet_prints_names_only(tmp_path: Path, capsys) -> None:
    f = tmp_path / "pins.log"
    f.write_text(json.dumps(["rust", "llvm"]), encoding="utf-8")
    obj = make_pin(f, [], quiet=True)
    obj._list()
    out = capsys.readouterr().out.splitlines()
    assert set(out) == {"rust", "llvm"}
