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

'''
**Beartype** :pep:`544` **unit tests.**

This submodule unit tests :pep:`544` support implemented in the
:func:`beartype.beartype` decorator.
'''

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

# ....................{ TESTS                              }....................
def test_decor_pep544() -> None:
    '''
    Test :pep:`544` support implemented in the :func:`beartype.beartype`
    decorator.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from beartype.roar import BeartypeDecorHintPep3119Exception
    from pytest import raises
    from typing import Protocol, runtime_checkable

    # ....................{ PROTOCOLS                      }....................
    @runtime_checkable
    class Easter1916(Protocol):
        '''
        User-defined runtime protocol declaring arbitrary methods.
        '''

        def i_have_met_them_at_close_of_day(self) -> str:
            return 'Coming with vivid faces'

        @abstractmethod
        def from_counter_or_desk_among_grey(self) -> str: pass


    class Easter1916Structural(object):
        '''
        User-defined class structurally (i.e., implicitly) satisfying *without*
        explicitly subclassing this user-defined protocol.
        '''

        def i_have_met_them_at_close_of_day(self) -> str:
            return 'Eighteenth-century houses.'

        def from_counter_or_desk_among_grey(self) -> str:
            return 'I have passed with a nod of the head'


    class TwoTrees(Protocol):
        '''
        User-defined protocol declaring arbitrary methods, but intentionally
        *not* decorated by the :func:`typing.runtime_checkable` decorator and
        thus unusable at runtime by :mod:`beartype`.
        '''

        def holy_tree(self) -> str:
            return 'Beloved, gaze in thine own heart,'

        @abstractmethod
        def bitter_glass(self) -> str: pass

    # ....................{ FUNCTIONS                      }....................
    # @beartype-decorated callable annotated by this user-defined protocol.
    @beartype
    def or_polite_meaningless_words(lingered_awhile: Easter1916) -> str:
        return (
            lingered_awhile.i_have_met_them_at_close_of_day() +
            lingered_awhile.from_counter_or_desk_among_grey()
        )

    # ....................{ PASS                           }....................
    # Assert this callable returns the expected string when passed this
    # user-defined class structurally satisfying this protocol.
    assert or_polite_meaningless_words(Easter1916Structural()) == (
        'Eighteenth-century houses.'
        'I have passed with a nod of the head'
    )

    # ....................{ FAIL                           }....................
    # @beartype-decorated callable annotated by this user-defined protocol.
    with raises(BeartypeDecorHintPep3119Exception):
        @beartype
        def times_of_old(god_slept: TwoTrees) -> str:
            return god_slept.holy_tree() + god_slept.bitter_glass()

# ....................{ TESTS ~ protocol                   }....................
def test_decor_pep544_hint_subprotocol_elision() -> None:
    '''
    Test that type-checking wrappers generated by the :func:`beartype.beartype`
    decorator for callables annotated by one or more **subprotocols** (i.e.,
    :pep:`544`-compliant subclasses subclassing one or more
    :pep:`544`-compliant superclasses themselves directly subclassing the root
    :class:`typing.Protocol` superclass) are optimized to perform only single
    :func:`isinstance` checks against those subprotocols rather than multiple
    :func:`isinstance` checks against both those subprotocols and one or more
    superclasses of those subprotocols.

    This unit test guards against non-trivial performance regressions that
    previously crippled the third-party :mod:`numerary` package, whose caching
    algorithm requires that :mod:`beartype` be optimized in this way.

    See Also
    --------
    This relevant issue: https://github.com/beartype/beartype/issues/76
    '''

    # ..................{ IMPORTS                            }..................
    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from typing import (
        Protocol,
        runtime_checkable,
    )

    # Private metaclass of the root "typing.Protocol" superclass, accessed
    # without violating privacy encapsulation.
    _ProtocolMeta = type(Protocol)

    # ..................{ METACLASSES                        }..................
    class UndesirableProtocolMeta(_ProtocolMeta):
        '''
        Undesirable :class:`typing.Protocol`-compliant metaclass additionally
        recording when classes with this metaclass are passed as the second
        parameter to the :func:`isinstance` builtin, enabling subsequent logic
        to validate this is *not* happening as expected.
        '''

        def __instancecheck__(cls, obj: object) -> bool:
            '''
            Unconditionally return ``False``.

            Additionally, this dunder method unconditionally records that this
            method has been called by setting an instance variable of the
            passed object set only by this method.
            '''

            # Set an instance variable of this object set only by this method.
            obj.is_checked_by_undesirable_protocol_meta = True

            # Print a string to standard output as an additional sanity check.
            print("Save when the eagle brings some hunter's bone,")

            # Unconditionally return false.
            return False


    class DesirableProtocolMeta(UndesirableProtocolMeta):
        '''
        Desirable :class:`typing.Protocol`-compliant metaclass.
        '''

        def __instancecheck__(cls, obj: object) -> bool:
            '''
            Unconditionally return ``True``.
            '''

            # Unconditionally return true.
            return True

    # ..................{ PROTOCOLS                          }..................
    @runtime_checkable
    class PileAroundIt(
        Protocol,
        metaclass=UndesirableProtocolMeta,
    ):
        '''
        Arbitrary protocol whose metaclass is undesirable.
        '''

        @abstractmethod
        def ice_and_rock(self, broad_vales_between: str) -> str:
            pass


    @runtime_checkable
    class OfFrozenFloods(
        PileAroundIt,
        Protocol,  # <-- Subprotocols *MUST* explicitly relist this. /facepalm/
        metaclass=DesirableProtocolMeta,
    ):
        '''
        Arbitrary subprotocol of the protocol declared above whose metaclass
        has been replaced with a more desirable metaclass.
        '''

        @abstractmethod
        def and_wind(self, among_the_accumulated_steeps: str) -> str:
            pass

    # ..................{ STRUCTURAL                         }..................
    class ADesertPeopledBy(object):
        '''
        Arbitrary concrete class structurally satisfying *without* subclassing
        the subprotocol declared above.
        '''

        def and_wind(self, among_the_accumulated_steeps: str) -> str:
            return among_the_accumulated_steeps + 'how hideously'

        def ice_and_rock(self, broad_vales_between: str) -> str:
            return broad_vales_between + 'unfathomable deeps,'

    # Arbitrary instance of this class.
    the_storms_alone = ADesertPeopledBy()

    # ..................{ BEARTYPE                           }..................
    @beartype
    def blue_as_the_overhanging_heaven(that_spread: OfFrozenFloods) -> str:
        '''
        Arbitrary callable annotated by the subprotocol declared above.
        '''

        return that_spread.ice_and_rock('Of frozen floods, ')

    # Assert this callable returns the expected string.
    assert blue_as_the_overhanging_heaven(that_spread=the_storms_alone) == (
        'Of frozen floods, unfathomable deeps,')

    # Critically, assert the instance variable *ONLY* defined by the
    # "UndesirableProtocolMeta" metaclass to remain undefined, implying the
    # DesirableProtocolMeta.__instancecheck__() rather than
    # UndesirableProtocolMeta.__instancecheck__() dunder method to have been
    # implicitly called by the single isinstance() call in the body of the
    # type-checking wrapper generated by @beartype above.
    assert not hasattr(
        the_storms_alone, 'is_checked_by_undesirable_protocol_meta')

# ....................{ TESTS ~ custom : direct            }....................
def test_typingpep544_protocol_custom_direct() -> None:
    '''
    Test the core operation of the public :class:`beartype.typing.Protocol`
    subclass with respect to user-defined :pep:`544`-compliant protocols
    directly subclassing :class:`beartype.typing.Protocol` under the
    :func:`beartype.beartype` decorator.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from beartype.roar import (
        BeartypeCallHintParamViolation,
        BeartypeCallHintReturnViolation,
    )
    from beartype.typing import (
        Protocol,
        Tuple,
    )
    from pytest import raises

    # ....................{ CLASSES                        }....................
    class SupportsFish(Protocol):
        '''
        Arbitrary direct protocol.
        '''

        @abstractmethod
        def fish(self) -> int:
            pass


    class OneFish:
        '''
        Arbitrary class satisfying this protocol *without* explicitly
        subclassing this protocol.
        '''

        def fish(self) -> int:
            return 1


    class TwoFish:
        '''
        Arbitrary class satisfying this protocol *without* explicitly
        subclassing this protocol.
        '''

        def fish(self) -> int:
            return 2


    class RedSnapper:
        '''
        Arbitrary class violating this protocol.
        '''

        def oh(self) -> str:
            return 'snap'

    # ....................{ CALLABLES                      }....................
    @beartype
    def _supports_fish_identity(arg: SupportsFish) -> SupportsFish:
        '''
        Arbitrary :func:`beartype.beartype`-decorated callable validating both
        parameters and returns to be instances of arbitrary classes satisfying
        this protocol.
        '''

        return arg


    @beartype
    def _lies_all_lies(arg: SupportsFish) -> Tuple[str]:
        '''
        Arbitrary :func:`beartype.beartype`-decorated callable guaranteed to
        *ALWAYS* raise a violation by returning an object that *never* satisfies
        its type hint.
        '''

        return (arg.fish(),)  # type: ignore [return-value]

    # ....................{ PASS                           }....................
    # Assert that instances of classes satisfying this protocol *WITHOUT*
    # subclassing this protocol satisfy @beartype validation as expected.
    assert isinstance(_supports_fish_identity(OneFish()), SupportsFish)
    assert isinstance(_supports_fish_identity(TwoFish()), SupportsFish)

    # ....................{ FAIL                           }....................
    # Assert that instances of classes violating this protocol violate
    # @beartype validation as expected.
    with raises(BeartypeCallHintParamViolation):
        _supports_fish_identity(RedSnapper())  # type: ignore [arg-type]

    # Assert this callable raises the expected exception when passed an
    # instance of a class otherwise satisfying this protocol.
    with raises(BeartypeCallHintReturnViolation):
        _lies_all_lies(OneFish())


def test_typingpep544_protocol_custom_direct_typevar() -> None:
    '''
    Test the core operation of the public :class:`beartype.typing.Protocol`
    subclass with respect to user-defined :pep:`544`-compliant protocols
    directly subclassing :class:`beartype.typing.Protocol` and parametrized by
    one or more type variables under the :func:`beartype.beartype` decorator.
    '''

    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from beartype.typing import (
        Any,
        Protocol,
        TypeVar,
        runtime_checkable,
    )

    # Arbitrary type variable.
    _T_co = TypeVar('_T_co', covariant=True)

    # Arbitrary direct protocol subscripted by this type variable.
    @runtime_checkable
    class SupportsAbsToo(Protocol[_T_co]):
        __slots__: Any = ()

        @abstractmethod
        def __abs__(self) -> _T_co:
            pass

    # Arbitrary @beartype-decorated callable validating a parameter to be an
    # instance of arbitrary classes satisfying this protocol.
    @beartype
    def myabs(arg: SupportsAbsToo[_T_co]) -> _T_co:
        return abs(arg)

    # Assert @beartype wrapped this callable with the expected type-checking.
    assert myabs(-1) == 1

# ....................{ TESTS ~ custom : indirect          }....................
def test_typingpep544_protocol_custom_indirect() -> None:
    '''
    Test the core operation of the public :class:`beartype.typing.Protocol`
    subclass with respect to user-defined :pep:`544`-compliant protocols
    indirectly subclassing :class:`beartype.typing.Protocol` under the
    :func:`beartype.beartype` decorator.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from beartype.roar import (
        BeartypeCallHintParamViolation,
        BeartypeCallHintReturnViolation,
    )
    from beartype.typing import (
        Protocol,
        Tuple,
    )
    from pytest import raises

    # ....................{ CLASSES                        }....................
    # Arbitrary direct protocol.
    class SupportsFish(Protocol):
        @abstractmethod
        def fish(self) -> int:
            pass

    # Arbitrary indirect protocol subclassing the above direct protocol.
    class SupportsCod(SupportsFish):
        @abstractmethod
        def dear_cod(self) -> str:
            pass

    # Arbitrary classes satisfying this protocol *WITHOUT* explicitly
    # subclassing this protocol.
    class OneCod:
        def fish(self) -> int:
            return 1

        def dear_cod(self) -> str:
            return 'Not bad, cod do better...'

    class TwoCod:
        def fish(self) -> int:
            return 2

        def dear_cod(self) -> str:
            return "I wouldn't be cod dead in that."

    # Arbitrary classes violating this protocol.
    class PacificSnapper:
        # def fish(self) -> int:
        #     return 0xFEEDBEEF

        def dear_cod(self) -> str:
            return 'Cod you pass the butterfish?'

        def berry_punny(self) -> str:
            return 'Had a girlfriend, I lobster. But then I flounder!'

    # ....................{ CALLABLES                      }....................
    # Arbitrary @beartype-decorated callable validating both parameters and
    # returns to be instances of arbitrary classes satisfying this protocol.
    @beartype
    def _supports_cod_identity(arg: SupportsCod) -> SupportsCod:
        return arg

    # Arbitrary @beartype-decorated callable guaranteed to *ALWAYS* raise a
    # violation by returning an object that *NEVER* satisfies its type hint.
    @beartype
    def _lies_all_lies(arg: SupportsCod) -> Tuple[int]:
        return (arg.dear_cod(),)  # type: ignore [return-value]

    # ....................{ PASS                           }....................
    # Assert that instances of classes satisfying this protocol *WITHOUT*
    # subclassing this protocol satisfy @beartype validation as expected.
    assert isinstance(_supports_cod_identity(OneCod()), SupportsCod)
    assert isinstance(_supports_cod_identity(TwoCod()), SupportsCod)

    # ....................{ FAIL                           }....................
    # Assert that instances of classes violating this protocol violate
    # @beartype validation as expected.
    with raises(BeartypeCallHintParamViolation):
        _supports_cod_identity(PacificSnapper())  # type: ignore [arg-type]

    # Assert this callable raises the expected exception when passed an
    # instance of a class otherwise satisfying this protocol.
    with raises(BeartypeCallHintReturnViolation):
        _lies_all_lies(OneCod())

# ....................{ TESTS ~ pep 593                    }....................
def test_typingpep544_pep593_integration() -> None:
    '''
    Test the public :class:`beartype.typing.Protocol` subclass when nested
    within a :pep:`593`-compliant :obj:`typing.Annotated` type hint.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from abc import abstractmethod
    from beartype import beartype
    from beartype.roar import BeartypeException
    from beartype.typing import (
        Annotated,
        Protocol,
    )
    from beartype.vale import Is
    from pytest import raises

    # ....................{ CLASSES                        }....................
    class SupportsOne(Protocol):
        @abstractmethod
        def one(self) -> int:
            pass

    class TallCoolOne:
        def one(self) -> int:
            return 1

    class FalseOne:
        def one(self) -> int:
            return 0

    class NotEvenOne:
        def two(self) -> str:
            return "two"

    # ....................{ CALLABLES                      }....................
    @beartype
    def _there_can_be_only_one(
        n: Annotated[SupportsOne, Is[lambda x: x.one() == 1]],  # type: ignore[name-defined]
    ) -> int:
        val = n.one()
        assert val == 1  # <-- should never fail because it's caught by beartype first
        return val

    # ....................{ PASS                           }....................
    _there_can_be_only_one(TallCoolOne())

    # ....................{ FAIL                           }....................
    with raises(BeartypeException):
        _there_can_be_only_one(FalseOne())

    with raises(BeartypeException):
        _there_can_be_only_one(NotEvenOne())  # type: ignore [arg-type]
