#!/usr/bin/python3
"""Tests for slpkg.config_editor.FormConfigs."""

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

import pytest
import tomlkit

from slpkg.config_editor import FormConfigs


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

SAMPLE_TOML = """\
[SLPKG]
colors = true
editor = "vim"
dialog = false
parallel_downloads = 2
"""

FLAT_CONFIG = {
    "colors": True,
    "editor": "vim",
    "dialog": False,
    "parallel_downloads": 2,
}


def make_form_configs(tmp_path: Path, *, edit: bool = False, dialog: bool = False) -> FormConfigs:
    """Construct FormConfigs bypassing __init__ (avoids config_load)."""
    obj: FormConfigs = object.__new__(FormConfigs)
    obj.dialog = dialog
    obj.etc_path = tmp_path
    obj.prog_name = "slpkg"
    obj.config = dict(FLAT_CONFIG)
    obj.editor = "vim"
    obj.config_file = tmp_path / "slpkg.toml"
    obj.option_for_edit = edit
    obj.original_configs = {}

    obj.dialogbox = MagicMock()
    obj.errors = MagicMock()
    obj.errors.message.side_effect = SystemExit(1)
    return obj


# ---------------------------------------------------------------------------
# read_configs
# ---------------------------------------------------------------------------


def test_read_configs_loads_toml(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    assert "SLPKG" in obj.original_configs
    assert obj.original_configs["SLPKG"]["editor"] == "vim"


def test_read_configs_missing_file_calls_errors(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    # config_file does not exist
    with pytest.raises(SystemExit):
        obj.read_configs()
    obj.errors.message.assert_called_once()


# ---------------------------------------------------------------------------
# write_configs — type coercion
# ---------------------------------------------------------------------------


def test_write_configs_writes_bool_true(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    # colors key is index 0 in FLAT_CONFIG
    tags = ["true", "vim", "false", "2"]
    obj.write_configs(tags)
    result = tomlkit.parse(obj.config_file.read_text(encoding="utf-8"))
    assert result["SLPKG"]["colors"] is True


def test_write_configs_writes_bool_false(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    tags = ["false", "vim", "false", "2"]
    obj.write_configs(tags)
    result = tomlkit.parse(obj.config_file.read_text(encoding="utf-8"))
    assert result["SLPKG"]["colors"] is False


def test_write_configs_writes_integer(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    tags = ["true", "vim", "false", "8"]
    obj.write_configs(tags)
    result = tomlkit.parse(obj.config_file.read_text(encoding="utf-8"))
    assert result["SLPKG"]["parallel_downloads"] == 8


def test_write_configs_writes_string(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    tags = ["true", "nano", "false", "2"]
    obj.write_configs(tags)
    result = tomlkit.parse(obj.config_file.read_text(encoding="utf-8"))
    assert result["SLPKG"]["editor"] == "nano"


def test_write_configs_parses_list_value(tmp_path: Path) -> None:
    toml_with_list = "[SLPKG]\npackage_type = ['.txz']\n"
    obj = make_form_configs(tmp_path)
    obj.config = {"package_type": [".txz"]}
    obj.config_file.write_text(toml_with_list, encoding="utf-8")
    obj.read_configs()
    tags = ["['.txz', '.tgz']"]
    obj.write_configs(tags)
    result = tomlkit.parse(obj.config_file.read_text(encoding="utf-8"))
    assert ".tgz" in result["SLPKG"]["package_type"]


def test_write_configs_invalid_list_calls_errors(tmp_path: Path) -> None:
    toml_with_list = "[SLPKG]\npackage_type = ['.txz']\n"
    obj = make_form_configs(tmp_path)
    obj.config = {"package_type": [".txz"]}
    obj.config_file.write_text(toml_with_list, encoding="utf-8")
    obj.read_configs()
    # Matches regex ^\s*\[.*\]\s*$ but ast.literal_eval fails (bare name 'broken')
    with pytest.raises(SystemExit):
        obj.write_configs(["[1, broken]"])
    obj.errors.message.assert_called_once()


def test_write_configs_extra_tags_ignored(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    obj.read_configs()
    # More tags than config keys — extras must be silently ignored
    tags = ["true", "vim", "false", "2", "extra1", "extra2"]
    obj.write_configs(tags)  # must not raise


# ---------------------------------------------------------------------------
# _coerce_value
# ---------------------------------------------------------------------------


def test_coerce_value_true(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "true") is True


def test_coerce_value_false(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "false") is False


def test_coerce_value_bool_case_insensitive(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "True") is True
    assert obj._coerce_value("k", "FALSE") is False


def test_coerce_value_integer(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "42") == 42
    assert isinstance(obj._coerce_value("k", "42"), int)


def test_coerce_value_float(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "3.14") == pytest.approx(3.14)
    assert isinstance(obj._coerce_value("k", "3.14"), float)


def test_coerce_value_negative_integer(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "-5") == -5


def test_coerce_value_list(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    result = obj._coerce_value("k", "['.txz', '.tgz']")
    assert result == [".txz", ".tgz"]


def test_coerce_value_string_passthrough(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    assert obj._coerce_value("k", "nano") == "nano"


def test_coerce_value_invalid_list_calls_errors(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    with pytest.raises(SystemExit):
        obj._coerce_value("k", "[1, broken]")
    obj.errors.message.assert_called_once()


# ---------------------------------------------------------------------------
# _update_section
# ---------------------------------------------------------------------------


def test_update_section_updates_existing_key(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.original_configs = {"SLPKG": {"editor": "vim", "colors": True}}
    assert obj._update_section("editor", "nano") is True
    assert obj.original_configs["SLPKG"]["editor"] == "nano"


def test_update_section_returns_false_for_missing_key(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.original_configs = {"SLPKG": {"editor": "vim"}}
    assert obj._update_section("nonexistent", "value") is False


def test_update_section_skips_non_dict_sections(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.original_configs = {"META": "not-a-dict", "SLPKG": {"editor": "vim"}}
    assert obj._update_section("editor", "nano") is True
    assert obj.original_configs["SLPKG"]["editor"] == "nano"


# ---------------------------------------------------------------------------
# editor_config
# ---------------------------------------------------------------------------


def test_editor_config_opens_editor_and_exits(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.config_file.touch()
    with patch("subprocess.run") as mock_run:
        with pytest.raises(SystemExit) as exc:
            obj.editor_config()
    assert exc.value.code == 0
    mock_run.assert_called_once()


def test_editor_config_no_editor_calls_errors(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.editor = ""
    with patch.dict("os.environ", {}, clear=True):
        with pytest.raises(SystemExit):
            obj.editor_config()
    obj.errors.message.assert_called_once()


def test_editor_config_file_not_found_calls_errors(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    with patch("subprocess.run", side_effect=FileNotFoundError):
        with pytest.raises(SystemExit):
            obj.editor_config()
    obj.errors.message.assert_called_once()


def test_editor_config_subprocess_error_calls_errors(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "vim")):
        with pytest.raises(SystemExit):
            obj.editor_config()
    obj.errors.message.assert_called_once()


def test_editor_config_uses_env_editor_when_not_configured(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path)
    obj.editor = ""
    obj.config_file.touch()
    with patch.dict("os.environ", {"EDITOR": "nano"}):
        with patch("subprocess.run") as mock_run:
            with pytest.raises(SystemExit):
                obj.editor_config()
    cmd = mock_run.call_args[0][0]
    assert cmd[0] == "nano"


# ---------------------------------------------------------------------------
# dialog_edit — dispatch
# ---------------------------------------------------------------------------


def test_dialog_edit_ok_calls_write_configs(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=True)
    obj.dialogbox.mixedform.return_value = ("ok", ["true", "vim", "false", "2"])
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    with patch("os.system"):
        obj.dialog_edit()
    # write_configs was reached — config_file was rewritten
    assert obj.config_file.exists()


def test_dialog_edit_cancel_does_not_write(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=True)
    obj.dialogbox.mixedform.return_value = ("cancel", [])
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    original_mtime = obj.config_file.stat().st_mtime
    with patch("os.system"):
        obj.dialog_edit()
    assert obj.config_file.stat().st_mtime == original_mtime


def test_dialog_edit_help_calls_help_method(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=True)
    # First call returns "help", second call returns "cancel" to break recursion
    obj.dialogbox.mixedform.side_effect = [("help", []), ("cancel", [])]
    obj.config_file.write_text(SAMPLE_TOML, encoding="utf-8")
    with patch("os.system"):
        obj.dialog_edit()
    obj.dialogbox.textbox.assert_called_once()


# ---------------------------------------------------------------------------
# run — dispatch
# ---------------------------------------------------------------------------


def test_run_no_dialog_calls_editor_config(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=False)
    obj.config_file.touch()
    with patch("subprocess.run"):
        with pytest.raises(SystemExit):
            obj.run()


def test_run_dialog_without_edit_option_calls_dialog_edit(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=True, edit=False)
    obj.dialogbox.mixedform.return_value = ("cancel", [])
    with patch("os.system"):
        obj.run()
    obj.dialogbox.mixedform.assert_called_once()


def test_run_dialog_with_edit_option_calls_editor_first(tmp_path: Path) -> None:
    obj = make_form_configs(tmp_path, dialog=True, edit=True)
    obj.config_file.touch()
    obj.dialogbox.mixedform.return_value = ("cancel", [])
    with patch("subprocess.run"), patch("os.system"):
        with pytest.raises(SystemExit):
            obj.run()
