"""Tests for error mapping from HTTP responses to typed exceptions."""

import httpx
import pytest

from baponi._base import raise_for_status
from baponi.exceptions import (
    APITimeoutError,
    APIValidationError,
    AuthenticationError,
    BaponiError,
    ForbiddenError,
    RateLimitError,
    ServerError,
    ThreadBusyError,
)


def _make_response(
    status_code: int,
    json_body: dict | None = None,
    text: str = "",
    headers: dict[str, str] | None = None,
) -> httpx.Response:
    """Create a mock httpx.Response."""
    resp = httpx.Response(
        status_code=status_code,
        headers=headers or {},
        json=json_body,
        text=text if json_body is None else None,
    )
    return resp


class TestRaiseForStatus:
    def test_success_does_not_raise(self):
        resp = _make_response(200, {"success": True})
        raise_for_status(resp)  # Should not raise

    def test_401_raises_authentication_error(self):
        resp = _make_response(401, {"error": "unauthorized", "message": "Invalid API key"})
        with pytest.raises(AuthenticationError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 401
        assert exc_info.value.error_type == "unauthorized"
        assert "Invalid API key" in exc_info.value.message

    def test_403_raises_forbidden_error(self):
        resp = _make_response(403, {"error": "forbidden", "message": "Insufficient permissions"})
        with pytest.raises(ForbiddenError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 403

    def test_409_raises_thread_busy_error(self):
        resp = _make_response(409, {"error": "conflict", "message": "thread_busy: my-thread"})
        with pytest.raises(ThreadBusyError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 409
        assert "thread_busy" in exc_info.value.message

    def test_429_raises_rate_limit_error(self):
        resp = _make_response(429, {"error": "rate_limited", "message": "Too many requests"})
        with pytest.raises(RateLimitError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 429
        assert exc_info.value.retry_after is None

    def test_429_parses_retry_after_header(self):
        resp = _make_response(
            429,
            {"error": "rate_limited", "message": "Too many requests"},
            headers={"retry-after": "5"},
        )
        with pytest.raises(RateLimitError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.retry_after == 5.0

    def test_504_raises_api_timeout_error(self):
        resp = _make_response(504, {"error": "gateway_timeout", "message": "Execution timed out"})
        with pytest.raises(APITimeoutError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 504

    def test_400_raises_api_validation_error(self):
        resp = _make_response(
            400, {"error": "validation_error", "message": "Invalid language"}
        )
        with pytest.raises(APIValidationError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 400

    def test_500_raises_server_error(self):
        resp = _make_response(500, {"error": "internal_error", "message": "Internal error"})
        with pytest.raises(ServerError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 500

    def test_503_raises_server_error(self):
        resp = _make_response(
            503, {"error": "service_unavailable", "message": "No capacity"}
        )
        with pytest.raises(ServerError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 503

    def test_unknown_4xx_raises_base_error(self):
        resp = _make_response(418, {"error": "teapot", "message": "I'm a teapot"})
        with pytest.raises(BaponiError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 418
        assert type(exc_info.value) is BaponiError

    def test_413_raises_validation_error(self):
        """413 with extra max_size_bytes field should still parse correctly."""
        resp = _make_response(
            413,
            {
                "error": "payload_too_large",
                "message": "Code exceeds maximum size",
                "max_size_bytes": 1048576,
            },
        )
        with pytest.raises(BaponiError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.status_code == 413
        assert "Code exceeds maximum size" in exc_info.value.message

    def test_non_json_response_uses_text(self):
        resp = _make_response(500, text="Internal Server Error")
        with pytest.raises(ServerError) as exc_info:
            raise_for_status(resp)
        assert "Internal Server Error" in exc_info.value.message

    def test_legacy_type_field_parsed(self):
        resp = _make_response(401, {"type": "auth_error", "message": "Bad key"})
        with pytest.raises(AuthenticationError) as exc_info:
            raise_for_status(resp)
        assert exc_info.value.error_type == "auth_error"


class TestExceptionHierarchy:
    def test_all_errors_are_baponi_error(self):
        assert issubclass(AuthenticationError, BaponiError)
        assert issubclass(ForbiddenError, BaponiError)
        assert issubclass(RateLimitError, BaponiError)
        assert issubclass(ThreadBusyError, BaponiError)
        assert issubclass(APITimeoutError, BaponiError)
        assert issubclass(ServerError, BaponiError)
        assert issubclass(APIValidationError, BaponiError)

    def test_baponi_error_is_exception(self):
        assert issubclass(BaponiError, Exception)

    def test_error_repr(self):
        err = BaponiError("test", status_code=500, error_type="internal")
        r = repr(err)
        assert "BaponiError" in r
        assert "test" in r
        assert "500" in r
