# -*- coding: utf-8 -*-
# Copyright 2017 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/.

"""
Property-based tests using https://hypothesis.readthedocs.io
"""

from collections import OrderedDict
from os import getenv

import pytest
from hypothesis import assume, given, settings
from hypothesis.strategies import integers, lists, tuples
from bidict import (
    IGNORE, OVERWRITE, RAISE,
    bidict, OrderedBidict,
    frozenbidict, FrozenOrderedBidict)
from bidict.compat import iteritems


settings.register_profile('default', settings(max_examples=200, deadline=None))
settings.load_profile(getenv('HYPOTHESIS_PROFILE', 'default'))


# pylint: disable=C0111
def to_inv_odict(items):
    return OrderedDict((v, k) for (k, v) in items)


def dedup(items):
    pruned = list(iteritems(to_inv_odict(iteritems(to_inv_odict(items)))))
    assume(len(pruned) >= len(items) // 2)
    return pruned


# pylint: disable=C0103
ondupbehaviors = (IGNORE, OVERWRITE, RAISE)
mutable_bidict_types = (bidict, OrderedBidict)
immutable_bidict_types = (frozenbidict, FrozenOrderedBidict)
bidict_types = mutable_bidict_types + immutable_bidict_types
mutating_methods_by_arity = {
    0: ('clear', 'popitem'),
    1: ('__delitem__', 'pop', 'setdefault', 'move_to_end'),
    2: ('__setitem__', 'pop', 'put', 'forceput', 'setdefault'),
    -1: ('update', 'forceupdate'),
}
immutable = integers()
itemlists = lists(tuples(immutable, immutable))
inititems = itemlists.map(dedup)


@pytest.mark.parametrize('B', bidict_types)
@given(init=inititems)
def test_equality(B, init):  # noqa
    b = B(init)
    d = dict(init)
    o = OrderedDict(init)
    oi = to_inv_odict(iteritems(o))
    di = OrderedDict(oi)
    assert b == d
    assert b == o
    assert not b != d
    assert not b != o
    assert b.inv == oi
    assert b.inv == di
    assert not b.inv != oi
    assert not b.inv != di


@pytest.mark.parametrize('B', bidict_types)
@given(init=inititems)
def test_bidirectional_mappings(B, init):  # noqa
    ordered = bool(getattr(B, '__reversed__', None))
    C = list if ordered else sorted  # noqa
    b = B(init)
    keysf = C(k for (k, v) in iteritems(b))
    keysi = C(b.inv[v] for (k, v) in iteritems(b))
    assert keysf == keysi
    valsf = C(b[k] for (v, k) in iteritems(b.inv))
    valsi = C(v for (v, k) in iteritems(b.inv))
    assert valsf == valsi


@pytest.mark.parametrize(
    'arity, methodname',
    [(a, m) for (a, ms) in iteritems(mutating_methods_by_arity) for m in ms])
@pytest.mark.parametrize('B', mutable_bidict_types)  # noqa
@given(init=inititems, arg1=immutable, arg2=immutable, items=itemlists)
def test_consistency_after_mutation(arity, methodname, B, init, arg1, arg2, items):
    method = getattr(B, methodname, None)
    if not method:
        return
    args = []
    if arity == -1:
        args.append(items)
    else:
        if arity > 0:
            args.append(arg1)
        if arity > 1:
            args.append(arg2)
    b0 = B(init)
    b1 = b0.copy()
    try:
        method(b1, *args)
    except Exception as exc:  # pylint: disable=W0703
        # method should fail clean, i.e. b1 should be in the same state it was before the call.
        assert b1 == b0, '%r did not fail clean: %r' % (method, exc)
        assert b1.inv == b0.inv, '%r did not fail clean: %r' % (method, exc)
    # Whether method failed or succeeded, b1 should pass consistency checks.
    assert len(b1) == sum(1 for _ in iteritems(b1))
    assert len(b1) == sum(1 for _ in iteritems(b1.inv))
    assert b1 == dict(iteritems(b1))
    assert b1.inv == dict(iteritems(b1.inv))
    assert b1 == to_inv_odict(iteritems(b1.inv))
    assert b1.inv == to_inv_odict(iteritems(b1))


@pytest.mark.parametrize('on_dup_key', ondupbehaviors)
@pytest.mark.parametrize('on_dup_val', ondupbehaviors)
@pytest.mark.parametrize('on_dup_kv', ondupbehaviors)
@pytest.mark.parametrize('B', mutable_bidict_types)
@given(init=inititems, items=itemlists)
def test_putall(on_dup_key, on_dup_val, on_dup_kv, B, init, items):  # noqa
    b0 = B(init)
    expect = b0.copy()
    expectexc = None
    for (k, v) in items:
        try:
            expect.put(k, v, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv)
        except Exception as exc:  # pylint: disable=W0703
            expectexc = exc
            expect = b0  # bulk updates fail clean
            break
    check = b0.copy()
    checkexc = None
    try:
        check.putall(items, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv)
    except Exception as exc:  # pylint: disable=W0703
        checkexc = exc
    assert type(checkexc) == type(expectexc)  # pylint: disable=C0123
    assert check == expect
    assert check.inv == expect.inv
