# -*- coding: utf-8 -*-
# Copyright 2009-2019 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"""Strategies for Hypothesis tests."""

import re
from collections import OrderedDict

from hypothesis import assume, strategies as st
from bidict import IGNORE, OVERWRITE, RAISE, OrderedBidictBase, namedbidict
from bidict.compat import izip

import _types as t


# pylint: disable=invalid-name


DATA = st.data()
RAND = st.randoms()
BIDICT_TYPES = st.sampled_from(t.BIDICT_TYPES)
MUTABLE_BIDICT_TYPES = st.sampled_from(t.MUTABLE_BIDICT_TYPES)
FROZEN_BIDICT_TYPES = st.sampled_from(t.FROZEN_BIDICT_TYPES)
ORDERED_BIDICT_TYPES = st.sampled_from(t.ORDERED_BIDICT_TYPES)
MAPPING_TYPES = st.sampled_from(t.MAPPING_TYPES)
NON_BIDICT_MAPPING_TYPES = st.sampled_from(t.NON_BIDICT_MAPPING_TYPES)
ORDERED_MAPPING_TYPES = st.sampled_from(t.ORDERED_MAPPING_TYPES)
HASHABLE_MAPPING_TYPES = st.sampled_from(t.HASHABLE_MAPPING_TYPES)
DUP_POLICIES = st.sampled_from((IGNORE, OVERWRITE, RAISE))
DUP_POLICIES_DICT = st.fixed_dictionaries(dict(
    on_dup_key=DUP_POLICIES,
    on_dup_val=DUP_POLICIES,
    on_dup_kv=DUP_POLICIES,
))

BOOLEANS = st.booleans()
FLOATS = st.floats(allow_nan=False)
INTS = st.integers()
TEXT = st.text()
NONE = st.none()
IMMUTABLES = BOOLEANS | TEXT | NONE | INTS | FLOATS

LISTS_MAX_SIZE = 10
LISTS_PAIRS = st.lists(st.tuples(IMMUTABLES, IMMUTABLES), max_size=LISTS_MAX_SIZE)

NON_MAPPINGS = IMMUTABLES | LISTS_PAIRS


@st.composite
def _lists_pairs_with_duplication(draw):
    # pylint: disable=too-many-locals
    n = draw(st.integers(min_value=3, max_value=LISTS_MAX_SIZE))
    indexes = st.integers(min_value=0, max_value=n - 1)
    keys = draw(st.lists(IMMUTABLES, min_size=n, max_size=n))
    vals = draw(st.lists(IMMUTABLES, min_size=n, max_size=n))
    fwd = OrderedDict(izip(keys, vals))
    inv = OrderedDict(izip(vals, keys))
    which_to_dup = draw(RAND).choice((1, 2, 3))
    should_dup_key = which_to_dup in (1, 3)
    should_dup_val = which_to_dup in (2, 3)
    should_add_dup_key = should_dup_key and len(fwd) < n
    should_add_dup_val = should_dup_val and len(inv) < n
    if not should_add_dup_key and not should_add_dup_val:
        return list(izip(keys, vals))
    if should_add_dup_key:
        dup_key_idx = draw(indexes)
        added_key = keys[dup_key_idx]
    else:
        added_key = draw(IMMUTABLES)
        assume(added_key not in fwd)
    if should_add_dup_val:
        dup_val_idx = draw(indexes)
        if should_add_dup_key:
            assume(dup_val_idx != dup_key_idx)
        added_val = vals[dup_val_idx]
    else:
        added_val = draw(IMMUTABLES)
        assume(added_val not in inv)
    insert_idx = draw(indexes)
    keys.insert(insert_idx, added_key)
    vals.insert(insert_idx, added_val)
    return list(izip(keys, vals))


# pylint: disable=no-value-for-parameter
LISTS_PAIRS_DUP = _lists_pairs_with_duplication()


@st.composite
def lists_pairs_nodup(draw, elements=IMMUTABLES, min_size=0, max_size=LISTS_MAX_SIZE):
    """Generate a list of pairs from the given elements with no duplication."""
    n = draw(st.integers(min_value=min_size, max_value=max_size))
    size_n_sets = st.sets(elements, min_size=n, max_size=n)
    keys = draw(size_n_sets)
    vals = draw(size_n_sets)
    return list(izip(keys, vals))


LISTS_PAIRS_NODUP = lists_pairs_nodup()
LISTS_TEXT_PAIRS_NODUP = lists_pairs_nodup(elements=TEXT)


# pylint: disable=dangerous-default-value
@st.composite
def bidicts(draw, bi_types=BIDICT_TYPES, init_items=LISTS_PAIRS_NODUP):
    """Generate bidicts."""
    bi_cls = draw(bi_types)
    items = draw(init_items)
    bi = bi_cls(items)
    # Return the inverse bidict with 50% probability to increase coverage.
    return bi if draw(RAND).choice((True, False)) else bi.inv


BIDICTS = bidicts()
FROZEN_BIDICTS = bidicts(bi_types=FROZEN_BIDICT_TYPES)
MUTABLE_BIDICTS = bidicts(bi_types=MUTABLE_BIDICT_TYPES)
ORDERED_BIDICTS = bidicts(bi_types=ORDERED_BIDICT_TYPES)


NAMEDBIDICT_VALID_NAME_PAT = re.compile('[A-z][A-z0-9_]*$')
NAMEDBIDICT_NAMES = st.from_regex(NAMEDBIDICT_VALID_NAME_PAT, fullmatch=True)
NAMEDBIDICT_3_NAMES = st.tuples(
    NAMEDBIDICT_NAMES,
    NAMEDBIDICT_NAMES,
    NAMEDBIDICT_NAMES,
)


@st.composite
def _namedbidict_types(draw, names=NAMEDBIDICT_3_NAMES, base_types=BIDICT_TYPES):
    typename, keyname, valname = draw(names)
    assume(keyname != valname)
    base_type = draw(base_types)
    return namedbidict(typename, keyname, valname, base_type=base_type)


NAMEDBIDICT_TYPES = _namedbidict_types()


@st.composite
def _namedbidicts(draw, nb_types=NAMEDBIDICT_TYPES, init_items=LISTS_PAIRS_NODUP):
    nb_cls = draw(nb_types)
    items = draw(init_items)
    nb = nb_cls(items)
    # Return the inverse namedbidict with 50% probability to increase coverage.
    return nb if draw(RAND).choice((True, False)) else nb.inv


NAMEDBIDICTS = _namedbidicts()


_COMPARE_DICT_TYPE = object()
_SAME_AS_BI_ITEMS = object()
_SAME_AS_BI_ITEMS_DIFF_ORDER = object()


@st.composite
def _bidict_and_mapping_from_items(
        draw,
        bi_types=BIDICT_TYPES,
        map_types=MAPPING_TYPES,
        bi_items=LISTS_PAIRS_NODUP,
        map_items=_SAME_AS_BI_ITEMS,
):
    bi_cls = draw(bi_types)
    if map_types is _COMPARE_DICT_TYPE:
        map_cls = OrderedDict if issubclass(bi_cls, OrderedBidictBase) else dict
    else:
        map_cls = draw(map_types)
    bi_items_ = draw(bi_items)
    if map_items in (_SAME_AS_BI_ITEMS, _SAME_AS_BI_ITEMS_DIFF_ORDER):
        map_items_ = bi_items_[:]
        if map_items is _SAME_AS_BI_ITEMS_DIFF_ORDER:
            draw(RAND).shuffle(map_items_)
            assume(map_items_ != bi_items_)
    else:
        map_items_ = draw(map_items)
        assume(map_items_ != bi_items_)
    return bi_cls(bi_items_), map_cls(map_items_)


BIDICT_AND_MAPPING_FROM_SAME_ITEMS_NODUP = _bidict_and_mapping_from_items()
BIDICT_AND_MAPPING_FROM_DIFFERENT_ITEMS = _bidict_and_mapping_from_items(
    map_items=LISTS_PAIRS_NODUP,
)
BIDICT_AND_COMPARE_DICT_FROM_SAME_ITEMS_NODUP = _bidict_and_mapping_from_items(
    map_types=_COMPARE_DICT_TYPE,
)
ORDERED_BIDICT_AND_ORDERED_DICT_FROM_SAME_ITEMS_NODUP = _bidict_and_mapping_from_items(
    bi_types=ORDERED_BIDICT_TYPES,
    map_types=_COMPARE_DICT_TYPE,
)
ORDERED_BIDICT_AND_ORDERED_MAPPING_FROM_SAME_ITEMS_NODUP = _bidict_and_mapping_from_items(
    bi_types=ORDERED_BIDICT_TYPES,
    map_types=ORDERED_MAPPING_TYPES,
)
HASHABLE_BIDICT_AND_MAPPING_FROM_SAME_ITEMS_NODUP = _bidict_and_mapping_from_items(
    bi_types=FROZEN_BIDICT_TYPES,
    map_types=HASHABLE_MAPPING_TYPES,
)
ORDERED_BIDICT_AND_ORDERED_MAPPING_FROM_SAME_ITEMS_DIFF_ORDER = _bidict_and_mapping_from_items(
    bi_types=ORDERED_BIDICT_TYPES,
    map_types=ORDERED_MAPPING_TYPES,
    map_items=_SAME_AS_BI_ITEMS_DIFF_ORDER,
)


NO_ARGS = st.just(())
IM_ARG = st.tuples(IMMUTABLES)
LP_ARG = st.tuples(LISTS_PAIRS)
TWO_IM_ARGS = st.tuples(IMMUTABLES, IMMUTABLES)

ARGS_BY_METHOD = st.fixed_dictionaries({
    # mutating
    # 0-arity
    (0, 'clear'): NO_ARGS,
    (0, 'popitem'): NO_ARGS,
    # 1-arity, an immutable atom
    (1, '__delitem__'): IM_ARG,
    (1, 'pop'): IM_ARG,
    (1, 'setdefault'): IM_ARG,
    (1, 'move_to_end'): IM_ARG,
    # 1-arity, a list of pairs
    (1, 'update'): LP_ARG,
    (1, 'forceupdate'): LP_ARG,
    # 2-arity
    (2, 'pop'): TWO_IM_ARGS,
    (2, 'setdefault'): TWO_IM_ARGS,
    (2, '__setitem__'): TWO_IM_ARGS,
    (2, 'put'): TWO_IM_ARGS,
    (2, 'forceput'): TWO_IM_ARGS,
    (2, 'move_to_end'): st.tuples(IMMUTABLES, BOOLEANS),
    # non-mutating
    # 0-arity
    (0, '__copy__'): NO_ARGS,
    (0, '__iter__'): NO_ARGS,
    (0, '__len__'): NO_ARGS,
    (0, 'copy'): NO_ARGS,
    (0, 'keys'): NO_ARGS,
    (0, 'items'): NO_ARGS,
    (0, 'values'): NO_ARGS,
    (0, 'iterkeys'): NO_ARGS,
    (0, 'iteritems'): NO_ARGS,
    (0, 'itervalues'): NO_ARGS,
    (0, 'viewkeys'): NO_ARGS,
    (0, 'viewitems'): NO_ARGS,
    (0, 'viewvalues'): NO_ARGS,
    # 1-arity
    (1, '__contains__'): IM_ARG,
    (1, '__getitem__'): IM_ARG,
    (1, 'get'): IM_ARG,
})
