"""Unit tests for ConfigLoader."""

from __future__ import annotations

import textwrap
from pathlib import Path

import pytest
import yaml

from tui.config.loader import ConfigLoader


# ── conf file parsing ──────────────────────────────────────────────


class TestLoadConfFrom:
    """Tests for _load_conf_from static method."""

    def test_parses_key_value_pairs(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text('FOO="bar"\nBAZ=123\n')
        result = ConfigLoader._load_conf_from(conf)
        assert result == {"FOO": "bar", "BAZ": "123"}

    def test_strips_double_quotes(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text('KEY="value"\n')
        assert ConfigLoader._load_conf_from(conf)["KEY"] == "value"

    def test_strips_single_quotes(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text("KEY='value'\n")
        assert ConfigLoader._load_conf_from(conf)["KEY"] == "value"

    def test_ignores_comments(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text("# comment\nKEY=val\n")
        result = ConfigLoader._load_conf_from(conf)
        assert "# comment" not in result
        assert result == {"KEY": "val"}

    def test_ignores_blank_lines(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text("\n\nKEY=val\n\n")
        assert ConfigLoader._load_conf_from(conf) == {"KEY": "val"}

    def test_handles_equals_in_value(self, tmp_path: Path) -> None:
        conf = tmp_path / "test.conf"
        conf.write_text('CONN="host=localhost port=5432"\n')
        assert ConfigLoader._load_conf_from(conf)["CONN"] == "host=localhost port=5432"

    def test_returns_empty_for_missing_file(self, tmp_path: Path) -> None:
        assert ConfigLoader._load_conf_from(tmp_path / "nope.conf") == {}


# ── deep merge ─────────────────────────────────────────────────────


class TestDeepMerge:
    """Tests for _deep_merge static method."""

    def test_override_replaces_scalar(self) -> None:
        base = {"a": 1}
        result = ConfigLoader._deep_merge(base, {"a": 2})
        assert result["a"] == 2

    def test_override_adds_new_keys(self) -> None:
        base = {"a": 1}
        result = ConfigLoader._deep_merge(base, {"b": 2})
        assert result == {"a": 1, "b": 2}

    def test_nested_dicts_are_merged(self) -> None:
        base = {"d": {"x": 1, "y": 2}}
        result = ConfigLoader._deep_merge(base, {"d": {"y": 3, "z": 4}})
        assert result == {"d": {"x": 1, "y": 3, "z": 4}}

    def test_list_replaces_not_merges(self) -> None:
        base = {"items": [1, 2, 3]}
        result = ConfigLoader._deep_merge(base, {"items": [4, 5]})
        assert result["items"] == [4, 5]

    def test_empty_override_is_noop(self) -> None:
        base = {"a": 1}
        result = ConfigLoader._deep_merge(base, {})
        assert result == {"a": 1}

    def test_deeply_nested_merge(self) -> None:
        base = {"a": {"b": {"c": 1, "d": 2}}}
        result = ConfigLoader._deep_merge(base, {"a": {"b": {"c": 99}}})
        assert result == {"a": {"b": {"c": 99, "d": 2}}}


# ── distributions loading ──────────────────────────────────────────


class TestDistributions:
    """Tests for distributions property and helpers."""

    def test_loads_bundled_distributions(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        distros = loader.distributions["distributions"]
        assert "ubuntu" in distros
        assert "rocky" in distros

    def test_user_override_adds_distro(
        self, tmp_config_dir: Path, tmp_user_config_dir: Path
    ) -> None:
        user_data = {
            "distributions": {
                "arch": {
                    "family": "arch",
                    "display_name": "Arch Linux",
                    "versions": {"rolling": {"name": "Arch Rolling"}},
                }
            }
        }
        with open(tmp_user_config_dir / "distributions.yml", "w") as f:
            yaml.dump(user_data, f)

        loader = ConfigLoader(
            config_dir=tmp_config_dir, user_config_dir=tmp_user_config_dir
        )
        distros = loader.distributions["distributions"]
        assert "arch" in distros
        assert "ubuntu" in distros  # originals preserved

    def test_user_override_adds_version(
        self, tmp_config_dir: Path, tmp_user_config_dir: Path
    ) -> None:
        user_data = {
            "distributions": {
                "ubuntu": {
                    "versions": {
                        "25.04": {"name": "Ubuntu 25.04"},
                    }
                }
            }
        }
        with open(tmp_user_config_dir / "distributions.yml", "w") as f:
            yaml.dump(user_data, f)

        loader = ConfigLoader(
            config_dir=tmp_config_dir, user_config_dir=tmp_user_config_dir
        )
        versions = loader.distributions["distributions"]["ubuntu"]["versions"]
        assert "25.04" in versions
        assert "22.04" in versions  # original preserved

    def test_get_distro_list(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        result = loader.get_distro_list()
        ids = [distro_id for distro_id, _ in result]
        assert "ubuntu" in ids
        assert "rocky" in ids

    def test_get_distro_list_display_names(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        result = dict(loader.get_distro_list())
        assert result["ubuntu"] == "Ubuntu"
        assert result["rocky"] == "Rocky Linux"

    def test_get_version_list(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        versions = loader.get_version_list("ubuntu")
        ids = [v_id for v_id, _ in versions]
        assert "22.04" in ids
        assert "24.04" in ids

    def test_get_version_list_unknown_distro(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_version_list("nonexistent") == []

    def test_get_version_info(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        info = loader.get_version_info("ubuntu", "22.04")
        assert info["name"] == "Ubuntu 22.04 LTS"
        assert "iso_url" in info

    def test_get_version_info_missing(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_version_info("ubuntu", "99.99") == {}

    def test_get_distro_family(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_distro_family("ubuntu") == "debian"
        assert loader.get_distro_family("rocky") == "rhel"

    def test_get_distro_family_unknown(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_distro_family("nope") == "unknown"


# ── defaults loading ───────────────────────────────────────────────


class TestDefaults:
    """Tests for defaults property and helpers."""

    def test_loads_bundled_defaults(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.defaults["DEFAULT_CLOUDINIT_USER"] == "admin"
        assert loader.defaults["DEFAULT_VM_CORES"] == "2"

    def test_user_override_replaces_key(
        self, tmp_config_dir: Path, tmp_user_config_dir: Path
    ) -> None:
        (tmp_user_config_dir / "defaults.conf").write_text(
            'DEFAULT_VM_CORES=8\n'
        )
        loader = ConfigLoader(
            config_dir=tmp_config_dir, user_config_dir=tmp_user_config_dir
        )
        assert loader.defaults["DEFAULT_VM_CORES"] == "8"
        # Non-overridden keys still present
        assert loader.defaults["DEFAULT_CLOUDINIT_USER"] == "admin"

    def test_user_override_adds_key(
        self, tmp_config_dir: Path, tmp_user_config_dir: Path
    ) -> None:
        (tmp_user_config_dir / "defaults.conf").write_text(
            'CUSTOM_KEY="custom_value"\n'
        )
        loader = ConfigLoader(
            config_dir=tmp_config_dir, user_config_dir=tmp_user_config_dir
        )
        assert loader.defaults["CUSTOM_KEY"] == "custom_value"

    def test_get_default(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_default("DEFAULT_CLOUDINIT_USER") == "admin"

    def test_get_default_fallback(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_default("NONEXISTENT", "fallback") == "fallback"

    def test_get_default_int(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_default_int("DEFAULT_VM_CORES") == 2
        assert loader.get_default_int("DEFAULT_VM_MEMORY") == 2048

    def test_get_default_int_invalid(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_default_int("DEFAULT_CLOUDINIT_USER", 42) == 42

    def test_get_default_int_missing(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        assert loader.get_default_int("NOPE", 99) == 99


# ── package loading ────────────────────────────────────────────────


class TestPackages:
    """Tests for package YAML loading."""

    def test_loads_base_packages(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        pkgs = loader.base_packages
        assert "apt_packages" in pkgs
        assert "curl" in pkgs["apt_packages"]

    def test_loads_extended_packages(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        pkgs = loader.extended_packages
        assert "docker.io" in pkgs["apt_packages"]

    def test_user_replaces_package_list(
        self, tmp_config_dir: Path, tmp_user_config_dir: Path
    ) -> None:
        pkg_dir = tmp_user_config_dir / "packages"
        pkg_dir.mkdir()
        user_pkgs = {"apt_packages": ["nginx", "redis"]}
        with open(pkg_dir / "base-packages.yml", "w") as f:
            yaml.dump(user_pkgs, f)

        loader = ConfigLoader(
            config_dir=tmp_config_dir, user_config_dir=tmp_user_config_dir
        )
        pkgs = loader.base_packages
        # apt_packages entirely replaced
        assert pkgs["apt_packages"] == ["nginx", "redis"]
        # dnf_packages untouched (user didn't override it)
        assert pkgs["dnf_packages"] == ["curl", "git", "wget"]


# ── user config dir resolution ─────────────────────────────────────


class TestUserConfigDir:
    """Tests for user config directory resolution."""

    def test_explicit_user_config_dir(self, tmp_path: Path) -> None:
        loader = ConfigLoader(
            config_dir=tmp_path, user_config_dir=tmp_path / "custom"
        )
        assert loader.user_config_dir == tmp_path / "custom"

    def test_disabled_user_config_dir(self, tmp_path: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_path, user_config_dir="")
        assert loader.user_config_dir is None

    def test_env_var_override(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("PROXMOX_BUILDER_CONFIG_DIR", str(tmp_path / "env"))
        loader = ConfigLoader(config_dir=tmp_path, user_config_dir=None)
        assert loader.user_config_dir == tmp_path / "env"

    def test_xdg_config_home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PROXMOX_BUILDER_CONFIG_DIR", raising=False)
        monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
        loader = ConfigLoader(config_dir=tmp_path, user_config_dir=None)
        assert loader.user_config_dir == tmp_path / "xdg" / "proxmox-template-builder"

    def test_default_home_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("PROXMOX_BUILDER_CONFIG_DIR", raising=False)
        monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
        monkeypatch.setenv("HOME", str(tmp_path))
        loader = ConfigLoader(config_dir=tmp_path, user_config_dir=None)
        assert loader.user_config_dir == tmp_path / ".config" / "proxmox-template-builder"


# ── caching ────────────────────────────────────────────────────────


class TestCaching:
    """Verify config properties are cached after first load."""

    def test_distributions_cached(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        first = loader.distributions
        second = loader.distributions
        assert first is second

    def test_defaults_cached(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        first = loader.defaults
        second = loader.defaults
        assert first is second

    def test_base_packages_cached(self, tmp_config_dir: Path) -> None:
        loader = ConfigLoader(config_dir=tmp_config_dir, user_config_dir="")
        first = loader.base_packages
        second = loader.base_packages
        assert first is second
