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

'''
Testing-specific **type hint metadata class hierarchy** (i.e., hierarchy of
classes encapsulating sample type hints instantiated by the
:mod:`beartype_test.a00_unit.data.hint` submodules).
'''

# ....................{ IMPORTS                            }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype.typing import (
    Iterable,
    Optional,
    Tuple,
    Type,
    TypeVar,
)
from beartype._conf.confmain import BeartypeConf
from beartype._conf.confcommon import BEARTYPE_CONF_DEFAULT
from collections.abc import Iterable as IterableABC

# ....................{ CLASSES ~ hint : [un]satisfied     }....................
class HintPithSatisfiedMetadata(object):
    '''
    **Type hint satisfied pith metadata** (i.e., dataclass whose instance
    variables describe an object satisfying a type hint when either passed as a
    parameter *or* returned as a value annotated by that hint).

    Attributes
    ----------
    pith : object
        Arbitrary object *not* satisfying this hint when either passed as a
        parameter *or* returned as a value annotated by this hint.
    is_context_manager : bool
        If this pith is a **context manager** (i.e., object defining both the
        ``__exit__`` and ``__enter__`` dunder methods required to satisfy the
        context manager protocol), this boolean is either:

        * :data:`True` if callers should preserve this context manager as is
          (e.g., by passing this context manager to the decorated callable).
        * :data:`False` if callers should safely open and close this context
          manager to its context *and* replace this context manager with that
          context (e.g., by passing this context to the decorated callable).

        If this pith is *not* a context manager, this boolean is ignored.
        Defaults to :data:`False`.
    is_pith_factory : bool
        :data:`True` only if this pith is actually a **pith factory** (i.e.,
        callable accepting *no* parameters and dynamically creating and
        returning the value to be used as the desired pith, presumably by
        passing this value to the decorated callable). Defaults to
        :data:`False`.
    '''

    # ..................{ INITIALIZERS                       }..................
    def __init__(
        self,

        # Mandatory parameters.
        pith: object,

        # Optional parameters.
        is_context_manager: bool = False,
        is_pith_factory: bool = False,
    ) -> None:
        assert isinstance(is_context_manager, bool), (
            f'{repr(is_context_manager)} not boolean.')
        assert isinstance(is_pith_factory, bool), (
            f'{repr(is_pith_factory)} not boolean.')

        # Classify all passed parameters.
        self.pith = pith
        self.is_context_manager = is_context_manager
        self.is_pith_factory = is_pith_factory

    # ..................{ STRINGIFIERS                       }..................
    def __repr__(self) -> str:
        return '\n'.join((
            f'{self.__class__.__name__}(',
            f'    pith={repr(self.pith)},',
            f'    is_context_manager={repr(self.is_context_manager)},',
            f'    is_pith_factory={repr(self.is_pith_factory)},',
            f')',
        ))


class HintPithUnsatisfiedMetadata(HintPithSatisfiedMetadata):
    '''
    **Type hint unsatisfied pith metadata** (i.e., dataclass whose instance
    variables describe an object *not* satisfying a type hint when either
    passed as a parameter *or* returned as a value annotated by that hint).

    Attributes
    ----------
    exception_str_match_regexes : Iterable[str]
        Iterable of zero or more r''-style uncompiled regular expression
        strings, each matching a substring of the exception message expected to
        be raised by wrapper functions when either passed or returning this
        ``pith``. Defaults to the empty tuple.
    exception_str_not_match_regexes : Iterable[str]
        Iterable of zero or more r''-style uncompiled regular expression
        strings, each *not* matching a substring of the exception message
        expected to be raised by wrapper functions when either passed or
        returning this ``pith``. Defaults to the empty tuple.
    '''

    # ..................{ INITIALIZERS                       }..................
    def __init__(
        self,
        *args,

        # Optional parameters.
        exception_str_match_regexes: Iterable[str] = (),
        exception_str_not_match_regexes: Iterable[str] = (),
        **kwargs
    ) -> None:
        assert isinstance(exception_str_match_regexes, IterableABC), (
            f'{repr(exception_str_match_regexes)} not iterable.')
        assert isinstance(exception_str_not_match_regexes, IterableABC), (
            f'{repr(exception_str_not_match_regexes)} not iterable.')
        assert all(
            isinstance(exception_str_match_regex, str)
            for exception_str_match_regex in exception_str_match_regexes
        ), f'{repr(exception_str_match_regexes)} not iterable of regexes.'
        assert all(
            isinstance(exception_str_not_match_regex, str)
            for exception_str_not_match_regex in (
                exception_str_not_match_regexes)
        ), f'{repr(exception_str_not_match_regexes)} not iterable of regexes.'

        # Initialize our superclass with all variadic parameters.
        super().__init__(*args, **kwargs)

        # Classify all remaining passed parameters.
        self.exception_str_not_match_regexes = exception_str_not_match_regexes

        # Classify the tuple of all r''-style uncompiled regular expression
        # strings appended by the tuple of all mandatory such strings.
        self.exception_str_match_regexes = (
            exception_str_match_regexes +
            _EXCEPTION_STR_MATCH_REGEXES_MANDATORY
        )

    # ..................{ STRINGIFIERS                       }..................
    def __repr__(self) -> str:
        return '\n'.join((
            f'{self.__class__.__name__}(',
            f'    pith={repr(self.pith)},',
            f'    is_context_manager={repr(self.is_context_manager)},',
            f'    is_pith_factory={repr(self.is_pith_factory)},',
            f'    exception_str_match_regexes={repr(self.exception_str_match_regexes)},',
            f'    exception_str_not_match_regexes={repr(self.exception_str_not_match_regexes)},',
            f')',
        ))

# ....................{ CLASSES ~ hint : superclass        }....................
class HintNonpepMetadata(object):
    '''
    **PEP-noncompliant type hint metadata** (i.e., dataclass whose instance
    variables describe a type hint that is either PEP-noncompliant or *mostly*
    indistinguishable from a PEP-noncompliant type hint with metadata
    applicable to various testing scenarios).

    Examples of PEP-compliant type hints *mostly* indistinguishable from
    PEP-noncompliant type hints include:

    * :func:`typing.NamedTuple`, a high-level factory function deferring to the
      lower-level :func:`collections.namedtuple` factory function creating and
      returning :class:`tuple` instances annotated by PEP-compliant type hints.
    * :func:`typing.TypedDict`, a high-level factory function creating and
      returning :class:`dict` instances annotated by PEP-compliant type hints.

    Attributes
    ----------
    hint : object
        Type hint to be tested.
    conf : BeartypeConf
        **Beartype configuration** (i.e., self-caching dataclass encapsulating
        all settings configuring type-checking for this type hint).
    is_ignorable : bool
        :data:`True` only if this hint is safely ignorable by the
        :func:`beartype.beartype` decorator. Defaults to :data:`False`.
    is_needs_cls_stack : bool
        :data:`True` only if this hint is **type stack-dependent** (i.e., if
        :mod:`beartype` requires the tuple of all classes lexically declaring
        the class variables or methods annotated by this hint to generate code
        type-checking this hint). Defaults to :data:`False`.
    is_supported : bool
        :data:`True` only if this hint is currently supported by
        the :func:`beartype.beartype` decorator. Defaults to :data:`False`.
    piths_meta : Iterable[HintPithSatisfiedMetadata]
        Iterable of zero or more **(un)satisfied metadata objects** (i.e.,
        :class:`HintPithSatisfiedMetadata` and
        :class:`HintPithUnsatisfiedMetadata` instances), each describing an
        arbitrary object either satisfying or violating this hint when passed as
        a parameter *or* returned as a value annotated by this hint. Defaults to
        the empty tuple.
    warning_type : Optional[Type[Warning]]
        Either:

        * If the :func:`beartype.beartype` decorator unconditionally emits a
          non-fatal warning when this type hint annotates *any* parameter or
          return of *any* callable, the type of that warning.
        * Else, :data:`None`.
    '''

    # ..................{ INITIALIZERS                       }..................
    def __init__(
        self,
        *,

        # Mandatory keyword-only parameters.
        hint: object,

        # Optional keyword-only parameters.
        conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
        is_ignorable: bool = False,
        is_needs_cls_stack: bool = False,
        is_supported: bool = True,
        piths_meta: Iterable[HintPithSatisfiedMetadata] = (),
        warning_type: Optional[Type[Warning]] = None,
    ) -> None:

        # Validate passed non-variadic parameters.
        assert isinstance(conf, BeartypeConf), (
            f'{repr(conf)} not configuration.')
        assert isinstance(is_ignorable, bool), (
            f'{repr(is_ignorable)} not bool.')
        assert isinstance(is_needs_cls_stack, bool), (
            f'{repr(is_needs_cls_stack)} not bool.')
        assert isinstance(is_supported, bool), (
            f'{repr(is_supported)} not bool.')
        assert isinstance(piths_meta, IterableABC), (
            f'{repr(piths_meta)} not iterable.')
        assert all(
            isinstance(piths_meta, HintPithSatisfiedMetadata)
            for piths_meta in piths_meta
        ), (
            f'{repr(piths_meta)} not iterable of '
            f'"HintPithSatisfiedMetadata" and '
            f'"HintPithUnsatisfiedMetadata" instances.')
        assert isinstance(warning_type, _NoneTypeOrType), (
            f'{repr(warning_type)} neither class nor "None".')

        # Classify all passed parameters.
        self.hint = hint
        self.conf = conf
        self.is_ignorable = is_ignorable
        self.is_needs_cls_stack = is_needs_cls_stack
        self.is_supported = is_supported
        self.piths_meta = piths_meta
        self.warning_type = warning_type

    # ..................{ DUNDERS                            }..................
    def __repr__(self) -> str:
        return '\n'.join((
            f'{self.__class__.__name__}(',
            f'    hint={repr(self.hint)},',
            f'    conf={repr(self.conf)},',
            f'    is_ignorable={repr(self.is_ignorable)},',
            f'    is_needs_cls_stack={repr(self.is_needs_cls_stack)},',
            f'    is_supported={repr(self.is_supported)},',
            f'    piths_meta={repr(self.piths_meta)},',
            f'    warning_type={repr(self.warning_type)},',
            f')',
        ))

# ....................{ CLASSES ~ hint : subclass          }....................
class HintPepMetadata(HintNonpepMetadata):
    '''
    **PEP-compliant type hint metadata** (i.e., dataclass whose instance
    variables describe a PEP-compliant type hint with metadata applicable to
    various testing scenarios).

    Attributes
    ----------
    pep_sign : HintSign
        **Sign** (i.e., arbitrary object uniquely identifying this
        PEP-compliant type hint) if this hint is uniquely identified by such a
        sign *or* ``None`` otherwise. Examples of PEP-compliant type hints
        *not* uniquely identified by such attributes include those reducing to
        standard builtins on instantiation such as:

        * :class:`typing.NamedTuple` reducing to :class:`tuple`.
        * :class:`typing.TypedDict` reducing to :class:`dict`.
    is_args : bool, optional
        :data:`True` only if this hint is subscripted by one or more
        **arguments** (i.e., PEP-compliant type hints that are *not* type
        variables) and/or **type variables** (i.e., :class:`typing.TypeVar`
        instances). Defaults to :data:`True` only if the machine-readable
        representation of this hint contains one or more "[" delimiters.
    is_pep585_builtin_subbed : bool, optional
        :data:`True` only if this hint is a :pep:`585`-compliant builtin. If
        :data:`True`, then :attr:`is_type_typing` *must* be :data:`False`.
        Defaults to the negation of :attr:`is_pep585_generic` if
        non-:data:`None` *or* :data:`False` otherwise (i.e., if
        :attr:`is_pep585_generic` is :data:`None`).
    is_pep585_generic : bool, optional
        :data:`True` only if this hint is a :pep:`585`-compliant generic. If
        :data:`True`, then :attr:`is_type_typing` *must* be :data:`False`.
        Defaults to :data:`False`.
    is_typeargs : bool, optional
        :data:`True` only if this hint is subscripted by one or more **type
        variables** (i.e., :class:`typing.TypeVar` instances). Defaults to
        ``bool(typeargs_packed)``.
    is_type_typing : bool, optional
        :data:`True` only if this hint's class is defined by the :mod:`typing`
        module. If ``True``, then :attr:`is_pep585_builtin_subbed` and
        :attr:`is_pep585_generic` *must* both be ``False``. Defaults to
        either:

        * If either :attr:`is_pep585_builtin_subbed` *or*
          :attr:`is_pep585_generic` are :data:`True`, :data:`False`.
        * Else, :data:`True`.
    is_typing : bool, optional
        :data:`True` only if this hint itself is defined by the :mod:`typing`
        module. Defaults to :attr:`is_type_typing`.
    isinstanceable_type : Optional[type]
        **Origin type** (i.e., non-:mod:`typing` class such that *all* objects
        satisfying this hint are instances of this class) originating this hint
        if this hint originates from a non-:mod:`typing` class *or* :data:`None`
        otherwise (i.e., if this hint does *not* originate from such a class).
        Defaults to :data:`None`.
    generic_type : Optional[type]
        Subscripted origin type associated with this hint if any *or*
        :data:`None` otherwise (i.e., if this hint is associated with *no* such
        type). Defaults to either:

        * If this hint is subscripted, :attr:`isinstanceable_type`.
        * Else, :data:`None`.
    typehint_cls : Optional[Type[beartype.door.TypeHint]]
        Concrete :class:`beartype.door.TypeHint` subclass responsible for
        handling this hint if any *or* :data:`None` otherwise (e.g., if the
        :mod:`beartype.door` submodule has yet to support this hint).
    typeargs_packed : Tuple[TypeVar, ...], optional
        Tuple of the zero or more :pep:`484`-compliant **type variables** (i.e.,
        :class:`typing.TypeVar` objects) parametrizing this hint. Defaults to
        the empty tuple, implying this hint to be parametrized by *no* type
        variables.

    All remaining keyword arguments are passed as is to the superclass
    :meth:`HintNonpepMetadata.__init__` method.
    '''

    # ..................{ INITIALIZERS                       }..................
    def __init__(
        self,
        *,

        # Mandatory keyword-only parameters.
        pep_sign: 'beartype._data.hint.sign.datahintsigncls.HintSign',

        # Optional keyword-only parameters.
        is_args: Optional[bool] = None,
        is_pep585_builtin_subbed: Optional[bool] = None,
        is_pep585_generic: Optional[bool] = None,
        is_typeargs: bool = None,  # type: ignore[assignment]
        is_type_typing: Optional[bool] = None,
        is_typing: Optional[bool] = None,
        isinstanceable_type: Optional[type] = None,
        generic_type: Optional[type] = None,
        typehint_cls: Optional[Type['beartype.door.TypeHint']] = None,
        typeargs_packed: 'TuplePep484612646TypeArgsPacked' = (),
        **kwargs
    ) -> None:

        # Defer test-specific imports.
        from beartype._data.hint.sign.datahintsigncls import HintSign
        from beartype._util.hint.utilhintget import get_hint_repr
        from beartype._util.hint.pep.proposal.pep484612646 import (
            is_hint_pep484612646_typearg_packed)
        from beartype.door import TypeHint

        # Validate passed non-variadic parameters.
        assert isinstance(pep_sign, HintSign), f'{repr(pep_sign)} not sign.'
        assert isinstance(isinstanceable_type, _NoneTypeOrType), (
            f'{repr(isinstanceable_type)} neither class nor "None".')
        assert isinstance(typeargs_packed, tuple), f'{repr(typeargs_packed)} not tuple.'
        assert all(
            is_hint_pep484612646_typearg_packed(typevar)
            for typevar in typeargs_packed
        ), (
            f'{repr(typeargs_packed)} not tuple of type variables.')

        # Initialize our superclass with all remaining variadic parameters.
        super().__init__(**kwargs)

        # Machine-readable representation of this hint.
        hint_repr = get_hint_repr(self.hint)

        # Conditionally default all unpassed parameters.
            # Default this parameter to true only if the machine-readable
            # representation of this hint contains "[": e.g., "List[str]".
        if is_args is None:
            is_args = '[' in hint_repr
        # Default this parameter to true only if...
        if is_pep585_builtin_subbed is None:
            is_pep585_builtin_subbed = (
                # This hint originates from an origin type *AND*...
                isinstanceable_type is not None and
                # The machine-readable representation of this hint is prefixed
                # by the unqualified name of this origin type (e.g., "list[str]
                # " is prefixed by "list"), suggesting this hint to be a PEP
                # 585-compliant builtin.
                hint_repr.startswith(isinstanceable_type.__name__)
            )
            # print(f'is_pep585_builtin_subbed: {is_pep585_builtin_subbed}')
            # print(f'hint_repr: {hint_repr}')
            # print(f'isinstanceable_type.__name__: {isinstanceable_type.__name__}')
        # Default this parameter to false, because we can't think of anything
        # better.
        if is_pep585_generic is None:
            is_pep585_generic = False
        # Default this parameter to the negation of all PEP 585-compliant
        # boolean parameters. By definition, PEP 585-compliant type hints are
        # *NOT* defined by the "typing" module and vice versa.
        if is_type_typing is None:
            is_type_typing = not (
                is_pep585_builtin_subbed or is_pep585_generic)
        # Default this parameter to true only if this hint is expected to be
        # parametrized by one or more type variables.
        if is_typeargs is None:
            is_typeargs = bool(typeargs_packed)
        # Default this parameter to true only if this hint's class is
        # defined by the "typing" module.
        if is_typing is None:
            is_typing = is_type_typing
        # Default this parameter to this hint's type origin only if this
        # hint is subscripted.
        if generic_type is None:
            generic_type = isinstanceable_type if is_args else None

        # Defer validating parameters defaulting to "None" until *AFTER*
        # initializing these parameters above.
        assert isinstance(is_args, bool), (
            f'{repr(is_args)} not bool.')
        assert isinstance(is_pep585_builtin_subbed, bool), (
            f'{repr(is_pep585_builtin_subbed)} not bool.')
        assert isinstance(is_pep585_generic, bool), (
            f'{repr(is_pep585_generic)} not bool.')
        assert isinstance(is_type_typing, bool), (
            f'{repr(is_type_typing)} not bool.')
        assert isinstance(is_typeargs, bool), (
            f'{repr(is_typeargs)} not bool.')
        assert isinstance(is_typing, bool), (
            f'{repr(is_typing)} not bool.')
        assert isinstance(generic_type, _NoneTypeOrType), (
            f'{repr(generic_type)} neither class nor "None".')
        assert isinstance(generic_type, _NoneTypeOrType), (
            f'{repr(generic_type)} neither class nor "None".')
        assert (
            typehint_cls is None or (
                isinstance(typehint_cls, type) and
                issubclass(typehint_cls, TypeHint),
            )
        ), (
            f'{repr(typehint_cls)} neither '
            f'"beartype.door.TypeHint" subclass nor "None".'
        )

        # Validate that the "is_pep585_builtin_subbed" and "is_type_typing"
        # parameters are *NOT* both true. Note, however, that both can be false
        # (e.g., for PEP 484-compliant user-defined generics).
        assert not (
            (is_pep585_builtin_subbed or is_pep585_generic) and is_type_typing), (
            f'Mutually incompatible boolean parameters '
            f'is_type_typing={repr(is_type_typing)} and either '
            f'is_pep585_builtin_subbed={repr(is_pep585_builtin_subbed)} or '
            f'is_pep585_generic={repr(is_pep585_generic)} enabled.'
        )

        # Classify all passed parameters.
        self.generic_type = generic_type
        self.is_args = is_args
        self.is_pep585_builtin_subbed = is_pep585_builtin_subbed
        self.is_pep585_generic = is_pep585_generic
        self.is_typeargs = is_typeargs
        self.is_type_typing = is_type_typing
        self.is_typing = is_typing
        self.isinstanceable_type = isinstanceable_type
        self.pep_sign = pep_sign
        self.typehint_cls = typehint_cls
        self.typeargs_packed = typeargs_packed

    # ..................{ DUNDERS                            }..................
    def __repr__(self) -> str:
        return '\n'.join((
            f'{self.__class__.__name__}(',
            f'    hint={repr(self.hint)},',
            f'    conf={repr(self.conf)},',
            f'    pep_sign={repr(self.pep_sign)},',
            f'    typehint_cls={repr(self.typehint_cls)},',
            f'    generic_type={repr(self.generic_type)},',
            f'    isinstanceable_type={repr(self.isinstanceable_type)},',
            f'    is_args={repr(self.is_args)},',
            f'    is_ignorable={repr(self.is_ignorable)},',
            f'    is_needs_cls_stack={repr(self.is_needs_cls_stack)},',
            f'    is_pep585_builtin_subbed={repr(self.is_pep585_builtin_subbed)},',
            f'    is_pep585_generic={repr(self.is_pep585_generic)},',
            f'    is_supported={repr(self.is_supported)},',
            f'    is_typeargs={repr(self.is_typeargs)},',
            f'    is_type_typing={repr(self.is_type_typing)},',
            f'    is_typing={repr(self.is_typing)},',
            f'    piths_meta={repr(self.piths_meta)},',
            f'    typeargs_packed={repr(self.typeargs_packed)},',
            f'    warning_type={repr(self.warning_type)},',
            f')',
        ))

# ....................{ PRIVATE ~ constants                }....................
_EXCEPTION_STR_MATCH_REGEXES_MANDATORY = (
    # Ensure *ALL* exception messages contain the substring "type hint".
    # Exception messages *NOT* containing this substring are overly ambiguous
    # and thus effectively erroneous.
    r'\btype hint\b',
)
'''
Tuple of all **mandatory exception matching regexes** (i.e., r''-style
uncompiled regular expression strings, each unconditionally matching a
substring of the exception message expected to be raised by wrapper functions
when either passed or returning *any* possible pith).
'''


_NoneTypeOrType = (type, type(None))
'''
2-tuple matching both classes and the :data:`None` singleton.
'''
