from http import HTTPStatus
from json import JSONDecodeError

import pytest

from arkindex.exceptions import (
    ArkindexClientError,
    ArkindexConflictError,
    ArkindexServerError,
    ArkindexValidationError,
    ErrorResponse,
)


def test_request_no_more_retries(responses, api_client):
    # First call is an error
    responses.add(
        responses.POST,
        "https://dummy.test/api/v1/user/login/",
        status=502,
        body="Not JSON!",
    )

    api_client.request.retry.wait = 0

    # There should be 5 retries
    max_retries = 5

    with pytest.raises(ErrorResponse) as exc_info:
        api_client.login("user@user.user", "Pa$$w0rd")

    assert exc_info.value.title == "502 Bad Gateway"
    assert exc_info.value.status_code == 502
    # The raw contents of the body are output in the error
    assert exc_info.value.content == "Not JSON!"

    assert len(responses.calls) == max_retries
    assert [call.request.url for call in responses.calls] == [
        "https://dummy.test/api/v1/user/login/",
    ] * max_retries


def test_non_5xx_parse_error(responses, api_client):
    """
    For any HTTP response that is not 5xx, a parse error should be re-raised
    """
    responses.add(
        responses.POST,
        "https://dummy.test/api/v1/user/login/",
        status=402,
        # Explicitly remove the Content-Type header
        content_type=None,
        body="Not JSON!",
    )

    with pytest.raises(JSONDecodeError):
        api_client.login("user@user.user", "Pa$$w0rd")

    assert len(responses.calls) == 1
    assert responses.calls[0].request.url == "https://dummy.test/api/v1/user/login/"


@pytest.mark.parametrize(
    ("status_code", "data", "expected_class"),
    [
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": None, "code": "invalid", "detail": "Wrong!"}],
            },
            ArkindexClientError,
        ),
        # A ConflictError requires both 409 and the `existing_id`, otherwise it falls back to ClientError
        (
            409,
            {
                "type": "client_error",
                "errors": [{"attr": ["a", 1], "code": "invalid", "detail": "Wrong!"}],
            },
            ArkindexClientError,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": ["a", 1], "code": "invalid", "detail": "Wrong!"}],
                "existing_id": "abc",
            },
            ArkindexClientError,
        ),
        (
            409,
            {
                "type": "client_error",
                "errors": [{"attr": ["a", 1], "code": "invalid", "detail": "Wrong!"}],
                "existing_id": None,
            },
            ArkindexConflictError,
        ),
        (
            409,
            {
                "type": "client_error",
                "errors": [{"attr": ["a", 1], "code": "invalid", "detail": "Wrong!"}],
                "existing_id": "abc",
            },
            ArkindexConflictError,
        ),
        (
            400,
            {
                "type": "validation_error",
                "errors": [{"attr": ["a", 1], "code": "invalid", "detail": "Wrong!"}],
            },
            ArkindexValidationError,
        ),
        (
            500,
            {
                "type": "server_error",
                "errors": [{"attr": None, "code": "oh_no", "detail": "Broken"}],
            },
            ArkindexServerError,
        ),
        # Errors that do not follow the standardized error structure fall back to an ErrorResponse
        (400, {}, ErrorResponse),
        (400, [{"type": "client_error"}], ErrorResponse),
        (400, [{"type": "client_error", "errors": []}], ErrorResponse),
        (400, {"type": "client_error", "errors": None}, ErrorResponse),
        (
            400,
            {
                "type": "cursed_error",
                "errors": [{"attr": None, "code": "invalid", "detail": "Wrong!"}],
            },
            ErrorResponse,
        ),
        (
            400,
            {
                "type": {"unexpected": "dict"},
                "errors": [{"attr": None, "code": "invalid", "detail": "Wrong!"}],
            },
            ErrorResponse,
        ),
        (
            400,
            {"errors": [{"attr": None, "code": "invalid", "detail": "Wrong!"}]},
            ErrorResponse,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": None, "code": "invalid", "detail": "Wrong!"}],
            },
            ErrorResponse,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [
                    {
                        "attr": {"unexpected": "dict"},
                        "code": "invalid",
                        "detail": "Wrong!",
                    }
                ],
            },
            ErrorResponse,
        ),
        (
            400,
            {"type": "client_error", "errors": [{"attr": None, "detail": "Wrong!"}]},
            ErrorResponse,
        ),
        (
            400,
            {"type": "client_error", "errors": [{"attr": None, "code": "invalid"}]},
            ErrorResponse,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": None, "code": 42, "detail": "Wrong!"}],
            },
            ErrorResponse,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": None, "code": "invalid", "detail": ["Wrong!"]}],
            },
            ErrorResponse,
        ),
        (
            400,
            {
                "type": "client_error",
                "errors": [{"attr": [None], "code": "invalid", "detail": "Wrong!"}],
            },
            ErrorResponse,
        ),
    ],
)
def test_standardized_errors(responses, api_client, status_code, data, expected_class):
    responses.add(
        responses.POST,
        "https://dummy.test/api/v1/user/login/",
        status=status_code,
        json=data,
    )

    with pytest.raises(expected_class) as exc_info:
        api_client.request("Login", body={"email": "a", "password": "a"})

    assert exc_info.value.status_code == status_code
    reason = HTTPStatus(status_code).phrase
    assert exc_info.value.title == f"{status_code} {reason}"
    assert exc_info.value.content == data
