from __future__ import annotations

from dataclasses import asdict
from typing import cast

import pytest

from delta_barth import errors
from delta_barth.constants import DEFAULT_API_ERR_CODE, DEFAULT_INTERNAL_ERR_CODE
from delta_barth.types import DelBarApiError, Status


@pytest.fixture(scope="module")
def status_hdlr() -> errors.StatusHandler:
    return errors.StatusHandler()


def test_status_handler_parsing(status_hdlr):
    predef_errs = errors.DATA_PIPELINE_STATUS_DESCR

    assert status_hdlr.pipe_states is not None
    parsed_pipe_states = status_hdlr.pipe_states
    assert parsed_pipe_states.SUCCESS == status_hdlr.SUCCESS
    parsed_pipe_states = asdict(parsed_pipe_states)

    for err in predef_errs:
        dopt_err = cast(Status, parsed_pipe_states[err[0]])
        assert isinstance(dopt_err, Status)
        assert dopt_err.code == err[1]
        assert dopt_err.description == err[2]
        assert dopt_err.message == ""

    status_hdlr._parse_data_pipe_states()


def test_status_handler_InternalErrors_Success(status_hdlr):
    DESCRIPTION = "test case"
    MESSAGE = "an error occurred"
    ERR_CODE = 101

    new_err = status_hdlr.error(
        description=DESCRIPTION,
        message=MESSAGE,
        code=ERR_CODE,
    )
    assert new_err.code == ERR_CODE
    assert new_err.description == DESCRIPTION
    assert new_err.message == MESSAGE
    # failure cases


def test_status_handler_InternalErrors_CodeOutOfBounds(status_hdlr):
    DESCRIPTION = "test case"
    MESSAGE = "an error occurred"

    err_code = DEFAULT_INTERNAL_ERR_CODE - 1
    with pytest.raises(ValueError):
        _ = status_hdlr.error(
            description=DESCRIPTION,
            message=MESSAGE,
            code=err_code,
        )
    err_code = DEFAULT_API_ERR_CODE + 1
    with pytest.raises(ValueError):
        _ = status_hdlr.error(
            description=DESCRIPTION,
            message=MESSAGE,
            code=err_code,
        )


def test_status_handler_ApiError(status_hdlr):
    MESSAGE = "an error occurred"
    api_err = DelBarApiError(status_code=401, message="test case")
    assert api_err.status_code == 401
    assert api_err.message == "test case"

    status_hdlr = errors.StatusHandler()
    new_err = status_hdlr.api_error(error=api_err)
    assert new_err.code == DEFAULT_API_ERR_CODE
    assert "API-Server" in new_err.description
    assert new_err.message != MESSAGE
    assert new_err.api_server_error is not None
    assert new_err.api_server_error == api_err


def test_status_handler_raise_for_status_Success(status_hdlr):
    # success: should not raise
    err_status = status_hdlr.SUCCESS
    assert status_hdlr.raise_for_status(err_status) is None


def test_status_handler_raise_for_status_PredefinedErrors(status_hdlr):
    # data related errors (predefined)
    err_status = status_hdlr.pipe_states.NO_RELIABLE_FORECAST
    err_descr = err_status.description
    with pytest.raises(errors.UDataProcessingError):
        try:
            status_hdlr.raise_for_status(err_status)
        except errors.UDataProcessingError as err:
            descr = str(err)
            assert err_descr in descr
            raise err


def test_status_handler_raise_for_status_InternalErrors(status_hdlr):
    # internal error, not data-related
    description = "test case"
    message = "an error occurred"
    err_code = 101
    err_status = status_hdlr.error(
        description=description,
        message=message,
        code=err_code,
    )
    with pytest.raises(errors.UInternalError):
        try:
            status_hdlr.raise_for_status(err_status)
        except errors.UInternalError as err:
            descr = str(err)
            assert description in descr
            raise err


def test_status_handler_raise_for_status_ExternalApiErrors(status_hdlr):
    # external API error
    api_err = DelBarApiError(status_code=401, message="test case", code="1234")
    description = "Kommunikation mit dem API-Server aufgetreten"
    msg = "Bitte beachten Sie die"
    err_status = status_hdlr.api_error(error=api_err)
    with pytest.raises(errors.UApiError):
        try:
            status_hdlr.raise_for_status(err_status)
        except errors.UApiError as err:
            descr = str(err)
            assert description in descr
            assert msg in descr
            raise err


def test_status_handler_exception_parsing():
    status_hdlr = errors.StatusHandler()
    test_message = "Test Exception"
    test_code = 110
    exception = ValueError(test_message)
    status = status_hdlr.exception_to_error(exception, test_code)
    assert status.code == test_code
    assert status.description == exception.__class__.__name__
    assert status.message == test_message


def test_NotSet():
    not_set = errors.NotSet()
    # using slots, dynamic attribute generation should not be possible
    with pytest.raises(AttributeError):
        not_set.test = "try to set value"  # type: ignore


def test_result_wrapper_class_Success():
    # successful operation
    test_result = 10
    error_code = 146
    twrapper: errors.ResultWrapper[int] = errors.ResultWrapper(
        result=test_result,
        exception=None,
        code_on_error=error_code,
    )
    assert twrapper.status == errors.STATUS_HANDLER.SUCCESS
    assert twrapper.result == test_result
    assert twrapper.unwrap() == test_result
    assert twrapper.status.code != error_code


def test_result_wrapper_class_Failure():
    # test for no result
    error_code = 146
    test_result = errors.NotSet()
    test_message = "Test of error message"
    exception = ValueError(test_message)
    twrapper: errors.ResultWrapper[int] = errors.ResultWrapper(
        result=test_result,
        exception=exception,
        code_on_error=error_code,
    )
    assert twrapper.status != errors.STATUS_HANDLER.SUCCESS
    assert twrapper.status.code == error_code
    with pytest.raises(errors.WAccessResultDespiteError):
        twrapper.result
    with pytest.raises(errors.UInternalError):
        twrapper.unwrap()


def test_result_wrapper_class_WrongCodeSpans():
    # test other error code spans
    test_result = errors.NotSet()
    test_message = "Test of error message"
    exception = ValueError(test_message)
    with pytest.raises(ValueError):
        error_code = DEFAULT_INTERNAL_ERR_CODE - 1
        _: errors.ResultWrapper[int] = errors.ResultWrapper(
            result=test_result,
            exception=exception,
            code_on_error=error_code,  # not possible because of lower bound
        )

    with pytest.raises(ValueError):
        error_code = DEFAULT_API_ERR_CODE + 1
        _: errors.ResultWrapper[int] = errors.ResultWrapper(
            result=test_result,
            exception=exception,
            code_on_error=error_code,  # not possible because of upper bound
        )


def test_wrap_result_None():
    MESSAGE = "Test case wrapped function decorator"
    error_code = 103

    @errors.wrap_result(error_code)
    def test_func_1() -> None:
        raise ValueError(MESSAGE)

    res = test_func_1()
    assert isinstance(res, errors.ResultWrapper)
    assert res.status.code == error_code
    assert res.status.description == "ValueError"
    assert res.status.message == MESSAGE
    assert res.status.api_server_error is None
    with pytest.raises(errors.UInternalError):
        res.unwrap()


def test_wrap_result_NotNone_Success():
    error_code = 103

    @errors.wrap_result(error_code)
    def test_func_2(x: str, y: str) -> int:
        return int(int(x) / int(y))

    res = test_func_2("2", "1")
    assert res.result == 2
    assert res.unwrap() == 2
    assert res.status == errors.STATUS_HANDLER.SUCCESS


def test_wrap_result_NotNone_FailureZeroDvisionError():
    error_code = 103

    @errors.wrap_result(error_code)
    def test_func_2(x: str, y: str) -> int:
        return int(int(x) / int(y))

    res = test_func_2("2", "0")
    with pytest.raises(errors.WAccessResultDespiteError):
        res.result
    with pytest.raises(errors.UInternalError):
        res.unwrap()
    assert res.status.code == error_code
    assert res.status.description == "ZeroDivisionError"


def test_wrap_result_NotNone_FailureValueError():
    error_code = 103

    @errors.wrap_result(error_code)
    def test_func_2(x: str, y: str) -> int:
        return int(int(x) / int(y))

    res = test_func_2("2", "test")
    with pytest.raises(errors.WAccessResultDespiteError):
        res.result
    with pytest.raises(errors.UInternalError):
        res.unwrap()
    assert res.status.code == error_code
    assert res.status.description == "ValueError"
