"""Tests for dekk.cli.errors -- exception hierarchy and exit codes."""

from __future__ import annotations

import builtins

import pytest

from dekk.cli.errors import (
    ConfigError,
    DekkError,
    DekkPermissionError,
    DekkRuntimeError,
    DekkTimeoutError,
    DependencyError,
    ExitCodes,
    NotFoundError,
    ValidationError,
)

# ---------------------------------------------------------------------------
# ExitCodes Enum
# ---------------------------------------------------------------------------


class TestExitCodes:
    """Tests for the ExitCodes IntEnum."""

    def test_all_codes_present(self):
        expected = {
            "SUCCESS",
            "GENERAL_ERROR",
            "VALIDATION_ERROR",
            "NOT_FOUND",
            "PERMISSION_ERROR",
            "TIMEOUT",
            "CONFIG_ERROR",
            "DEPENDENCY_ERROR",
            "RUNTIME_ERROR",
            "INTERRUPTED",
        }
        assert expected == {c.name for c in ExitCodes}

    def test_values_are_sequential(self):
        values = [c.value for c in ExitCodes]
        assert values == list(range(10))

    def test_success_is_zero(self):
        assert ExitCodes.SUCCESS == 0

    def test_is_int(self):
        for code in ExitCodes:
            assert isinstance(code, int)

    def test_can_use_as_exit_code(self):
        assert int(ExitCodes.GENERAL_ERROR) == 1


# ---------------------------------------------------------------------------
# DekkError (base class)
# ---------------------------------------------------------------------------


class TestDekkError:
    """Tests for the base DekkError exception."""

    def test_is_exception(self):
        assert issubclass(DekkError, Exception)

    def test_message_stored(self):
        err = DekkError("Something went wrong")
        assert err.message == "Something went wrong"

    def test_hint_none_by_default(self):
        err = DekkError("error")
        assert err.hint is None

    def test_hint_stored(self):
        err = DekkError("error", hint="Try this")
        assert err.hint == "Try this"

    def test_default_exit_code(self):
        err = DekkError("error")
        assert err.exit_code == ExitCodes.GENERAL_ERROR

    def test_details_stored(self):
        err = DekkError("error", path="/tmp", count=42)
        assert err.details == {"path": "/tmp", "count": 42}

    def test_details_empty_by_default(self):
        err = DekkError("error")
        assert err.details == {}

    def test_str_is_message(self):
        err = DekkError("Something went wrong")
        assert str(err) == "Something went wrong"

    def test_can_be_raised_and_caught(self):
        with pytest.raises(DekkError, match="boom"):
            raise DekkError("boom")

    def test_to_dict_basic(self):
        err = DekkError("broken", hint="Fix it")
        d = err.to_dict()
        assert d["error"] == "DekkError"
        assert d["message"] == "broken"
        assert d["hint"] == "Fix it"
        assert d["exit_code"] == 1

    def test_to_dict_includes_details(self):
        err = DekkError("broken", searched_paths=["/a", "/b"])
        d = err.to_dict()
        assert d["searched_paths"] == ["/a", "/b"]

    def test_to_dict_hint_none(self):
        err = DekkError("broken")
        d = err.to_dict()
        assert d["hint"] is None

    def test_to_dict_no_extra_keys(self):
        err = DekkError("broken")
        assert (
            set(d.keys()) == {"error", "message", "hint", "exit_code"}
            if not (d := err.to_dict())
            else True
        )
        d = err.to_dict()
        assert "error" in d
        assert "message" in d
        assert "hint" in d
        assert "exit_code" in d


# ---------------------------------------------------------------------------
# Subclass exit codes
# ---------------------------------------------------------------------------


class TestSubclassExitCodes:
    """Tests that each subclass has the correct exit_code."""

    @pytest.mark.parametrize(
        "cls, expected_code",
        [
            (NotFoundError, ExitCodes.NOT_FOUND),
            (ValidationError, ExitCodes.VALIDATION_ERROR),
            (ConfigError, ExitCodes.CONFIG_ERROR),
            (DependencyError, ExitCodes.DEPENDENCY_ERROR),
            (DekkTimeoutError, ExitCodes.TIMEOUT),
            (DekkPermissionError, ExitCodes.PERMISSION_ERROR),
            (DekkRuntimeError, ExitCodes.RUNTIME_ERROR),
        ],
    )
    def test_exit_code(self, cls, expected_code):
        err = cls("test")
        assert err.exit_code == expected_code

    @pytest.mark.parametrize(
        "cls, expected_int",
        [
            (NotFoundError, 3),
            (ValidationError, 2),
            (ConfigError, 6),
            (DependencyError, 7),
            (DekkTimeoutError, 5),
            (DekkPermissionError, 4),
            (DekkRuntimeError, 8),
        ],
    )
    def test_exit_code_int_value(self, cls, expected_int):
        err = cls("test")
        assert int(err.exit_code) == expected_int


# ---------------------------------------------------------------------------
# Subclass inheritance
# ---------------------------------------------------------------------------


class TestSubclassInheritance:
    """Tests that all subclasses inherit from DekkError."""

    @pytest.mark.parametrize(
        "cls",
        [
            NotFoundError,
            ValidationError,
            ConfigError,
            DependencyError,
            DekkTimeoutError,
            DekkPermissionError,
            DekkRuntimeError,
        ],
    )
    def test_is_dekk_error(self, cls):
        assert issubclass(cls, DekkError)

    @pytest.mark.parametrize(
        "cls",
        [
            NotFoundError,
            ValidationError,
            ConfigError,
            DependencyError,
            DekkTimeoutError,
            DekkPermissionError,
            DekkRuntimeError,
        ],
    )
    def test_catchable_as_dekk_error(self, cls):
        with pytest.raises(DekkError):
            raise cls("test")


# ---------------------------------------------------------------------------
# to_dict on subclasses
# ---------------------------------------------------------------------------


class TestSubclassToDict:
    """Tests for to_dict on error subclasses."""

    def test_not_found_to_dict(self):
        err = NotFoundError("Environment 'ml' not found", hint="Create it", searched_paths=["/opt"])
        d = err.to_dict()
        assert d["error"] == "NotFoundError"
        assert d["message"] == "Environment 'ml' not found"
        assert d["hint"] == "Create it"
        assert d["exit_code"] == 3
        assert d["searched_paths"] == ["/opt"]

    def test_validation_to_dict(self):
        err = ValidationError("Invalid format", field="name")
        d = err.to_dict()
        assert d["error"] == "ValidationError"
        assert d["field"] == "name"
        assert d["exit_code"] == 2

    def test_config_to_dict(self):
        err = ConfigError("Missing key", hint="Add 'database.path' to config.toml")
        d = err.to_dict()
        assert d["error"] == "ConfigError"
        assert d["exit_code"] == 6

    def test_dependency_to_dict(self):
        err = DependencyError("tomli not installed", package="tomli")
        d = err.to_dict()
        assert d["error"] == "DependencyError"
        assert d["package"] == "tomli"

    def test_timeout_to_dict(self):
        err = DekkTimeoutError("Request timed out", timeout_seconds=30)
        d = err.to_dict()
        assert d["error"] == "DekkTimeoutError"
        assert d["timeout_seconds"] == 30

    def test_permission_to_dict(self):
        err = DekkPermissionError("Cannot write", path="/etc/config")
        d = err.to_dict()
        assert d["error"] == "DekkPermissionError"
        assert d["path"] == "/etc/config"

    def test_runtime_to_dict(self):
        err = DekkRuntimeError("Subprocess failed", returncode=127)
        d = err.to_dict()
        assert d["error"] == "DekkRuntimeError"
        assert d["returncode"] == 127


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Edge case tests for errors module."""

    def test_empty_message(self):
        err = DekkError("")
        assert err.message == ""
        assert str(err) == ""

    def test_unicode_message(self):
        err = DekkError("Failed \u2717 badly")
        assert "\u2717" in err.message

    def test_multiple_details(self):
        err = DekkError("err", a=1, b="two", c=[3])
        assert err.details == {"a": 1, "b": "two", "c": [3]}

    def test_detail_with_colliding_key(self):
        """Details whose key collides with to_dict base keys override them."""
        # "error" is set by to_dict but can be overridden via **details
        err = DekkError("msg", error="CustomName")
        d = err.to_dict()
        # result.update(self.details) runs after setting result["error"],
        # so the detail value wins.
        assert d["error"] == "CustomName"

    def test_no_longer_shadows_builtins(self):
        """DekkPermissionError does not shadow builtins.PermissionError."""
        err = DekkPermissionError("denied")
        assert isinstance(err, DekkError)
        assert not isinstance(err, builtins.PermissionError)
