# SPDX-License-Identifier: MIT

"""
Testing strategies for Hypothesis-based tests.
"""

import keyword
import string

from collections import OrderedDict

from hypothesis import strategies as st

import attr

from .utils import make_class


optional_bool = st.one_of(st.none(), st.booleans())


def gen_attr_names():
    """
    Generate names for attributes, 'a'...'z', then 'aa'...'zz'.

    ~702 different attribute names should be enough in practice.

    Some short strings (such as 'as') are keywords, so we skip them.
    """
    lc = string.ascii_lowercase
    yield from lc
    for outer in lc:
        for inner in lc:
            res = outer + inner
            if keyword.iskeyword(res):
                continue
            yield outer + inner


def maybe_underscore_prefix(source):
    """
    A generator to sometimes prepend an underscore.
    """
    to_underscore = False
    for val in source:
        yield val if not to_underscore else "_" + val
        to_underscore = not to_underscore


@st.composite
def _create_hyp_nested_strategy(draw, simple_class_strategy):
    """
    Create a recursive attrs class.

    Given a strategy for building (simpler) classes, create and return
    a strategy for building classes that have as an attribute: either just
    the simpler class, a list of simpler classes, a tuple of simpler classes,
    an ordered dict or a dict mapping the string "cls" to a simpler class.
    """
    cls = draw(simple_class_strategy)
    factories = [
        cls,
        lambda: [cls()],
        lambda: (cls(),),
        lambda: {"cls": cls()},
        lambda: OrderedDict([("cls", cls())]),
    ]
    factory = draw(st.sampled_from(factories))
    attrs = draw(list_of_attrs) + [attr.ib(default=attr.Factory(factory))]
    return make_class("HypClass", dict(zip(gen_attr_names(), attrs)))


bare_attrs = st.builds(attr.ib, default=st.none())
int_attrs = st.integers().map(lambda i: attr.ib(default=i))
str_attrs = st.text().map(lambda s: attr.ib(default=s))
float_attrs = st.floats().map(lambda f: attr.ib(default=f))
dict_attrs = st.dictionaries(keys=st.text(), values=st.integers()).map(
    lambda d: attr.ib(default=d)
)

simple_attrs_without_metadata = (
    bare_attrs | int_attrs | str_attrs | float_attrs | dict_attrs
)


@st.composite
def simple_attrs_with_metadata(draw):
    """
    Create a simple attribute with arbitrary metadata.
    """
    c_attr = draw(simple_attrs)
    keys = st.booleans() | st.binary() | st.integers() | st.text()
    vals = st.booleans() | st.binary() | st.integers() | st.text()
    metadata = draw(
        st.dictionaries(keys=keys, values=vals, min_size=1, max_size=3)
    )

    return attr.ib(
        default=c_attr._default,
        validator=c_attr._validator,
        repr=c_attr.repr,
        eq=c_attr.eq,
        order=c_attr.order,
        hash=c_attr.hash,
        init=c_attr.init,
        metadata=metadata,
        type=None,
        converter=c_attr.converter,
    )


simple_attrs = simple_attrs_without_metadata | simple_attrs_with_metadata()

# Python functions support up to 255 arguments.
list_of_attrs = st.lists(simple_attrs, max_size=3)


@st.composite
def simple_classes(
    draw, slots=None, frozen=None, weakref_slot=None, private_attrs=None
):
    """
    A strategy that generates classes with default non-attr attributes.

    For example, this strategy might generate a class such as:

    @attr.s(slots=True, frozen=True, weakref_slot=True)
    class HypClass:
        a = attr.ib(default=1)
        _b = attr.ib(default=None)
        c = attr.ib(default='text')
        _d = attr.ib(default=1.0)
        c = attr.ib(default={'t': 1})

    By default, all combinations of slots, frozen, and weakref_slot classes
    will be generated.  If `slots=True` is passed in, only slotted classes will
    be generated, and if `slots=False` is passed in, no slotted classes will be
    generated. The same applies to `frozen` and `weakref_slot`.

    By default, some attributes will be private (i.e. prefixed with an
    underscore). If `private_attrs=True` is passed in, all attributes will be
    private, and if `private_attrs=False`, no attributes will be private.
    """
    attrs = draw(list_of_attrs)
    frozen_flag = draw(st.booleans())
    slots_flag = draw(st.booleans())
    weakref_flag = draw(st.booleans())

    if private_attrs is None:
        attr_names = maybe_underscore_prefix(gen_attr_names())
    elif private_attrs is True:
        attr_names = ("_" + n for n in gen_attr_names())
    elif private_attrs is False:
        attr_names = gen_attr_names()

    cls_dict = dict(zip(attr_names, attrs))
    pre_init_flag = draw(st.booleans())
    post_init_flag = draw(st.booleans())
    init_flag = draw(st.booleans())

    if pre_init_flag:

        def pre_init(self):
            pass

        cls_dict["__attrs_pre_init__"] = pre_init

    if post_init_flag:

        def post_init(self):
            pass

        cls_dict["__attrs_post_init__"] = post_init

    if not init_flag:

        def init(self, *args, **kwargs):
            self.__attrs_init__(*args, **kwargs)

        cls_dict["__init__"] = init

    return make_class(
        "HypClass",
        cls_dict,
        slots=slots_flag if slots is None else slots,
        frozen=frozen_flag if frozen is None else frozen,
        weakref_slot=weakref_flag if weakref_slot is None else weakref_slot,
        init=init_flag,
    )


# st.recursive works by taking a base strategy (in this case, simple_classes)
# and a special function.  This function receives a strategy, and returns
# another strategy (building on top of the base strategy).
nested_classes = st.recursive(
    simple_classes(), _create_hyp_nested_strategy, max_leaves=3
)
