"""Tests for error classes and utilities."""

from __future__ import annotations

import pytest

from undetecta.errors import (
    ApiKeyError,
    AuthenticationError,
    BadRequestError,
    HttpError,
    NetworkError,
    NotFoundError,
    RateLimitError,
    ServerError,
    TimeoutError,
    UndetectaError,
    ValidationError,
    is_retryable_status,
    parse_error_response,
)


class TestUndetectaError:
    """Tests for UndetectaError base class."""

    def test_initialization(self):
        """Test error initialization."""
        error = UndetectaError("TEST_CODE", "Test message")
        assert error.code == "TEST_CODE"
        assert error.message == "Test message"
        assert error.status_code is None
        assert str(error) == "Test message"

    def test_initialization_with_status_code(self):
        """Test error initialization with status code."""
        error = UndetectaError("TEST_CODE", "Test message", 404)
        assert error.code == "TEST_CODE"
        assert error.message == "Test message"
        assert error.status_code == 404

    def test_repr(self):
        """Test error string representation."""
        error = UndetectaError("TEST_CODE", "Test message")
        assert repr(error) == "UndetectaError(code='TEST_CODE', message='Test message')"


class TestApiKeyError:
    """Tests for ApiKeyError."""

    def test_default_message(self):
        """Test default error message."""
        error = ApiKeyError()
        assert error.code == "INVALID_API_KEY"
        assert error.message == "Invalid or missing API key"
        assert error.status_code == 401

    def test_custom_message(self):
        """Test custom error message."""
        error = ApiKeyError("Custom key message")
        assert error.code == "INVALID_API_KEY"
        assert error.message == "Custom key message"
        assert error.status_code == 401


class TestAuthenticationError:
    """Tests for AuthenticationError."""

    def test_default_message(self):
        """Test default error message."""
        error = AuthenticationError()
        assert error.code == "UNAUTHORIZED"
        assert error.message == "Authentication failed"
        assert error.status_code == 401

    def test_custom_message(self):
        """Test custom error message."""
        error = AuthenticationError("Auth failed")
        assert error.code == "UNAUTHORIZED"
        assert error.message == "Auth failed"


class TestRateLimitError:
    """Tests for RateLimitError."""

    def test_default_message(self):
        """Test default error message."""
        error = RateLimitError()
        assert error.code == "RATE_LIMIT_EXCEEDED"
        assert error.message == "Rate limit exceeded"
        assert error.status_code == 429


class TestNotFoundError:
    """Tests for NotFoundError."""

    def test_default_message(self):
        """Test default error message."""
        error = NotFoundError()
        assert error.code == "NOT_FOUND"
        assert error.message == "Resource not found"
        assert error.status_code == 404


class TestValidationError:
    """Tests for ValidationError."""

    def test_custom_message(self):
        """Test custom error message."""
        error = ValidationError("Invalid input")
        assert error.code == "VALIDATION_ERROR"
        assert error.message == "Invalid input"
        assert error.status_code == 400


class TestTimeoutError:
    """Tests for TimeoutError."""

    def test_default_message(self):
        """Test default error message."""
        error = TimeoutError()
        assert error.code == "TIMEOUT"
        assert error.message == "Request timed out"


class TestNetworkError:
    """Tests for NetworkError."""

    def test_custom_message(self):
        """Test custom error message."""
        error = NetworkError("Connection failed")
        assert error.code == "NETWORK_ERROR"
        assert error.message == "Connection failed"


class TestHttpError:
    """Tests for HttpError."""

    def test_initialization(self):
        """Test error initialization."""
        error = HttpError("Bad request", 400, {"error": "details"})
        assert error.code == "HTTP_ERROR"
        assert error.message == "Bad request"
        assert error.status_code == 400
        assert error.data == {"error": "details"}

    def test_without_data(self):
        """Test error initialization without data."""
        error = HttpError("Not found", 404)
        assert error.data is None


class TestParseErrorResponse:
    """Tests for parse_error_response function."""

    def test_api_key_error_unauthorized(self):
        """Test parsing UNAUTHORIZED error."""
        error = parse_error_response({
            "error": {"code": "UNAUTHORIZED", "message": "Auth required"}
        })
        assert isinstance(error, ApiKeyError)
        assert error.message == "Auth required"

    def test_api_key_error_invalid_key(self):
        """Test parsing INVALID_API_KEY error."""
        error = parse_error_response({
            "error": {"code": "INVALID_API_KEY", "message": "Invalid key"}
        })
        assert isinstance(error, ApiKeyError)
        assert error.message == "Invalid key"

    def test_rate_limit_error(self):
        """Test parsing RATE_LIMIT_EXCEEDED error."""
        error = parse_error_response({
            "error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests"}
        })
        assert isinstance(error, RateLimitError)
        assert error.message == "Too many requests"

    def test_not_found_error(self):
        """Test parsing NOT_FOUND error."""
        error = parse_error_response({
            "error": {"code": "NOT_FOUND", "message": "Resource not found"}
        })
        assert isinstance(error, NotFoundError)
        assert error.message == "Resource not found"

    def test_validation_error(self):
        """Test parsing VALIDATION_ERROR error."""
        error = parse_error_response({
            "error": {"code": "VALIDATION_ERROR", "message": "Invalid input"}
        })
        assert isinstance(error, ValidationError)
        assert error.message == "Invalid input"

    def test_unknown_error_code(self):
        """Test parsing unknown error code."""
        error = parse_error_response({
            "error": {"code": "UNKNOWN_ERROR", "message": "Something went wrong"}
        })
        assert isinstance(error, UndetectaError)
        assert not isinstance(error, ApiKeyError)
        assert error.code == "UNKNOWN_ERROR"
        assert error.message == "Something went wrong"

    def test_missing_error_object(self):
        """Test parsing response without error object."""
        error = parse_error_response({"data": "some data"})
        assert isinstance(error, UndetectaError)
        assert error.code == "UNKNOWN_ERROR"
        assert error.message == "An unknown error occurred"

    def test_missing_code_and_message(self):
        """Test parsing error response without code/message."""
        error = parse_error_response({"error": {}})
        assert isinstance(error, UndetectaError)
        assert error.code == "UNKNOWN_ERROR"
        assert error.message == "An unknown error occurred"

    def test_non_dict_input(self):
        """Test parsing non-dict input."""
        error = parse_error_response("invalid")
        assert isinstance(error, UndetectaError)
        assert error.code == "UNKNOWN_ERROR"
        assert error.message == "An unknown error occurred"

    def test_none_input(self):
        """Test parsing None input."""
        error = parse_error_response(None)
        assert isinstance(error, UndetectaError)
        assert error.code == "UNKNOWN_ERROR"
        assert error.message == "An unknown error occurred"


class TestIsRetryableStatus:
    """Tests for is_retryable_status function."""

    @pytest.mark.parametrize("status", [408, 429, 500, 502, 503, 504])
    def test_retryable_status_codes(self, status):
        """Test that retryable status codes return True."""
        assert is_retryable_status(status) is True

    @pytest.mark.parametrize("status", [200, 201, 204, 301, 302, 400, 401, 403, 404])
    def test_non_retryable_status_codes(self, status):
        """Test that non-retryable status codes return False."""
        assert is_retryable_status(status) is False

    def test_request_timeout(self):
        """Test 408 Request Timeout is retryable."""
        assert is_retryable_status(408) is True

    def test_too_many_requests(self):
        """Test 429 Too Many Requests is retryable."""
        assert is_retryable_status(429) is True

    def test_internal_server_error(self):
        """Test 500 Internal Server Error is retryable."""
        assert is_retryable_status(500) is True

    def test_bad_gateway(self):
        """Test 502 Bad Gateway is retryable."""
        assert is_retryable_status(502) is True

    def test_service_unavailable(self):
        """Test 503 Service Unavailable is retryable."""
        assert is_retryable_status(503) is True

    def test_gateway_timeout(self):
        """Test 504 Gateway Timeout is retryable."""
        assert is_retryable_status(504) is True

    def test_ok(self):
        """Test 200 OK is not retryable."""
        assert is_retryable_status(200) is False

    def test_bad_request(self):
        """Test 400 Bad Request is not retryable."""
        assert is_retryable_status(400) is False

    def test_unauthorized(self):
        """Test 401 Unauthorized is not retryable."""
        assert is_retryable_status(401) is False

    def test_forbidden(self):
        """Test 403 Forbidden is not retryable."""
        assert is_retryable_status(403) is False

    def test_not_found(self):
        """Test 404 Not Found is not retryable."""
        assert is_retryable_status(404) is False


class TestErrorHierarchy:
    """Tests for error class hierarchy."""

    def test_all_errors_inherit_from_base(self):
        """Test that all specific errors inherit from UndetectaError."""
        assert issubclass(ApiKeyError, UndetectaError)
        assert issubclass(AuthenticationError, UndetectaError)
        assert issubclass(BadRequestError, UndetectaError)
        assert issubclass(RateLimitError, UndetectaError)
        assert issubclass(NotFoundError, UndetectaError)
        assert issubclass(ServerError, UndetectaError)
        assert issubclass(NetworkError, UndetectaError)
        assert issubclass(ValidationError, UndetectaError)
        assert issubclass(TimeoutError, UndetectaError)
        assert issubclass(HttpError, UndetectaError)

    def test_catching_base_error(self):
        """Test that specific errors can be caught as UndetectaError."""
        error = ApiKeyError("Test")
        assert isinstance(error, UndetectaError)
        assert isinstance(error, Exception)

        caught = False
        try:
            raise error
        except UndetectaError:
            caught = True
        assert caught
