"""Tests for MCPGateway SDK exception hierarchy."""

from __future__ import annotations

import pytest

from mcpgateway_sdk._exceptions import (
    AuthError,
    ConflictError,
    ForbiddenError,
    GatewayError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    _extract_message,
    error_from_response,
)


class TestGatewayError:
    """Tests for the base GatewayError class."""

    def test_has_status_code_and_message(self) -> None:
        err = GatewayError(status_code=500, message="Internal error")
        assert err.status_code == 500
        assert err.message == "Internal error"

    def test_str_includes_status_code_and_message(self) -> None:
        err = GatewayError(status_code=502, message="Bad gateway")
        assert str(err) == "[502] Bad gateway"

    def test_is_exception(self) -> None:
        err = GatewayError(status_code=500, message="fail")
        assert isinstance(err, Exception)


class TestSubclasses:
    """Tests for named exception subclasses."""

    @pytest.mark.parametrize(
        ("cls", "expected_code", "default_msg"),
        [
            (AuthError, 401, "Unauthorized"),
            (ForbiddenError, 403, "Forbidden"),
            (NotFoundError, 404, "Not found"),
            (ConflictError, 409, "Conflict"),
            (ValidationError, 422, "Validation error"),
            (RateLimitError, 429, "Rate limited"),
        ],
    )
    def test_default_status_code_and_message(
        self,
        cls: type[GatewayError],
        expected_code: int,
        default_msg: str,
    ) -> None:
        err = cls()
        assert err.status_code == expected_code
        assert err.message == default_msg

    @pytest.mark.parametrize(
        ("cls", "expected_code"),
        [
            (AuthError, 401),
            (ForbiddenError, 403),
            (NotFoundError, 404),
            (ConflictError, 409),
            (ValidationError, 422),
            (RateLimitError, 429),
        ],
    )
    def test_custom_message_preserves_status_code(
        self,
        cls: type[GatewayError],
        expected_code: int,
    ) -> None:
        err = cls(message="custom message")
        assert err.status_code == expected_code
        assert err.message == "custom message"

    def test_all_subclasses_are_gateway_errors(self) -> None:
        for cls in (
            AuthError,
            ForbiddenError,
            NotFoundError,
            ConflictError,
            ValidationError,
            RateLimitError,
        ):
            assert issubclass(cls, GatewayError)


class TestErrorFromResponse:
    """Tests for error_from_response factory function."""

    @pytest.mark.parametrize(
        ("status_code", "expected_cls"),
        [
            (401, AuthError),
            (403, ForbiddenError),
            (404, NotFoundError),
            (409, ConflictError),
            (422, ValidationError),
            (429, RateLimitError),
        ],
    )
    def test_maps_known_status_codes(
        self, status_code: int, expected_cls: type[GatewayError]
    ) -> None:
        err = error_from_response(status_code, {"detail": "test msg"})
        assert isinstance(err, expected_cls)
        assert err.message == "test msg"

    def test_unknown_status_code_returns_gateway_error(self) -> None:
        err = error_from_response(500, {"detail": "server error"})
        assert type(err) is GatewayError
        assert err.status_code == 500
        assert err.message == "server error"

    def test_unknown_status_code_418(self) -> None:
        err = error_from_response(418, {"detail": "I'm a teapot"})
        assert type(err) is GatewayError
        assert err.status_code == 418


class TestExtractMessage:
    """Tests for _extract_message helper."""

    def test_detail_string(self) -> None:
        assert _extract_message({"detail": "simple string"}) == "simple string"

    def test_detail_dict_with_message(self) -> None:
        body = {"detail": {"message": "inner message", "code": "ERR"}}
        assert _extract_message(body) == "inner message"

    def test_detail_dict_without_message(self) -> None:
        body = {"detail": {"code": "ERR", "info": "something"}}
        result = _extract_message(body)
        assert "code" in result  # Falls back to str(detail)

    def test_error_dict_with_message(self) -> None:
        body = {"error": {"message": "error msg", "code": "VALIDATION_ERROR"}}
        assert _extract_message(body) == "error msg"

    def test_error_dict_without_message(self) -> None:
        body = {"error": {"code": "UNKNOWN"}}
        result = _extract_message(body)
        assert "code" in result

    def test_fallback_to_str_body(self) -> None:
        body = {"unexpected_key": "value"}
        result = _extract_message(body)
        assert "unexpected_key" in result

    def test_detail_list(self) -> None:
        body = {"detail": [{"loc": ["body", "email"], "msg": "invalid"}]}
        result = _extract_message(body)
        assert "invalid" in result or "loc" in result  # str(detail)
