"""Tests for custom exception classes.

This module contains comprehensive tests for all custom exception
classes in the aiokwikset library, including their hierarchy,
attributes, and behavior.
"""

from __future__ import annotations

import pytest

from aiokwikset.errors import (
    AuthenticationError,
    DeviceCommandError,
    DeviceError,
    InvalidActionError,
    InvalidDeviceError,
    InvalidStatusError,
    InvalidUserError,
    KwiksetError,
    MFAChallengeRequired,
    MinimumAccessCodeError,
    PasswordChangeRequired,
    RequestError,
    Unauthenticated,
    UnknownError,
    UserExists,
    UserNotConfirmed,
    UserNotFound,
)

# ============================================================================
# Exception Hierarchy Tests
# ============================================================================


class TestExceptionHierarchy:
    """Tests for exception class hierarchy."""

    def test_kwikset_error_is_base(self) -> None:
        """Test KwiksetError is the base exception."""
        assert issubclass(KwiksetError, Exception)

    def test_request_error_inherits_from_base(self) -> None:
        """Test RequestError inherits from KwiksetError."""
        assert issubclass(RequestError, KwiksetError)

    def test_authentication_error_inherits_from_base(self) -> None:
        """Test AuthenticationError inherits from KwiksetError."""
        assert issubclass(AuthenticationError, KwiksetError)

    def test_device_error_inherits_from_base(self) -> None:
        """Test DeviceError inherits from KwiksetError."""
        assert issubclass(DeviceError, KwiksetError)

    def test_unauthenticated_inherits_from_auth_error(self) -> None:
        """Test Unauthenticated inherits from AuthenticationError."""
        assert issubclass(Unauthenticated, AuthenticationError)

    def test_user_not_found_inherits_from_auth_error(self) -> None:
        """Test UserNotFound inherits from AuthenticationError."""
        assert issubclass(UserNotFound, AuthenticationError)

    def test_user_exists_inherits_from_auth_error(self) -> None:
        """Test UserExists inherits from AuthenticationError."""
        assert issubclass(UserExists, AuthenticationError)

    def test_user_not_confirmed_inherits_from_auth_error(self) -> None:
        """Test UserNotConfirmed inherits from AuthenticationError."""
        assert issubclass(UserNotConfirmed, AuthenticationError)

    def test_password_change_required_inherits_from_auth_error(self) -> None:
        """Test PasswordChangeRequired inherits from AuthenticationError."""
        assert issubclass(PasswordChangeRequired, AuthenticationError)

    def test_mfa_challenge_required_inherits_from_auth_error(self) -> None:
        """Test MFAChallengeRequired inherits from AuthenticationError."""
        assert issubclass(MFAChallengeRequired, AuthenticationError)

    def test_unknown_error_inherits_from_base(self) -> None:
        """Test UnknownError inherits from KwiksetError."""
        assert issubclass(UnknownError, KwiksetError)

    def test_invalid_device_error_inherits_from_device_error(self) -> None:
        """Test InvalidDeviceError inherits from DeviceError."""
        assert issubclass(InvalidDeviceError, DeviceError)

    def test_invalid_user_error_inherits_from_device_error(self) -> None:
        """Test InvalidUserError inherits from DeviceError."""
        assert issubclass(InvalidUserError, DeviceError)

    def test_invalid_action_error_inherits_from_device_error(self) -> None:
        """Test InvalidActionError inherits from DeviceError."""
        assert issubclass(InvalidActionError, DeviceError)

    def test_invalid_status_error_inherits_from_device_error(self) -> None:
        """Test InvalidStatusError inherits from DeviceError."""
        assert issubclass(InvalidStatusError, DeviceError)

    def test_device_command_error_inherits_from_device_error(self) -> None:
        """Test DeviceCommandError inherits from DeviceError."""
        assert issubclass(DeviceCommandError, DeviceError)

    def test_minimum_access_code_error_inherits_from_device_error(self) -> None:
        """Test MinimumAccessCodeError inherits from DeviceError."""
        assert issubclass(MinimumAccessCodeError, DeviceError)

    def test_minimum_access_code_error_inherits_from_kwikset_error(self) -> None:
        """Test MinimumAccessCodeError inherits from KwiksetError."""
        assert issubclass(MinimumAccessCodeError, KwiksetError)


# ============================================================================
# Exception Catching Tests
# ============================================================================


class TestExceptionCatching:
    """Tests for catching exceptions at various hierarchy levels."""

    def test_catch_auth_errors_with_base(self) -> None:
        """Test all auth errors can be caught with AuthenticationError."""
        auth_exceptions = [
            Unauthenticated("test"),
            UserNotFound("test"),
            UserExists("test"),
            UserNotConfirmed("test"),
            PasswordChangeRequired(),
            MFAChallengeRequired(),
        ]

        for exc in auth_exceptions:
            try:
                raise exc
            except AuthenticationError:
                pass  # Expected
            except Exception:
                pytest.fail(f"{type(exc).__name__} should be caught by AuthenticationError")

    def test_catch_device_errors_with_base(self) -> None:
        """Test all device errors can be caught with DeviceError."""
        device_exceptions = [
            InvalidDeviceError(),
            InvalidUserError(),
            InvalidActionError(),
            InvalidStatusError(),
            DeviceCommandError(),
            MinimumAccessCodeError(),
        ]

        for exc in device_exceptions:
            try:
                raise exc
            except DeviceError:
                pass  # Expected
            except Exception:
                pytest.fail(f"{type(exc).__name__} should be caught by DeviceError")

    def test_catch_all_with_kwikset_error(self) -> None:
        """Test all exceptions can be caught with KwiksetError."""
        all_exceptions = [
            KwiksetError("test"),
            RequestError("test"),
            AuthenticationError("test"),
            Unauthenticated("test"),
            UserNotFound("test"),
            DeviceError("test"),
            InvalidDeviceError(),
            UnknownError("test"),
        ]

        for exc in all_exceptions:
            try:
                raise exc
            except KwiksetError:
                pass  # Expected
            except Exception:
                pytest.fail(f"{type(exc).__name__} should be caught by KwiksetError")


# ============================================================================
# KwiksetError Tests
# ============================================================================


class TestKwiksetError:
    """Tests for KwiksetError base class."""

    def test_kwikset_error_with_message(self) -> None:
        """Test KwiksetError with message."""
        error = KwiksetError("Test error message")

        assert str(error) == "Test error message"

    def test_kwikset_error_no_message(self) -> None:
        """Test KwiksetError without message."""
        error = KwiksetError()

        assert str(error) == ""


# ============================================================================
# RequestError Tests
# ============================================================================


class TestRequestError:
    """Tests for RequestError class."""

    def test_request_error_with_message(self) -> None:
        """Test RequestError with message."""
        error = RequestError("Connection failed")

        assert str(error) == "Connection failed"
        assert isinstance(error, KwiksetError)


# ============================================================================
# PasswordChangeRequired Tests
# ============================================================================


class TestPasswordChangeRequired:
    """Tests for PasswordChangeRequired class."""

    def test_default_message(self) -> None:
        """Test PasswordChangeRequired has default message."""
        error = PasswordChangeRequired()

        assert "Password change required" in str(error)

    def test_custom_message(self) -> None:
        """Test PasswordChangeRequired with custom message."""
        error = PasswordChangeRequired("Please reset your password")

        assert str(error) == "Please reset your password"


# ============================================================================
# MFAChallengeRequired Tests
# ============================================================================


class TestMFAChallengeRequired:
    """Tests for MFAChallengeRequired class."""

    def test_default_values(self) -> None:
        """Test MFAChallengeRequired with default values."""
        error = MFAChallengeRequired()

        assert "MFA challenge required" in str(error)
        assert error.mfa_type is None
        assert error.mfa_tokens == {}

    def test_with_mfa_type(self) -> None:
        """Test MFAChallengeRequired with mfa_type."""
        error = MFAChallengeRequired(
            message="Enter code",
            mfa_type="SOFTWARE_TOKEN_MFA",
        )

        assert error.mfa_type == "SOFTWARE_TOKEN_MFA"

    def test_with_mfa_tokens(self) -> None:
        """Test MFAChallengeRequired with mfa_tokens."""
        tokens = {"session": "abc123", "challenge": "xyz"}
        error = MFAChallengeRequired(
            message="Enter code",
            mfa_tokens=tokens,
        )

        assert error.mfa_tokens == tokens
        assert error.mfa_tokens["session"] == "abc123"

    def test_with_all_parameters(self) -> None:
        """Test MFAChallengeRequired with all parameters."""
        tokens = {"session": "test_session"}
        error = MFAChallengeRequired(
            message="SMS code required",
            mfa_type="SMS_MFA",
            mfa_tokens=tokens,
        )

        assert str(error) == "SMS code required"
        assert error.mfa_type == "SMS_MFA"
        assert error.mfa_tokens == tokens

    def test_mfa_tokens_none_becomes_empty_dict(self) -> None:
        """Test None mfa_tokens becomes empty dict."""
        error = MFAChallengeRequired(mfa_tokens=None)

        assert error.mfa_tokens == {}
        assert isinstance(error.mfa_tokens, dict)


# ============================================================================
# InvalidDeviceError Tests
# ============================================================================


class TestInvalidDeviceError:
    """Tests for InvalidDeviceError class."""

    def test_default_values(self) -> None:
        """Test InvalidDeviceError with default values."""
        error = InvalidDeviceError()

        assert "Invalid device data" in str(error)
        assert error.missing_fields == []

    def test_custom_message(self) -> None:
        """Test InvalidDeviceError with custom message."""
        error = InvalidDeviceError(message="Device not found")

        assert str(error) == "Device not found"

    def test_with_missing_fields(self) -> None:
        """Test InvalidDeviceError with missing_fields."""
        error = InvalidDeviceError(
            message="Device missing required fields",
            missing_fields=["serialnumber", "deviceid"],
        )

        assert error.missing_fields == ["serialnumber", "deviceid"]
        assert "serialnumber" in error.missing_fields

    def test_missing_fields_none_becomes_empty_list(self) -> None:
        """Test None missing_fields becomes empty list."""
        error = InvalidDeviceError(missing_fields=None)

        assert error.missing_fields == []
        assert isinstance(error.missing_fields, list)


# ============================================================================
# InvalidUserError Tests
# ============================================================================


class TestInvalidUserError:
    """Tests for InvalidUserError class."""

    def test_default_values(self) -> None:
        """Test InvalidUserError with default values."""
        error = InvalidUserError()

        assert "Invalid user data" in str(error)
        assert error.missing_fields == []

    def test_with_missing_fields(self) -> None:
        """Test InvalidUserError with missing_fields."""
        error = InvalidUserError(
            message="User incomplete",
            missing_fields=["firstname"],
        )

        assert error.missing_fields == ["firstname"]


# ============================================================================
# InvalidActionError Tests
# ============================================================================


class TestInvalidActionError:
    """Tests for InvalidActionError class."""

    def test_default_values(self) -> None:
        """Test InvalidActionError with default values."""
        error = InvalidActionError()

        assert "Invalid action" in str(error)
        assert error.action is None
        assert error.valid_actions == []

    def test_with_action(self) -> None:
        """Test InvalidActionError with action."""
        error = InvalidActionError(
            message="Unknown action",
            action="toggle",
        )

        assert error.action == "toggle"

    def test_with_valid_actions(self) -> None:
        """Test InvalidActionError with valid_actions."""
        error = InvalidActionError(
            message="Action not supported",
            action="toggle",
            valid_actions=["lock", "unlock"],
        )

        assert error.valid_actions == ["lock", "unlock"]
        assert "lock" in error.valid_actions
        assert "unlock" in error.valid_actions

    def test_valid_actions_none_becomes_empty_list(self) -> None:
        """Test None valid_actions becomes empty list."""
        error = InvalidActionError(valid_actions=None)

        assert error.valid_actions == []


# ============================================================================
# InvalidStatusError Tests
# ============================================================================


class TestInvalidStatusError:
    """Tests for InvalidStatusError class."""

    def test_default_values(self) -> None:
        """Test InvalidStatusError with default values."""
        error = InvalidStatusError()

        assert "Invalid status" in str(error)
        assert error.status is None
        assert error.valid_statuses == []

    def test_with_status(self) -> None:
        """Test InvalidStatusError with status."""
        error = InvalidStatusError(
            message="Status invalid",
            status="on",
        )

        assert error.status == "on"

    def test_with_valid_statuses(self) -> None:
        """Test InvalidStatusError with valid_statuses."""
        error = InvalidStatusError(
            message="Use true or false",
            status="on",
            valid_statuses=["true", "false"],
        )

        assert error.valid_statuses == ["true", "false"]


# ============================================================================
# DeviceCommandError Tests
# ============================================================================


class TestDeviceCommandError:
    """Tests for DeviceCommandError class."""

    def test_default_values(self) -> None:
        """Test DeviceCommandError with default values."""
        error = DeviceCommandError()

        assert "Device command failed" in str(error)
        assert error.device_id is None
        assert error.command is None

    def test_with_device_id(self) -> None:
        """Test DeviceCommandError with device_id."""
        error = DeviceCommandError(
            message="Command timeout",
            device_id="SN12345",
        )

        assert error.device_id == "SN12345"

    def test_with_command(self) -> None:
        """Test DeviceCommandError with command."""
        error = DeviceCommandError(
            message="Lock command failed",
            device_id="SN12345",
            command="lock",
        )

        assert error.command == "lock"

    def test_with_all_parameters(self) -> None:
        """Test DeviceCommandError with all parameters."""
        error = DeviceCommandError(
            message="Device not responding",
            device_id="SN67890",
            command="unlock",
        )

        assert str(error) == "Device not responding"
        assert error.device_id == "SN67890"
        assert error.command == "unlock"


# ============================================================================
# Simple Exception Tests
# ============================================================================


class TestSimpleExceptions:
    """Tests for simple exception classes without extra attributes."""

    def test_unauthenticated(self) -> None:
        """Test Unauthenticated exception."""
        error = Unauthenticated("Invalid credentials")

        assert str(error) == "Invalid credentials"
        assert isinstance(error, AuthenticationError)

    def test_user_not_found(self) -> None:
        """Test UserNotFound exception."""
        error = UserNotFound("User does not exist")

        assert str(error) == "User does not exist"
        assert isinstance(error, AuthenticationError)

    def test_user_exists(self) -> None:
        """Test UserExists exception."""
        error = UserExists("User already registered")

        assert str(error) == "User already registered"
        assert isinstance(error, AuthenticationError)

    def test_user_not_confirmed(self) -> None:
        """Test UserNotConfirmed exception."""
        error = UserNotConfirmed("Please confirm your email")

        assert str(error) == "Please confirm your email"
        assert isinstance(error, AuthenticationError)

    def test_unknown_error(self) -> None:
        """Test UnknownError exception."""
        error = UnknownError("Something unexpected happened")

        assert str(error) == "Something unexpected happened"
        assert isinstance(error, KwiksetError)


# ============================================================================
# Exception Chaining Tests
# ============================================================================


class TestExceptionChaining:
    """Tests for exception chaining support."""

    def test_exception_chaining_with_from(self) -> None:
        """Test exception can be raised with __cause__."""
        original = ValueError("Original error")

        try:
            try:
                raise original
            except ValueError as err:
                raise RequestError("Request failed") from err
        except RequestError as request_err:
            assert request_err.__cause__ is original

    def test_exception_preserves_traceback(self) -> None:
        """Test exception preserves traceback information."""
        try:
            raise KwiksetError("Test error")
        except KwiksetError as err:
            assert err.__traceback__ is not None


# ============================================================================
# MinimumAccessCodeError Tests
# ============================================================================


class TestMinimumAccessCodeError:
    """Tests for MinimumAccessCodeError class."""

    def test_default_message(self) -> None:
        """Test MinimumAccessCodeError has default message."""
        error = MinimumAccessCodeError()

        assert "Cannot delete the last access code" in str(error)
        assert "At least one access code is required" in str(error)

    def test_custom_message(self) -> None:
        """Test MinimumAccessCodeError with custom message."""
        error = MinimumAccessCodeError(message="Custom error message")

        assert str(error) == "Custom error message"

    def test_default_attributes(self) -> None:
        """Test MinimumAccessCodeError default attribute values."""
        error = MinimumAccessCodeError()

        assert error.device_id is None
        assert error.current_count is None

    def test_with_device_id(self) -> None:
        """Test MinimumAccessCodeError with device_id."""
        error = MinimumAccessCodeError(device_id="SN12345")

        assert error.device_id == "SN12345"

    def test_with_current_count(self) -> None:
        """Test MinimumAccessCodeError with current_count."""
        error = MinimumAccessCodeError(current_count=1)

        assert error.current_count == 1

    def test_with_all_parameters(self) -> None:
        """Test MinimumAccessCodeError with all parameters."""
        error = MinimumAccessCodeError(
            message="Only one code left",
            device_id="SN67890",
            current_count=1,
        )

        assert str(error) == "Only one code left"
        assert error.device_id == "SN67890"
        assert error.current_count == 1

    def test_catchable_by_device_error(self) -> None:
        """Test MinimumAccessCodeError can be caught by DeviceError handler."""
        try:
            raise MinimumAccessCodeError(device_id="SN12345")
        except DeviceError:
            pass  # Expected
        except Exception:
            pytest.fail("MinimumAccessCodeError should be caught by DeviceError")

    def test_catchable_by_kwikset_error(self) -> None:
        """Test MinimumAccessCodeError can be caught by KwiksetError handler."""
        try:
            raise MinimumAccessCodeError()
        except KwiksetError:
            pass  # Expected
        except Exception:
            pytest.fail("MinimumAccessCodeError should be caught by KwiksetError")
