#!/usr/bin/python3
"""Tests for slpkg.repo_manager.RepoManager."""

from __future__ import annotations

import json
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
import tomlkit

from slpkg.repo_manager import RepoManager


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

MINIMAL_TOML = """\
[SBO]
ENABLE = true
MIRROR = "https://slackbuilds.org/slackbuilds/15.0/"

[PONCE]
ENABLE = false
MIRROR = "https://github.com/Ponce/slackbuilds.git"

[ALIEN]
ENABLE = false
MIRROR = "https://slackware.uk/people/alien/sbrepos/15.0/x86_64/"
TAG = "alien"
"""

MINIMAL_REPOS = {
    "sbo": {"enable": True, "mirror_packages": "https://slackbuilds.org/slackbuilds/15.0/", "mirror_changelog": ""},
    "ponce": {"enable": False, "mirror_packages": "https://github.com/Ponce/slackbuilds.git", "mirror_changelog": ""},
    "alien": {
        "enable": False,
        "mirror_packages": "https://slackware.uk/people/alien/sbrepos/15.0/x86_64/",
        "mirror_changelog": "",
    },
}


def make_manager(repos: dict | None = None, default: str = "sbo", toml_content: str = MINIMAL_TOML) -> RepoManager:
    """Create a RepoManager bypassing real config/Repositories/filesystem."""
    mgr: RepoManager = object.__new__(RepoManager)

    mock_repos = MagicMock()
    mock_repos.repositories = repos if repos is not None else dict(MINIMAL_REPOS)
    mock_repos.default_repository = default
    mock_repos.repositories_toml_file = Path("/fake/repositories.toml")
    mgr.repos = mock_repos

    mgr.toml_file = Path("/fake/repositories.toml")
    mgr.cyan = ""
    mgr.green = ""
    mgr.red = ""
    mgr.grey = ""
    mgr.bold = ""
    mgr.endc = ""
    mgr.columns = 80

    mgr._toml_content = toml_content  # type: ignore[attr-defined]
    return mgr


def _patch_toml_io(mgr: RepoManager, content: str):
    """Context manager that patches _load_toml and captures _write_toml calls."""
    written: list[str] = []

    def fake_load() -> tomlkit.TOMLDocument:
        return tomlkit.parse(content)

    def fake_write(doc: tomlkit.TOMLDocument) -> None:
        written.append(tomlkit.dumps(doc))

    return patch.object(mgr, "_load_toml", side_effect=fake_load), \
        patch.object(mgr, "_write_toml", side_effect=fake_write), \
        written


# ---------------------------------------------------------------------------
# list_repos()
# ---------------------------------------------------------------------------


def test_list_repos_shows_all_repos(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos()
    out = capsys.readouterr().out
    assert "sbo" in out
    assert "ponce" in out
    assert "alien" in out


def test_list_repos_shows_enabled_disabled(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos()
    out = capsys.readouterr().out
    assert "Enabled" in out
    assert "Disabled" in out


def test_list_repos_marks_default_repo(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(default="sbo")
    mgr.list_repos()
    out = capsys.readouterr().out
    # default repo shown with " *" suffix
    assert "sbo *" in out


def test_list_repos_shows_mirror_url(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos()
    out = capsys.readouterr().out
    assert "slackbuilds.org" in out


def test_list_repos_shows_enabled_count(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos()
    out = capsys.readouterr().out
    # 1 of 3 enabled
    assert "1/3" in out


def test_list_repos_truncates_long_mirror(capsys: pytest.CaptureFixture[str]) -> None:
    repos = {
        "myrepo": {
            "enable": True,
            "mirror_packages": "https://example.com/" + "x" * 200,
            "mirror_changelog": "",
        }
    }
    mgr = make_manager(repos=repos, default="myrepo")
    mgr.list_repos()
    out = capsys.readouterr().out
    assert "..." in out


# ---------------------------------------------------------------------------
# enable()
# ---------------------------------------------------------------------------


def test_enable_sets_enable_true(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.enable(["ponce"])
    assert written
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is True


def test_enable_multiple_repos(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.enable(["ponce", "alien"])
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is True
    assert doc["ALIEN"]["ENABLE"] is True


def test_enable_already_enabled_repo(capsys: pytest.CaptureFixture[str]) -> None:
    """Enabling an already-enabled repo prints 'Already enabled' and does not write."""
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.enable(["sbo"])
    assert written == []
    out = capsys.readouterr().out
    assert "Already enabled" in out


def test_enable_case_insensitive(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.enable(["PONCE"])
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is True


def test_enable_unknown_repo_exits(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write, pytest.raises(SystemExit):
        mgr.enable(["nonexistent"])


def test_enable_prints_success_message(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.enable(["ponce"])
    out = capsys.readouterr().out
    assert "ponce" in out.lower()


# ---------------------------------------------------------------------------
# disable()
# ---------------------------------------------------------------------------


def test_disable_sets_enable_false(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.disable(["sbo"])
    doc = tomlkit.parse(written[0])
    assert doc["SBO"]["ENABLE"] is False


def test_disable_multiple_repos(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.disable(["sbo", "ponce"])
    doc = tomlkit.parse(written[0])
    assert doc["SBO"]["ENABLE"] is False
    assert doc["PONCE"]["ENABLE"] is False


def test_disable_already_disabled_repo(capsys: pytest.CaptureFixture[str]) -> None:
    """Disabling an already-disabled repo prints 'Already disabled' and does not write."""
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.disable(["ponce"])
    assert written == []
    out = capsys.readouterr().out
    assert "Already disabled" in out


def test_disable_unknown_repo_exits(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write, pytest.raises(SystemExit):
        mgr.disable(["notarepo"])


def test_disable_case_insensitive(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.disable(["SBO"])
    doc = tomlkit.parse(written[0])
    assert doc["SBO"]["ENABLE"] is False


# ---------------------------------------------------------------------------
# set_mirror()
# ---------------------------------------------------------------------------


def test_set_mirror_updates_mirror_key(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    new_url = "https://mirror.example.com/alien/15.0/x86_64/"
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_mirror("alien", new_url)
    doc = tomlkit.parse(written[0])
    assert doc["ALIEN"]["MIRROR"] == new_url


def test_set_mirror_case_insensitive(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    new_url = "https://mirror.example.com/sbo/"
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_mirror("SBO", new_url)
    doc = tomlkit.parse(written[0])
    assert doc["SBO"]["MIRROR"] == new_url


def test_set_mirror_unknown_repo_exits(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write, pytest.raises(SystemExit):
        mgr.set_mirror("nonexistent", "https://example.com/")


def test_set_mirror_prints_success_message(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    new_url = "https://mirror.example.com/ponce/"
    p_load, p_write, _ = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_mirror("ponce", new_url)
    out = capsys.readouterr().out
    assert "ponce" in out.lower()
    assert new_url in out


def test_set_mirror_does_not_affect_enable(capsys: pytest.CaptureFixture[str]) -> None:
    """Changing the mirror must not change the ENABLE value."""
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_mirror("ponce", "https://new.example.com/")
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is False  # unchanged


def test_set_mirror_preserves_other_sections(capsys: pytest.CaptureFixture[str]) -> None:
    """Updating one repo's mirror must not touch other sections."""
    mgr = make_manager()
    original_sbo = "https://slackbuilds.org/slackbuilds/15.0/"
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_mirror("alien", "https://new.example.com/")
    doc = tomlkit.parse(written[0])
    assert doc["SBO"]["MIRROR"] == original_sbo


# ---------------------------------------------------------------------------
# list_repos(quiet=True)
# ---------------------------------------------------------------------------


def test_list_repos_quiet_prints_names_only(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(quiet=True)
    out = capsys.readouterr().out
    lines = [l for l in out.splitlines() if l.strip()]
    assert set(lines) == {"sbo", "ponce", "alien"}


def test_list_repos_quiet_no_table_headers(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(quiet=True)
    out = capsys.readouterr().out
    assert "Repositories:" not in out
    assert "Status:" not in out
    assert "Mirror:" not in out


# ---------------------------------------------------------------------------
# list_repos(option_json=True)
# ---------------------------------------------------------------------------


def test_list_repos_json_is_valid_json(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    assert isinstance(data, list)


def test_list_repos_json_entry_count(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    assert len(data) == 3


def test_list_repos_json_contains_expected_keys(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    for item in data:
        for key in ("name", "enabled", "default", "mirror"):
            assert key in item, f"Missing key: {key}"


def test_list_repos_json_enabled_field_is_bool(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    for item in data:
        assert isinstance(item["enabled"], bool)


def test_list_repos_json_default_field_correct(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(default="sbo")
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    defaults = [item for item in data if item["default"]]
    assert len(defaults) == 1
    assert defaults[0]["name"] == "sbo"


def test_list_repos_json_enabled_status_matches_repos(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    data = json.loads(capsys.readouterr().out)
    by_name = {item["name"]: item for item in data}
    assert by_name["sbo"]["enabled"] is True
    assert by_name["ponce"]["enabled"] is False
    assert by_name["alien"]["enabled"] is False


def test_list_repos_json_no_table_output(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    mgr.list_repos(option_json=True)
    out = capsys.readouterr().out
    assert "Repositories:" not in out
    assert "Status:" not in out


# ---------------------------------------------------------------------------
# set_default()
# ---------------------------------------------------------------------------

TOML_WITH_DEFAULT = """\
[DEFAULT]
REPOSITORY = "sbo"

[SBO]
ENABLE = true
MIRROR = "https://slackbuilds.org/slackbuilds/15.0/"

[PONCE]
ENABLE = false
MIRROR = "https://github.com/Ponce/slackbuilds.git"
"""


def test_set_default_updates_default_section(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_DEFAULT)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_DEFAULT)
    with p_load, p_write:
        mgr.set_default("ponce")
    doc = tomlkit.parse(written[0])
    assert doc["DEFAULT"]["REPOSITORY"] == "ponce"


def test_set_default_unknown_repo_exits(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, TOML_WITH_DEFAULT)
    with p_load, p_write, pytest.raises(SystemExit):
        mgr.set_default("nonexistent")


def test_set_default_case_insensitive(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_DEFAULT)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_DEFAULT)
    with p_load, p_write:
        mgr.set_default("PONCE")
    doc = tomlkit.parse(written[0])
    assert doc["DEFAULT"]["REPOSITORY"] == "ponce"


def test_set_default_creates_default_section_if_missing(capsys: pytest.CaptureFixture[str]) -> None:
    """If [DEFAULT] section is absent, set_default() should create it."""
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_default("sbo")
    doc = tomlkit.parse(written[0])
    assert doc["DEFAULT"]["REPOSITORY"] == "sbo"


def test_set_default_prints_success_message(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_DEFAULT)
    p_load, p_write, _ = _patch_toml_io(mgr, TOML_WITH_DEFAULT)
    with p_load, p_write:
        mgr.set_default("ponce")
    out = capsys.readouterr().out
    assert "ponce" in out.lower()


def test_set_default_does_not_affect_enable(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_DEFAULT)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_DEFAULT)
    with p_load, p_write:
        mgr.set_default("ponce")
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is False  # unchanged


# ---------------------------------------------------------------------------
# set_priority()
# ---------------------------------------------------------------------------

TOML_WITH_ALL_REPOS = """\
[ALL_REPOS]
PRIORITY = []

[SBO]
ENABLE = true
MIRROR = "https://slackbuilds.org/slackbuilds/15.0/"

[PONCE]
ENABLE = false
MIRROR = "https://github.com/Ponce/slackbuilds.git"

[ALIEN]
ENABLE = false
MIRROR = "https://slackware.uk/people/alien/sbrepos/15.0/x86_64/"
"""


def test_set_priority_updates_list(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.set_priority(["ponce", "alien"])
    doc = tomlkit.parse(written[0])
    assert list(doc["ALL_REPOS"]["PRIORITY"]) == ["ponce", "alien"]


def test_set_priority_clears_list(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.set_priority([])
    doc = tomlkit.parse(written[0])
    assert list(doc["ALL_REPOS"]["PRIORITY"]) == []


def test_set_priority_case_insensitive(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.set_priority(["PONCE", "ALIEN"])
    doc = tomlkit.parse(written[0])
    assert list(doc["ALL_REPOS"]["PRIORITY"]) == ["ponce", "alien"]


def test_set_priority_unknown_repo_exits(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager()
    p_load, p_write, _ = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write, pytest.raises(SystemExit):
        mgr.set_priority(["nonexistent"])


def test_set_priority_creates_all_repos_section(capsys: pytest.CaptureFixture[str]) -> None:
    """If [ALL_REPOS] is absent, set_priority() should create it."""
    mgr = make_manager()
    p_load, p_write, written = _patch_toml_io(mgr, MINIMAL_TOML)
    with p_load, p_write:
        mgr.set_priority(["sbo"])
    doc = tomlkit.parse(written[0])
    assert list(doc["ALL_REPOS"]["PRIORITY"]) == ["sbo"]


def test_set_priority_prints_success_message(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, _ = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.set_priority(["ponce", "alien"])
    out = capsys.readouterr().out
    assert "ponce" in out
    assert "alien" in out


def test_clear_priority_prints_cleared(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, _ = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.clear_priority()
    out = capsys.readouterr().out
    assert "cleared" in out.lower()


def test_show_priority_when_set(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, _, _ = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load:
        mgr.show_priority()
    out = capsys.readouterr().out
    assert "Priority" in out


def test_show_priority_when_empty(capsys: pytest.CaptureFixture[str]) -> None:
    toml_no_priority = "[ALL_REPOS]\nPRIORITY = []\n"
    mgr = make_manager(toml_content=toml_no_priority)
    p_load, _, _ = _patch_toml_io(mgr, toml_no_priority)
    with p_load:
        mgr.show_priority()
    out = capsys.readouterr().out
    assert "not set" in out


def test_set_priority_does_not_affect_enable(capsys: pytest.CaptureFixture[str]) -> None:
    mgr = make_manager(toml_content=TOML_WITH_ALL_REPOS)
    p_load, p_write, written = _patch_toml_io(mgr, TOML_WITH_ALL_REPOS)
    with p_load, p_write:
        mgr.set_priority(["ponce"])
    doc = tomlkit.parse(written[0])
    assert doc["PONCE"]["ENABLE"] is False  # unchanged


# ---------------------------------------------------------------------------
# tomlkit formatting preservation
# ---------------------------------------------------------------------------


def test_enable_preserves_comments() -> None:
    """tomlkit must preserve comments in the file when writing."""
    toml_with_comment = """\
# My comment
[SBO]
ENABLE = true
MIRROR = "https://slackbuilds.org/"

[PONCE]
ENABLE = false
MIRROR = "https://github.com/Ponce/slackbuilds.git"
"""
    repos = {
        "sbo": {"enable": True, "mirror_packages": "https://slackbuilds.org/", "mirror_changelog": ""},
        "ponce": {"enable": False, "mirror_packages": "https://github.com/Ponce/slackbuilds.git", "mirror_changelog": ""},
    }
    mgr = make_manager(repos=repos)
    p_load, p_write, written = _patch_toml_io(mgr, toml_with_comment)
    with p_load, p_write:
        mgr.enable(["ponce"])
    assert "# My comment" in written[0]
