#!/usr/bin/python3
"""Tests for slpkg.config_validate.ConfigValidator."""

import pytest
from pathlib import Path

from slpkg.config_validate import ConfigValidator, SCHEMA


# ---------------------------------------------------------------------------
# Fixture: build a ConfigValidator bypassing __init__ (no config_load needed)
# ---------------------------------------------------------------------------


def make_validator(config_file: Path) -> ConfigValidator:
    """Construct ConfigValidator with a custom config_file path."""
    val: ConfigValidator = object.__new__(ConfigValidator)
    val.config_file = config_file
    val.bold = val.green = val.red = val.yellow = val.grey = val.endc = ""
    return val


# ---------------------------------------------------------------------------
# Minimal valid TOML (all required keys present with correct types/values)
# ---------------------------------------------------------------------------

_VALID_TOML = """\
[logging]
LOGGING_LEVEL = "INFO"

[ui]
COLORS = true
PAGER = "most"
EDITOR = "vim"
DIALOG = false
TERMINAL_SELECTOR = true
QUIET = true
PROGRESS_SPINNER = "spinner"
SPINNER_COLOR = "white"

[packages]
PACKAGE_TYPE = [".txz", ".tgz"]
FILE_LIST_SUFFIX = ".pkgs"
PACKAGE_METHOD = true
DOWNGRADE_PACKAGES = false
VIEW_MISSING_DEPS = false
KERNEL_VERSION = true
BOOTLOADER_COMMAND = ""

[build]
MAKEFLAGS = "-j4"
DELETE_SOURCES = false

[verification]
GPG_VERIFICATION = false
CHECKSUM_MD5 = true
VERIFY_SKIP_VIRTUAL = ["dev/", "proc/"]
VERIFY_SKIP_PREFIXES = ["install/"]
VERIFY_SKIP_SUFFIXES = [".new"]

[download]
DOWNLOADER = "wget"
DOWNLOAD_ONLY_PATH = "/tmp/slpkg/"
PARALLEL_DOWNLOADS = false
MAXIMUM_PARALLEL = 5
WGET_OPTIONS = "-c"
CURL_OPTIONS = ""
ARIA2_OPTIONS = "-c"
LFTP_GET_OPTIONS = "-c get -e"
LFTP_MIRROR_OPTIONS = "-c mirror"
GIT_CLONE = "git clone --depth 1"

[commands]
INSTALLPKG = "upgradepkg --install-new"
LOCAL_INSTALLPKG = "installpkg"
REINSTALL = "upgradepkg --reinstall"
REMOVEPKG = "removepkg"

[behavior]
ASK_QUESTION = true
ALL_REPOS_CONFIRM = "once"
PROCESS_LOG = true

[network]
URLLIB_RETRIES = false
URLLIB_REDIRECT = false
URLLIB_TIMEOUT = 3.0

[proxy]
PROXY_ADDRESS = ""
PROXY_USERNAME = ""
PROXY_PASSWORD = ""
"""


# ---------------------------------------------------------------------------
# _validate(): file-level checks
# ---------------------------------------------------------------------------


def test_file_not_found_returns_error(tmp_path: Path) -> None:
    val = make_validator(tmp_path / "nonexistent.toml")
    errors, warnings = val._validate()
    assert any("not found" in e for e in errors)
    assert not warnings


def test_toml_syntax_error_returns_error(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text("this is not = valid toml = at all !!!\n", encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert any("syntax" in e.lower() or "TOML" in e for e in errors)


def test_empty_file_returns_warning(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text("", encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert not errors
    assert any("empty" in w for w in warnings)


def test_valid_config_no_errors_no_warnings(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(_VALID_TOML, encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert not errors
    assert not warnings


# ---------------------------------------------------------------------------
# _validate(): section-level checks
# ---------------------------------------------------------------------------


def test_unknown_section_is_warning(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(_VALID_TOML + "\n[mysection]\nFOO = 1\n", encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert not errors
    assert any("mysection" in w for w in warnings)


def test_unknown_key_in_known_section_is_warning(tmp_path: Path) -> None:
    toml = _VALID_TOML + "\n"
    toml = toml.replace("[logging]\nLOGGING_LEVEL", "[logging]\nMYSTERY_KEY = 42\nLOGGING_LEVEL")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert not errors
    assert any("MYSTERY_KEY" in w for w in warnings)


def test_missing_key_is_warning(tmp_path: Path) -> None:
    # Remove LOGGING_LEVEL from the file
    toml = _VALID_TOML.replace("LOGGING_LEVEL = \"INFO\"\n", "")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, warnings = val._validate()
    assert not errors
    assert any("LOGGING_LEVEL" in w for w in warnings)


# ---------------------------------------------------------------------------
# _validate_key(): type errors
# ---------------------------------------------------------------------------


def test_bool_field_wrong_type_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("COLORS = true", "COLORS = 1")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("COLORS" in e and "bool" in e for e in errors)


def test_int_field_wrong_type_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("MAXIMUM_PARALLEL = 5", 'MAXIMUM_PARALLEL = "five"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("MAXIMUM_PARALLEL" in e for e in errors)


def test_str_field_wrong_type_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('EDITOR = "vim"', "EDITOR = 42")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("EDITOR" in e for e in errors)


def test_list_field_wrong_type_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('PACKAGE_TYPE = [".txz", ".tgz"]', 'PACKAGE_TYPE = "wrong"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("PACKAGE_TYPE" in e for e in errors)


# ---------------------------------------------------------------------------
# _validate_key(): enum choices
# ---------------------------------------------------------------------------


def test_invalid_logging_level_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('LOGGING_LEVEL = "INFO"', 'LOGGING_LEVEL = "VERBOSE"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("LOGGING_LEVEL" in e and "VERBOSE" in e for e in errors)


def test_invalid_downloader_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('DOWNLOADER = "wget"', 'DOWNLOADER = "fetch"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("DOWNLOADER" in e and "fetch" in e for e in errors)


def test_invalid_progress_spinner_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('PROGRESS_SPINNER = "spinner"', 'PROGRESS_SPINNER = "dots"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("PROGRESS_SPINNER" in e for e in errors)


def test_invalid_spinner_color_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('SPINNER_COLOR = "white"', 'SPINNER_COLOR = "purple"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("SPINNER_COLOR" in e for e in errors)


def test_invalid_all_repos_confirm_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace('ALL_REPOS_CONFIRM = "once"', 'ALL_REPOS_CONFIRM = "always"')
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("ALL_REPOS_CONFIRM" in e for e in errors)


def test_valid_enum_choices_no_error(tmp_path: Path) -> None:
    for level in ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"):
        toml = _VALID_TOML.replace('LOGGING_LEVEL = "INFO"', f'LOGGING_LEVEL = "{level}"')
        cfg = tmp_path / "slpkg.toml"
        cfg.write_text(toml, encoding="utf-8")
        val = make_validator(cfg)
        errors, _ = val._validate()
        assert not any("LOGGING_LEVEL" in e for e in errors), f"False error for level {level}"


# ---------------------------------------------------------------------------
# _validate_key(): minimum value
# ---------------------------------------------------------------------------


def test_maximum_parallel_below_min_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("MAXIMUM_PARALLEL = 5", "MAXIMUM_PARALLEL = 0")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("MAXIMUM_PARALLEL" in e and "minimum" in e for e in errors)


def test_maximum_parallel_at_min_no_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("MAXIMUM_PARALLEL = 5", "MAXIMUM_PARALLEL = 1")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert not any("MAXIMUM_PARALLEL" in e for e in errors)


def test_urllib_timeout_negative_is_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("URLLIB_TIMEOUT = 3.0", "URLLIB_TIMEOUT = -1.0")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert any("URLLIB_TIMEOUT" in e and "minimum" in e for e in errors)


def test_urllib_timeout_zero_no_error(tmp_path: Path) -> None:
    toml = _VALID_TOML.replace("URLLIB_TIMEOUT = 3.0", "URLLIB_TIMEOUT = 0")
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(toml, encoding="utf-8")
    val = make_validator(cfg)
    errors, _ = val._validate()
    assert not any("URLLIB_TIMEOUT" in e for e in errors)


# ---------------------------------------------------------------------------
# _print_report()
# ---------------------------------------------------------------------------


def test_print_report_no_issues(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    val = make_validator(tmp_path / "slpkg.toml")
    val._print_report([], [])
    out = capsys.readouterr().out
    assert "valid" in out.lower()
    assert "FAIL" not in out
    assert "WARN" not in out


def test_print_report_warnings_only(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    val = make_validator(tmp_path / "slpkg.toml")
    val._print_report([], ["some warning"])
    out = capsys.readouterr().out
    assert "WARN" in out
    assert "FAIL" not in out
    assert "usable" in out.lower()


def test_print_report_errors(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    val = make_validator(tmp_path / "slpkg.toml")
    val._print_report(["some error"], [])
    out = capsys.readouterr().out
    assert "ERR" in out
    assert "FAIL" in out


def test_print_report_errors_and_warnings(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
    val = make_validator(tmp_path / "slpkg.toml")
    val._print_report(["an error"], ["a warning"])
    out = capsys.readouterr().out
    assert "ERR" in out
    assert "WARN" in out
    assert "FAIL" in out
    assert "warning(s)" in out


# ---------------------------------------------------------------------------
# run(): exit codes
# ---------------------------------------------------------------------------


def test_run_valid_config_exits_0(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(_VALID_TOML, encoding="utf-8")
    val = make_validator(cfg)
    with pytest.raises(SystemExit) as exc:
        val.run()
    assert exc.value.code == 0


def test_run_invalid_config_exits_1(tmp_path: Path) -> None:
    cfg = tmp_path / "slpkg.toml"
    cfg.write_text(_VALID_TOML.replace('DOWNLOADER = "wget"', 'DOWNLOADER = "badtool"'), encoding="utf-8")
    val = make_validator(cfg)
    with pytest.raises(SystemExit) as exc:
        val.run()
    assert exc.value.code == 1


def test_run_missing_file_exits_1(tmp_path: Path) -> None:
    val = make_validator(tmp_path / "missing.toml")
    with pytest.raises(SystemExit) as exc:
        val.run()
    assert exc.value.code == 1


# ---------------------------------------------------------------------------
# SCHEMA integrity: every section/key in SCHEMA has required fields
# ---------------------------------------------------------------------------


def test_schema_all_rules_have_type() -> None:
    for section, keys in SCHEMA.items():
        for key, rule in keys.items():
            assert "type" in rule, f"[{section}] '{key}' rule is missing 'type'"
            assert isinstance(rule["type"], tuple), f"[{section}] '{key}' type must be a tuple"


def test_schema_choices_are_sets() -> None:
    for section, keys in SCHEMA.items():
        for key, rule in keys.items():
            if "choices" in rule:
                assert isinstance(rule["choices"], set), (
                    f"[{section}] '{key}' choices must be a set"
                )


def test_schema_sections_nonempty() -> None:
    for section, keys in SCHEMA.items():
        assert keys, f"Section [{section}] has no keys in SCHEMA"
