#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype decorator exception unit tests.**

This submodule unit tests the :func:`beartype.beartype` decorator with respect
to exceptions raised by the :mod:`beartype._check.error.errorget` submodule.
'''

# ....................{ IMPORTS                            }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ....................{ TESTS                              }....................
def test_decor_violation_culprits() -> None:
    '''
    Test the :func:`beartype.beartype` decorator with respect to **type-checking
    violations** (i.e., :exc:`beartype.roar.BeartypeCallHintViolation`
    exceptions raised by callables decorated by that decorator).
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from beartype import beartype
    from beartype.roar import BeartypeCallHintViolation
    from beartype.typing import List
    from pytest import raises

    # ....................{ ASSERTS ~ strong ref           }....................
    class SpiritBearIGiveYouSalmonToGoAway(object):
        '''
        Arbitrary user-defined class.
        '''

        pass

    class SpiritBearIGiftYouHoneyNotToStay(object):
        '''
        Arbitrary user-defined class.
        '''

        pass

    # Arbitrary object guaranteed to *NOT* be an instance of this class.
    #
    # Since CPython permits weak references to almost all instances of
    # user-defined classes (including both the above class and the "object"
    # superclass), this object exhibits the expected behaviour from the
    # "culprits" API exercised below that depends upon weak references.
    SPIRIT_BEAR_REFUSE_TO_GO_AWAY = SpiritBearIGiftYouHoneyNotToStay()

    @beartype
    def when_spirit_bear_hibernates_in_your_bed(
        best_bear_den: SpiritBearIGiveYouSalmonToGoAway) -> None:
        '''
        Arbitrary function decorated by :func:`beartype.beartype` annotated by a
        type hint inducing edge-case behaviour in type-checking violations
        raised by this callable.
        '''

        pass

    # Assert that calling this function with a parameter violating its type hint
    # raises the expected exception.
    with raises(BeartypeCallHintViolation) as exception_info:
        when_spirit_bear_hibernates_in_your_bed(SPIRIT_BEAR_REFUSE_TO_GO_AWAY)

    # Exception captured by the prior call to this wrapper function.
    exception = exception_info.value

    # Assert that exactly one culprit was responsible for this exception.
    assert len(exception.culprits) == 1

    # Culprit of this exception.
    culprit = exception.culprits[0]

    # Assert that this culprit is the same object passed to this function.
    assert culprit is SPIRIT_BEAR_REFUSE_TO_GO_AWAY

    # ....................{ ASSERTS ~ weak ref             }....................
    # List of lists of byte strings violating type hints annotating the
    # we_are_all_spirit_bear() function defined below.
    #
    # Since CPython prohibits weak references to both lists *AND* byte strings,
    # this data structure exhibits known flaws in the "culprits" API exercised
    # below that depends upon weak references.
    YOU_WENT_THERE_SPIRIT_BEAR = (
        b'Why do you sleep in my pinball room, Spirit Bear?')
    DO_NOT_GO_THERE_SPIRIT_BEAR = [[YOU_WENT_THERE_SPIRIT_BEAR]]

    @beartype
    def we_are_all_spirit_bear(
        best_bear_dens: List[List[str]]) -> None:
        '''
        Arbitrary function decorated by :func:`beartype.beartype` annotated by a
        type hint inducing edge-case behaviour in type-checking violations
        raised by this callable.
        '''

        pass

    # Assert that calling this function with a parameter violating its type hint
    # raises the expected exception.
    with raises(BeartypeCallHintViolation) as exception_info:
        we_are_all_spirit_bear(DO_NOT_GO_THERE_SPIRIT_BEAR)

    # Exception captured by the prior call to this wrapper function.
    exception = exception_info.value

    # Assert that exactly two culprits were responsible for this exception.
    assert len(exception.culprits) == 2

    # Root and leaf culprits of this exception.
    root_culprit = exception.culprits[0]
    leaf_culprit = exception.culprits[1]

    # Assert that both of these culprits are the machine-readable
    # representations of their underlying objects rather than those objects.
    # Why? Because CPython prohibits weak references to those objects. *sigh*
    assert isinstance(root_culprit, str)
    assert isinstance(leaf_culprit, str)
    assert root_culprit == repr(DO_NOT_GO_THERE_SPIRIT_BEAR)
    assert leaf_culprit == repr(YOU_WENT_THERE_SPIRIT_BEAR)


def test_decor_violation_types() -> None:
    '''
    Test the
    :func:`beartype.beartype` decorator with respect to the
    :attr:`beartype.BeartypeConf.violation_param_type` and
    :attr:`beartype.BeartypeConf.violation_return_type` configuration
    parameters.
    '''

    # ..................{ IMPORTS                            }..................
    # Defer test-specific imports.
    from beartype import (
        BeartypeConf,
        beartype,
    )
    from beartype.typing import (
        List,
        Tuple,
        Union,
    )
    from pytest import (
        raises,
        warns,
    )

    # ..................{ CLASSES                            }..................
    class TheVacantWoods(Exception):
        '''
        Arbitrary exception subclass.
        '''

        pass


    class SpreadRoundHim(Exception):
        '''
        Arbitrary exception subclass.
        '''

        pass


    class LowInTheWest(UserWarning):
        '''
        Arbitrary warning subclass.
        '''

        pass


    class WhereHeStood(UserWarning):
        '''
        Arbitrary warning subclass.
        '''

        pass

    # ..................{ CALLABLES                          }..................
    def the_clear_and(
        garish_hills: List[str], the_distinct_valley) -> (
        Union[int, Tuple[str, ...]]):
        '''
        Arbitrary callable to be decorated by the :func:`beartype.beartype`
        decorator, configured in various ways below.
        '''

        return the_distinct_valley

    # Beartype decorator configured to raise different types of user-defined
    # exceptions on invalid parameters and returns.
    beartype_raise = beartype(conf=BeartypeConf(
        violation_param_type=TheVacantWoods,
        violation_return_type=SpreadRoundHim,
    ))

    # Beartype decorator configured to emit different types of user-defined
    # warnings on invalid parameters and returns.
    beartype_warn = beartype(conf=BeartypeConf(
        violation_param_type=LowInTheWest,
        violation_return_type=WhereHeStood,
        # is_debug=True,
    ))

    # Above callable decorated with type-checking raising custom exceptions.
    the_clear_and_raise = beartype_raise(the_clear_and)

    # Above callable decorated with type-checking emitting custom warnings.
    the_clear_and_warn = beartype_warn(the_clear_and)

    # ..................{ CONSTANTS                          }..................
    # Arbitrary list of strings to be passed to these callables.
    WHITHER_HAVE_FLED = [
        'Low in the west, the clear and garish hills,',
        'The distinct valley and the vacant woods,',
    ]

    # Arbitrary tuple of strings to be passed to these callables.
    THE_HUES_OF_HEAVEN = (
        'Spread round him where he stood. Whither have fled',
        'The hues of heaven that canopied his bower',
    )

    # ..................{ PASS                               }..................
    # Assert that these decorated callables neither raise exceptions nor emit
    # warnings when passed valid parameters.
    assert the_clear_and_raise(WHITHER_HAVE_FLED, THE_HUES_OF_HEAVEN) is (
        THE_HUES_OF_HEAVEN)
    assert the_clear_and_warn(WHITHER_HAVE_FLED, THE_HUES_OF_HEAVEN) is (
        THE_HUES_OF_HEAVEN)

    # Assert that this decorated callable raises the expected exception that
    # this callable was configured to raise on receiving an invalid parameter.
    with raises(TheVacantWoods):
        the_clear_and_raise(THE_HUES_OF_HEAVEN, WHITHER_HAVE_FLED)

    # Assert that this decorated callable raises the expected exception that
    # this callable was configured to raise on returning an invalid return.
    with raises(SpreadRoundHim):
        the_clear_and_raise(WHITHER_HAVE_FLED, WHITHER_HAVE_FLED)

    # Assert that this decorated callable emits the expected warning that
    # this callable was configured to emit on receiving an invalid parameter.
    with warns(LowInTheWest):
        the_clear_and_warn(THE_HUES_OF_HEAVEN, THE_HUES_OF_HEAVEN)

    # Assert that this decorated callable emits the expected warning that
    # this callable was configured to emit on returning an invalid return.
    with warns(WhereHeStood):
        the_clear_and_warn(WHITHER_HAVE_FLED, WHITHER_HAVE_FLED)
