# -*- 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 ItemsView, PY2, izip

from . 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

IDENTIFIER_TYPE = TEXT
if PY2:
    IDENTIFIER_TYPE = IDENTIFIER_TYPE.map(lambda x: x.encode('utf-8'))


@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)
    same_items = map_items in (_SAME_AS_BI_ITEMS, _SAME_AS_BI_ITEMS_DIFF_ORDER)
    if same_items:
        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)
    b = bi_cls(bi_items_)
    m = map_cls(map_items_)
    if not same_items:
        assume(ItemsView(m) != ItemsView(b))
    return b, m


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,
})
