"""Tests for the exception hierarchy and _map_error factory.

Covers DexFatherError, all APIError subclasses, NetworkError subclasses,
the _map_error factory function, RateLimited.retry_after parsing, and
MAX_RETRY_AFTER cap.
"""

import pytest

from dexfather.exceptions import (
    MAX_RETRY_AFTER,
    APIError,
    BadRequest,
    DexFatherError,
    Forbidden,
    NetworkError,
    NotFound,
    RateLimited,
    TimeoutError,
    Unauthorized,
    WebSocketError,
    _map_error,
)


# ── Base exception ──────────────────────────────────────────────


class TestDexFatherError:
    def test_default_message(self):
        err = DexFatherError()
        assert str(err) == "An error occurred"
        assert err.message == "An error occurred"

    def test_custom_message(self):
        err = DexFatherError("custom error")
        assert str(err) == "custom error"
        assert err.message == "custom error"

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


# ── APIError ────────────────────────────────────────────────────


class TestAPIError:
    def test_basic(self):
        err = APIError(500, "Internal Server Error")
        assert err.error_code == 500
        assert err.description == "Internal Server Error"
        assert str(err) == "[500] Internal Server Error"

    def test_inherits_dexfather_error(self):
        assert issubclass(APIError, DexFatherError)

    def test_is_catchable_as_base(self):
        err = APIError(502, "Bad Gateway")
        with pytest.raises(DexFatherError):
            raise err


# ── Specific API error subclasses ───────────────────────────────


class TestBadRequest:
    def test_default(self):
        err = BadRequest()
        assert err.error_code == 400
        assert err.description == "Bad Request"

    def test_custom_description(self):
        err = BadRequest("Invalid JSON body")
        assert err.error_code == 400
        assert err.description == "Invalid JSON body"

    def test_inherits_api_error(self):
        assert issubclass(BadRequest, APIError)
        with pytest.raises(APIError):
            raise BadRequest()


class TestUnauthorized:
    def test_default(self):
        err = Unauthorized()
        assert err.error_code == 401
        assert err.description == "Unauthorized"

    def test_custom(self):
        err = Unauthorized("Token expired")
        assert err.description == "Token expired"


class TestForbidden:
    def test_default(self):
        err = Forbidden()
        assert err.error_code == 403
        assert err.description == "Forbidden"

    def test_custom(self):
        err = Forbidden("Bot lacks permission")
        assert err.description == "Bot lacks permission"


class TestNotFound:
    def test_default(self):
        err = NotFound()
        assert err.error_code == 404
        assert err.description == "Not Found"

    def test_custom(self):
        err = NotFound("Chat not found")
        assert err.description == "Chat not found"


class TestRateLimited:
    def test_default(self):
        err = RateLimited()
        assert err.error_code == 429
        assert err.retry_after == 1.0
        assert err.description == "Rate limited"

    def test_custom_retry_after(self):
        err = RateLimited(retry_after=5.0, description="Slow down")
        assert err.retry_after == 5.0
        assert err.description == "Slow down"

    def test_inherits_api_error(self):
        assert issubclass(RateLimited, APIError)


# ── Network errors ──────────────────────────────────────────────


class TestNetworkError:
    def test_inherits_dexfather_error(self):
        assert issubclass(NetworkError, DexFatherError)

    def test_basic(self):
        err = NetworkError("connection refused")
        assert str(err) == "connection refused"


class TestTimeoutError:
    def test_inherits_network_error(self):
        assert issubclass(TimeoutError, NetworkError)

    def test_basic(self):
        err = TimeoutError("request timed out after 30s")
        assert str(err) == "request timed out after 30s"


class TestWebSocketError:
    def test_inherits_network_error(self):
        assert issubclass(WebSocketError, NetworkError)

    def test_basic(self):
        err = WebSocketError("connection closed unexpectedly")
        assert str(err) == "connection closed unexpectedly"


# ── _map_error factory ──────────────────────────────────────────


class TestMapError:
    def test_400_returns_bad_request(self):
        err = _map_error(400, "Invalid input")
        assert isinstance(err, BadRequest)
        assert err.error_code == 400
        assert err.description == "Invalid input"

    def test_401_returns_unauthorized(self):
        err = _map_error(401, "Invalid token")
        assert isinstance(err, Unauthorized)
        assert err.error_code == 401

    def test_403_returns_forbidden(self):
        err = _map_error(403, "No access")
        assert isinstance(err, Forbidden)
        assert err.error_code == 403

    def test_404_returns_not_found(self):
        err = _map_error(404, "User not found")
        assert isinstance(err, NotFound)
        assert err.error_code == 404

    def test_429_returns_rate_limited_default_retry(self):
        """429 without retry info falls back to 1.0s."""
        err = _map_error(429, "Too many requests")
        assert isinstance(err, RateLimited)
        assert err.retry_after == 1.0
        assert err.description == "Too many requests"

    def test_429_parses_retry_after(self):
        """429 with 'Retry after Ns' pattern parses retry_after."""
        err = _map_error(429, "Rate limited. Retry after 5s")
        assert isinstance(err, RateLimited)
        assert err.retry_after == 5.0

    def test_429_parses_float_retry_after(self):
        """429 with float retry after value."""
        err = _map_error(429, "Retry after 2.5s please")
        assert isinstance(err, RateLimited)
        assert err.retry_after == 2.5

    def test_429_case_insensitive(self):
        """Regex matching is case-insensitive."""
        err = _map_error(429, "RETRY AFTER 10s")
        assert isinstance(err, RateLimited)
        assert err.retry_after == 10.0

    def test_429_caps_at_max_retry_after(self):
        """Retry after is capped at MAX_RETRY_AFTER (300s)."""
        err = _map_error(429, "Retry after 99999s")
        assert isinstance(err, RateLimited)
        assert err.retry_after == MAX_RETRY_AFTER
        assert err.retry_after == 300

    def test_unknown_status_returns_generic_api_error(self):
        """Unknown HTTP codes return generic APIError."""
        err = _map_error(502, "Bad Gateway")
        assert isinstance(err, APIError)
        assert type(err) is APIError  # exactly APIError, not a subclass
        assert err.error_code == 502
        assert err.description == "Bad Gateway"

    def test_500_returns_generic_api_error(self):
        err = _map_error(500, "Internal Server Error")
        assert isinstance(err, APIError)
        assert type(err) is APIError
        assert err.error_code == 500

    def test_all_mapped_errors_are_api_error(self):
        """All mapped errors inherit from APIError."""
        for code in (400, 401, 403, 404, 429, 500, 502):
            err = _map_error(code, "test")
            assert isinstance(err, APIError)
            assert isinstance(err, DexFatherError)


# ── MAX_RETRY_AFTER constant ────────────────────────────────────


class TestMaxRetryAfter:
    def test_value(self):
        assert MAX_RETRY_AFTER == 300

    def test_prevents_huge_retry(self):
        err = _map_error(429, "Retry after 600s")
        assert err.retry_after == 300
