#!/usr/bin/python3
"""Tests for slpkg.new_configs.NewConfigs."""

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest
import tomlkit

from slpkg.new_configs import NewConfigs


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


def make_new_configs(tmp_path: Path) -> NewConfigs:
    """Construct NewConfigs with paths redirected to tmp_path."""
    obj: NewConfigs = object.__new__(NewConfigs)
    obj.etc_path = tmp_path
    obj.slpkg_config = tmp_path / "slpkg.toml"
    obj.repositories_config = tmp_path / "repositories.toml"
    obj.blacklist_config = tmp_path / "blacklist.toml"
    obj.slpkg_config_new = tmp_path / "slpkg.toml.new"
    obj.repositories_config_new = tmp_path / "repositories.toml.new"
    obj.blacklist_config_new = tmp_path / "blacklist.toml.new"
    obj.bold = ""
    obj.red = ""
    obj.green = ""
    obj.bgreen = ""
    obj.yellow = ""
    obj.byellow = ""
    obj.endc = ""
    return obj


# ---------------------------------------------------------------------------
# set_no_colors
# ---------------------------------------------------------------------------


def test_set_no_colors_clears_all_color_codes(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.bold = "\033[1m"
    obj.red = "\x1b[91m"
    obj.set_no_colors()
    assert obj.bold == ""
    assert obj.red == ""
    assert obj.green == ""
    assert obj.yellow == ""
    assert obj.endc == ""


# ---------------------------------------------------------------------------
# keep
# ---------------------------------------------------------------------------


def test_keep_prints_no_changes(capsys) -> None:
    NewConfigs.keep()
    out = capsys.readouterr().out
    assert "No changes were made" in out


# ---------------------------------------------------------------------------
# check — no .new files
# ---------------------------------------------------------------------------


def test_check_no_new_files_prints_message(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.menu = MagicMock()
    obj.check()
    out = capsys.readouterr().out
    assert "No .new files found" in out
    obj.menu.assert_not_called()


# ---------------------------------------------------------------------------
# check — .new files present
# ---------------------------------------------------------------------------


def test_check_with_new_files_calls_menu(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config_new.write_text("[SLPKG]\n", encoding="utf-8")
    obj.menu = MagicMock()
    obj.check()
    obj.menu.assert_called_once()
    out = capsys.readouterr().out
    assert "There are NEW files" in out


def test_check_lists_all_new_files(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config_new.write_text("", encoding="utf-8")
    obj.repositories_config_new.write_text("", encoding="utf-8")
    obj.blacklist_config_new.write_text("", encoding="utf-8")
    obj.menu = MagicMock()
    obj.check()
    out = capsys.readouterr().out
    assert "slpkg.toml.new" in out
    assert "repositories.toml.new" in out
    assert "blacklist.toml.new" in out


# ---------------------------------------------------------------------------
# remove_*
# ---------------------------------------------------------------------------


def test_remove_config_new_file_deletes_file(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config_new.write_text("[SLPKG]\n", encoding="utf-8")
    obj.remove_config_new_file()
    assert not obj.slpkg_config_new.exists()


def test_remove_repositories_new_file_deletes_file(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.repositories_config_new.write_text("[SBO]\n", encoding="utf-8")
    obj.remove_repositories_new_file()
    assert not obj.repositories_config_new.exists()


def test_remove_blacklist_new_file_deletes_file(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.blacklist_config_new.write_text("[BLACKLIST]\n", encoding="utf-8")
    obj.remove_blacklist_new_file()
    assert not obj.blacklist_config_new.exists()


def test_remove_file_not_present_is_silent_noop(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.remove_config_new_file()  # file doesn't exist — must not raise
    out = capsys.readouterr().out
    assert "Error" not in out


# ---------------------------------------------------------------------------
# overwrite_config_file
# ---------------------------------------------------------------------------


def test_overwrite_config_file_moves_new_to_current(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text("[OLD]\n", encoding="utf-8")
    obj.slpkg_config_new.write_text("[NEW]\n", encoding="utf-8")
    obj.overwrite_config_file()
    assert obj.slpkg_config.read_text(encoding="utf-8") == "[NEW]\n"
    assert not obj.slpkg_config_new.exists()


def test_overwrite_config_file_backs_up_old(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text("[OLD]\n", encoding="utf-8")
    obj.slpkg_config_new.write_text("[NEW]\n", encoding="utf-8")
    obj.overwrite_config_file()
    orig = tmp_path / "slpkg.toml.orig"
    assert orig.exists()
    assert orig.read_text(encoding="utf-8") == "[OLD]\n"


def test_overwrite_repositories_file_moves_new_to_current(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.repositories_config.write_text("[OLD]\n", encoding="utf-8")
    obj.repositories_config_new.write_text("[NEW]\n", encoding="utf-8")
    obj.overwrite_repositories_file()
    assert obj.repositories_config.read_text(encoding="utf-8") == "[NEW]\n"


def test_overwrite_blacklist_file_moves_new_to_current(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.blacklist_config.write_text("[OLD]\n", encoding="utf-8")
    obj.blacklist_config_new.write_text("[NEW]\n", encoding="utf-8")
    obj.overwrite_blacklist_file()
    assert obj.blacklist_config.read_text(encoding="utf-8") == "[NEW]\n"


# ---------------------------------------------------------------------------
# diff_files
# ---------------------------------------------------------------------------


def test_diff_files_shows_differences(tmp_path: Path, capsys) -> None:
    current = tmp_path / "current.toml"
    new = tmp_path / "new.toml"
    current.write_text("line_a = 1\n", encoding="utf-8")
    new.write_text("line_a = 2\n", encoding="utf-8")
    NewConfigs.diff_files(new, current)
    out = capsys.readouterr().out
    assert "line_a" in out


def test_diff_files_missing_file_prints_error(tmp_path: Path, capsys) -> None:
    current = tmp_path / "nonexistent.toml"
    new = tmp_path / "also_nonexistent.toml"
    NewConfigs.diff_files(new, current)
    out = capsys.readouterr().out
    assert "Error" in out


# ---------------------------------------------------------------------------
# vimdiff
# ---------------------------------------------------------------------------


def test_vimdiff_not_found_raises_systemexit(tmp_path: Path) -> None:
    f1, f2 = tmp_path / "a.toml", tmp_path / "b.toml"
    with patch("subprocess.run", side_effect=FileNotFoundError):
        with pytest.raises(SystemExit) as exc:
            NewConfigs.vimdiff(f1, f2)
    assert exc.value.code == 1


def test_vimdiff_called_process_error_raises_systemexit(tmp_path: Path) -> None:
    f1, f2 = tmp_path / "a.toml", tmp_path / "b.toml"
    with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "vimdiff")):
        with pytest.raises(SystemExit) as exc:
            NewConfigs.vimdiff(f1, f2)
    assert exc.value.code == 1


def test_vimdiff_success_calls_subprocess(tmp_path: Path) -> None:
    f1, f2 = tmp_path / "a.toml", tmp_path / "b.toml"
    with patch("subprocess.run") as mock_run:
        NewConfigs.vimdiff(f1, f2)
    mock_run.assert_called_once_with(["vimdiff", str(f1), str(f2)], check=True)


# ---------------------------------------------------------------------------
# menu — dispatch
# ---------------------------------------------------------------------------


def test_menu_dispatches_k_to_keep(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    with patch("builtins.input", return_value="k"):
        obj.menu()
    out = capsys.readouterr().out
    assert "No changes were made" in out


def test_menu_dispatches_r_to_remove(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.remove = MagicMock()
    with patch("builtins.input", return_value="r"):
        obj.menu()
    obj.remove.assert_called_once()


def test_menu_invalid_choice_defaults_to_keep(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    with patch("builtins.input", return_value="z"):
        obj.menu()
    out = capsys.readouterr().out
    assert "No changes were made" in out


def test_menu_dispatches_m_to_merge(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.merge = MagicMock()
    with patch("builtins.input", return_value="m"):
        obj.menu()
    obj.merge.assert_called_once()


# ---------------------------------------------------------------------------
# _merge_toml
# ---------------------------------------------------------------------------

_CURRENT_TOML = """\
[behavior]
ASK_QUESTION = true
COLORS = true
OLD_KEY = "deprecated"
"""

_NEW_TOML_ADDS_KEY = """\
[behavior]
ASK_QUESTION = true
COLORS = true
ALL_REPOS_CONFIRM = "once"

[verification]
VERIFY_SKIP_VIRTUAL = ["dev/", "proc/"]
"""

_NEW_TOML_NO_CHANGES = """\
[behavior]
ASK_QUESTION = true
COLORS = true
OLD_KEY = "deprecated"
"""

_NEW_TOML_REMOVES_KEY = """\
[behavior]
ASK_QUESTION = true
COLORS = true
"""


def test_merge_toml_returns_added_and_removed_keys(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    added, removed = obj._merge_toml(current, new)

    assert "behavior.ALL_REPOS_CONFIRM" in added
    assert "[verification]" in added
    assert "behavior.OLD_KEY" in removed


def test_merge_toml_preserves_existing_values(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._merge_toml(current, new)

    result = tomlkit.parse(current.read_text(encoding="utf-8"))
    assert result["behavior"]["ASK_QUESTION"] is True
    assert result["behavior"]["COLORS"] is True


def test_merge_toml_adds_new_key(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._merge_toml(current, new)

    result = tomlkit.parse(current.read_text(encoding="utf-8"))
    assert result["behavior"]["ALL_REPOS_CONFIRM"] == "once"
    assert "verification" in result


def test_merge_toml_removes_obsolete_key(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_REMOVES_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._merge_toml(current, new)

    result = tomlkit.parse(current.read_text(encoding="utf-8"))
    assert "OLD_KEY" not in result["behavior"]


def test_merge_toml_creates_orig_backup(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._merge_toml(current, new)

    assert (tmp_path / "slpkg.toml.orig").exists()


def test_merge_toml_no_changes_returns_empty_tuples(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_NO_CHANGES, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    added, removed = obj._merge_toml(current, new)

    assert added == []
    assert removed == []


def test_merge_toml_no_changes_no_backup_created(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_NO_CHANGES, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._merge_toml(current, new)

    assert not (tmp_path / "slpkg.toml.orig").exists()


# ---------------------------------------------------------------------------
# _compute_merge_diff
# ---------------------------------------------------------------------------


def test_compute_merge_diff_returns_additions_and_removals(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    additions, removals = obj._compute_merge_diff(current, new)

    assert "behavior.ALL_REPOS_CONFIRM" in additions
    assert "[verification]" in additions
    assert "behavior.OLD_KEY" in removals


def test_compute_merge_diff_returns_empty_when_identical(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    current.write_text(_CURRENT_TOML, encoding="utf-8")
    new.write_text(_NEW_TOML_NO_CHANGES, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    additions, removals = obj._compute_merge_diff(current, new)

    assert additions == []
    assert removals == []


def test_compute_merge_diff_does_not_write_files(tmp_path: Path) -> None:
    current = tmp_path / "slpkg.toml"
    new = tmp_path / "slpkg.toml.new"
    original_content = _CURRENT_TOML
    current.write_text(original_content, encoding="utf-8")
    new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    obj = make_new_configs(tmp_path)

    obj._compute_merge_diff(current, new)

    assert current.read_text(encoding="utf-8") == original_content
    assert not (tmp_path / "slpkg.toml.orig").exists()


# ---------------------------------------------------------------------------
# merge_config_file / merge_repositories_file / merge_blacklist_file
# ---------------------------------------------------------------------------


def test_merge_config_file_removes_new_file_after_merge(tmp_path: Path, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.slpkg_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "y")

    obj.merge_config_file()

    assert not obj.slpkg_config_new.exists()
    assert (tmp_path / "slpkg.toml.new.orig").exists()


def test_merge_config_file_moves_new_when_current_missing(tmp_path: Path) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")

    obj.merge_config_file()

    assert obj.slpkg_config.exists()
    assert not obj.slpkg_config_new.exists()


def test_merge_config_file_prints_added_keys(tmp_path: Path, capsys, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.slpkg_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "y")

    obj.merge_config_file()

    out = capsys.readouterr().out
    assert "Merged" in out
    assert "ALL_REPOS_CONFIRM" in out


def test_merge_config_file_up_to_date_message(tmp_path: Path, capsys) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.slpkg_config_new.write_text(_NEW_TOML_NO_CHANGES, encoding="utf-8")

    obj.merge_config_file()

    out = capsys.readouterr().out
    assert "up-to-date" in out


def test_merge_repositories_file_removes_new_file(tmp_path: Path, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.repositories_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.repositories_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "y")

    obj.merge_repositories_file()

    assert not obj.repositories_config_new.exists()
    assert (tmp_path / "repositories.toml.new.orig").exists()


def test_merge_blacklist_file_removes_new_file(tmp_path: Path, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.blacklist_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.blacklist_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "y")

    obj.merge_blacklist_file()

    assert not obj.blacklist_config_new.exists()
    assert (tmp_path / "blacklist.toml.new.orig").exists()


def test_merge_config_file_cancelled_leaves_files_unchanged(tmp_path: Path, capsys, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.slpkg_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.slpkg_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "n")

    obj.merge_config_file()

    assert obj.slpkg_config_new.exists()
    assert obj.slpkg_config.read_text(encoding="utf-8") == _CURRENT_TOML
    out = capsys.readouterr().out
    assert "cancelled" in out.lower()


def test_merge_repositories_file_cancelled_leaves_files_unchanged(tmp_path: Path, capsys, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.repositories_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.repositories_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "n")

    obj.merge_repositories_file()

    assert obj.repositories_config_new.exists()
    assert obj.repositories_config.read_text(encoding="utf-8") == _CURRENT_TOML
    out = capsys.readouterr().out
    assert "cancelled" in out.lower()


def test_merge_blacklist_file_cancelled_leaves_files_unchanged(tmp_path: Path, capsys, monkeypatch) -> None:
    obj = make_new_configs(tmp_path)
    obj.blacklist_config.write_text(_CURRENT_TOML, encoding="utf-8")
    obj.blacklist_config_new.write_text(_NEW_TOML_ADDS_KEY, encoding="utf-8")
    monkeypatch.setattr("builtins.input", lambda _: "n")

    obj.merge_blacklist_file()

    assert obj.blacklist_config_new.exists()
    assert obj.blacklist_config.read_text(encoding="utf-8") == _CURRENT_TOML
    out = capsys.readouterr().out
    assert "cancelled" in out.lower()
