from aifc import Error
from typing import Any, Dict
from unittest.mock import patch

import elasticapm
import pytest
from fastapi import HTTPException
from loguru import logger
from meiga import AnyResult, BoolResult, Failure, Result, Success, isFailure, isSuccess

from petisco import AlreadyExists, DomainError, HttpError, NotFound, PrintMiddleware
from petisco.extra.fastapi import (
    FASTAPI_DEFAULT_RESPONSE,
    AsyncFastAPIController,
    as_fastapi,
    assert_http_exception,
)


@pytest.mark.unit
@pytest.mark.asyncio
class TestAsyncFastApiController:
    async def should_return_dict_when_success_result(self):
        class MyController(AsyncFastAPIController):
            async def execute(self) -> BoolResult:
                return isSuccess

        result = await MyController().execute()
        assert as_fastapi(result) == FASTAPI_DEFAULT_RESPONSE

    async def should_raise_http_exception_when_failure_result(self):
        class MyController(AsyncFastAPIController):
            async def execute(self) -> BoolResult:
                return isFailure

        with pytest.raises(HTTPException) as excinfo:
            result = await MyController().execute()
            as_fastapi(result)

        assert excinfo.value.status_code == 500
        assert excinfo.value.detail == "Unknown Error"

    @patch.object(logger, "error")
    @patch.object(elasticapm, "set_custom_context")
    async def should_logging_error_with_traceback_and_inject_in_apm(
        self, apm_set_custom_context_mock, logger_error_mock
    ):
        class MyController(AsyncFastAPIController):
            async def execute(self) -> BoolResult:
                raise TypeError("test_error")

        with pytest.raises(HTTPException):
            result = await MyController().execute()
            as_fastapi(result)

        apm_set_custom_context_mock.assert_called()
        apm_internal_error_message = apm_set_custom_context_mock.call_args[0][0]["internal_error_message"]
        logger_error_mock.assert_called()
        logger_error_message = logger_error_mock.call_args[0][0]
        assert "test_error" in logger_error_message and "test_error" in apm_internal_error_message
        assert __name__ in logger_error_message and __name__ in apm_internal_error_message

    async def should_return_tranformed_result_when_success_handler(self):
        expected_result = {"message": "ok"}

        class MyController(AsyncFastAPIController):
            class Config:
                success_handler = lambda result: expected_result  # noqa E731

            async def execute(self) -> BoolResult:
                return isSuccess

        result = await MyController().execute()

        assert as_fastapi(result) == expected_result

    @patch.object(elasticapm, "set_custom_context")
    async def should_raise_fastapi_http_exception_mapped_and_inject_info_in_apm(
        self,
        apm_set_custom_context_mock,
    ):
        http_error = HttpError(status_code=404, detail="Task not Found")

        class MyController(AsyncFastAPIController):
            class Config:
                error_map = {NotFound: http_error}

            async def execute(self) -> BoolResult:
                return Failure(NotFound())

        with pytest.raises(HTTPException) as excinfo:
            result = await MyController().execute()
            as_fastapi(result)

        assert excinfo.value.status_code == http_error.status_code
        assert excinfo.value.detail == http_error.detail

        apm_set_custom_context_mock.assert_called()
        apm_set_custom_context_arg = apm_set_custom_context_mock.call_args[0][0]

        assert apm_set_custom_context_arg["http_response"] == str(http_error)

    @patch.object(logger, "error")
    @patch.object(elasticapm, "set_custom_context")
    async def should_logging_error_message_and_inject_it_in_apm_with_non_mapped_error(
        self, apm_set_custom_context_mock, logger_error_mock
    ):
        class MyError(DomainError):
            pass

        class MyController(AsyncFastAPIController):
            async def execute(self) -> BoolResult:
                return Failure(MyError())

        with pytest.raises(HTTPException) as excinfo:
            result = await MyController().execute()
            as_fastapi(result)

        assert excinfo.value.status_code == 500
        error_message = "Error 'MyError' is not mapped in controller"

        logger_error_mock.assert_called()
        logger_error_message = logger_error_mock.call_args_list[0][0]
        assert error_message in logger_error_message

        apm_set_custom_context_mock.assert_called()
        apm_set_custom_context_arg = apm_set_custom_context_mock.call_args[0][0]

        assert apm_set_custom_context_arg["internal_error_message"] == error_message

    @pytest.mark.parametrize(
        "configured_middlewares",
        [[], [PrintMiddleware], [PrintMiddleware, PrintMiddleware]],
    )
    async def should_return_success_result_with_middlewares(
        self,
        configured_middlewares,
    ):
        class MyController(AsyncFastAPIController):
            class Config:
                middlewares = configured_middlewares

            async def execute(self) -> BoolResult:
                return isSuccess

        result = await MyController().execute()
        assert as_fastapi(result) == FASTAPI_DEFAULT_RESPONSE

    async def should_execute_middleware_when_controller_raise_an_unexpected_exception(self):
        class MyController(AsyncFastAPIController):
            class Config:
                middlewares = [PrintMiddleware]

            async def execute(self) -> BoolResult:
                raise Exception()

        with patch.object(
            PrintMiddleware, "before", return_value=isSuccess
        ) as mock_middleware_before, patch.object(
            PrintMiddleware, "after", return_value=isSuccess
        ) as mock_middleware_after, pytest.raises(HTTPException):
            result = await MyController().execute()
            as_fastapi(result)

        mock_middleware_before.assert_called()
        mock_middleware_after.assert_called()

    async def should_success_with_all_configurations_when_success_result(self):
        class MyController(AsyncFastAPIController):
            class Config:
                middlewares = [PrintMiddleware]
                success_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                error_map = {NotFound: HttpError(status_code=404, detail="Task not Found")}

            async def execute(self) -> BoolResult:
                return isSuccess

        result = await MyController().execute()
        assert as_fastapi(result) == FASTAPI_DEFAULT_RESPONSE

    @pytest.mark.parametrize(
        "result,expected_http_exception",
        [
            (isFailure, HTTPException(500, detail="Unknown Error")),
            (Failure(NotFound()), HTTPException(404, detail="Task not Found")),
            (Failure(AlreadyExists()), HTTPException(409, detail="AlreadyExists")),
            (
                Failure(AlreadyExists(uuid_value="76f5994d-b16f-45b0-b3e4-e531f784f801")),
                HTTPException(409, detail="AlreadyExists (76f5994d-b16f-45b0-b3e4-e531f784f801)"),
            ),
            (
                Failure(
                    AlreadyExists(
                        uuid_value="76f5994d-b16f-45b0-b3e4-e531f784f801",
                        additional_info={
                            "patterns": "Products",
                            "table": "UserProduct",
                        },
                    )
                ),
                HTTPException(
                    409,
                    detail="AlreadyExists (76f5994d-b16f-45b0-b3e4-e531f784f801) [{'patterns': 'Products', 'table': 'UserProduct'}]",
                ),
            ),
            (
                Failure(AlreadyExists(additional_info={"patterns": "Products", "table": "UserProduct"})),
                HTTPException(
                    409,
                    detail="AlreadyExists [{'patterns': 'Products', 'table': 'UserProduct'}]",
                ),
            ),
        ],
    )
    async def should_raise_fastapi_http_exception_with_all_configurations_when_failure_result(
        self, result, expected_http_exception
    ):
        class MyController(AsyncFastAPIController):
            class Config:
                middlewares = [PrintMiddleware]
                success_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                error_map = {NotFound: HttpError(status_code=404, detail="Task not Found")}

            async def execute(self) -> BoolResult:
                return result

        with pytest.raises(HTTPException) as excinfo:
            result = await MyController().execute()
            as_fastapi(result)

        assert_http_exception(excinfo.value, expected_http_exception)

    @pytest.mark.parametrize(
        "result,expected_response",
        [
            (Success(5), {"result": 5}),
            (Success("ok"), {"result": "ok"}),
            (Success(5.45), {"result": 5.45}),
            (Success([1, 2]), {"result": [1, 2]}),
            (Success((1, 2)), {"result": (1, 2)}),
            # (Success(type("MyClass", (), {})()), {"result": "<test_fastapi_controller.MyClass object at 0x106140518>"}),
        ],
    )
    async def should_return_parsed_result_when_success_result_with_value(
        self, result: Result, expected_response: Dict[str, Any]
    ):
        class MyController(AsyncFastAPIController):
            async def execute(self) -> Result:
                return result

        result = await MyController().execute()
        assert as_fastapi(result) == expected_response

    @pytest.mark.parametrize(
        "result,expected_response",
        [
            (Success(5), FASTAPI_DEFAULT_RESPONSE),
            (Failure(), FASTAPI_DEFAULT_RESPONSE),
        ],
    )
    async def should_return_default_response_overriding_failure_handler(
        self, result: Result, expected_response: Dict[str, Any]
    ):
        class MyController(AsyncFastAPIController):
            class Config:
                success_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                failure_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                error_map = {NotFound: HttpError(status_code=404, detail="Task not Found")}

            async def execute(self) -> BoolResult:
                return result

        result = await MyController().execute()
        assert as_fastapi(result) == expected_response

    @pytest.mark.parametrize(
        "result,expected_response",
        [
            (Success(5), FASTAPI_DEFAULT_RESPONSE),
            (Failure(), FASTAPI_DEFAULT_RESPONSE),
        ],
    )
    async def should_return_default_response_overriding_failure_handler_with_error_map(
        self, result: Result, expected_response: Dict[str, Any]
    ):
        class MyController(AsyncFastAPIController):
            class Config:
                success_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                failure_handler = lambda result, error_map: FASTAPI_DEFAULT_RESPONSE  # noqa E731
                error_map = {NotFound: HttpError(status_code=404, detail="Task not Found")}

            async def execute(self) -> BoolResult:
                return result

        result = await MyController().execute()
        assert as_fastapi(result) == expected_response

    async def should_succees_when_use_as_fastapi_controller_result(self):  # noqa
        class MyController(AsyncFastAPIController):
            async def execute(self) -> Result[int, Error]:
                return Success(1)

        async def function() -> int:
            result = await MyController().execute()
            return as_fastapi(result, expected_type=int)

        await function()

    async def should_fail_when_return_type_is_not_a_result(self):  # noqa
        class MyController(AsyncFastAPIController):
            async def execute(self) -> AnyResult:
                return 1

        with pytest.raises(TypeError, match="Controller Error"):
            await MyController().execute()

    async def should_return_responses_when_error_map_is_defined(self):
        class MyController(AsyncFastAPIController):
            class Config:
                error_map = {NotFound: HttpError(status_code=404, detail="Task not Found")}

            async def execute(self) -> BoolResult:
                return isSuccess

        responses = MyController.responses()
        assert responses == {404: {"description": "Task not Found"}}

    async def should_return_none_responses_when_config_is_not_defined(self):
        class MyController(AsyncFastAPIController):
            async def execute(self) -> BoolResult:
                return isSuccess

        responses = MyController.responses()
        assert responses is None

    async def should_return_none_responses_when_error_map_is_not_defined(self):
        class MyController(AsyncFastAPIController):
            class Config:
                success_handler = lambda result: FASTAPI_DEFAULT_RESPONSE  # noqa E731

            async def execute(self) -> BoolResult:
                return isSuccess

        responses = MyController.responses()
        assert responses is None
