# Copyright 2009-2022 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/.

"""Microbenchmarks."""

import pickle
import typing as t
from collections import deque
from functools import partial

import pytest

import bidict as b


consume = partial(deque, maxlen=0)
LENS = (99, 999, 9_999)
DICTS_BY_LEN = {n: dict(zip(range(n), range(n))) for n in LENS}
BIDICTS_BY_LEN = {n: b.bidict(DICTS_BY_LEN[n]) for n in LENS}
ORDERED_BIDICTS_BY_LEN = {n: b.OrderedBidict(DICTS_BY_LEN[n]) for n in LENS}
DICTS_BY_LEN_LAST_ITEM_DUPVAL = {n: {**DICTS_BY_LEN[n], **{n - 1: 0}} for n in LENS}
if isinstance(dict, t.Reversible):
    _checkd = next(iter(DICTS_BY_LEN_LAST_ITEM_DUPVAL.values()))
    _lastk, _lastv = next(reversed(_checkd.items()))
    _firstk, _firstv = next(iter(_checkd.items()))
    assert _firstk != _lastk and _firstv == _lastv

BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT = {
    n: (BIDICTS_BY_LEN[n], DICTS_BY_LEN_LAST_ITEM_DUPVAL[n])
    for n in LENS
}
_checkbi, _checkd = next(iter(BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT.values()))
assert _checkbi != _checkd
assert tuple(_checkbi.items())[:-1] == tuple(_checkd.items())[:-1]

ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT = {
    n: (b.OrderedBidict(bi_and_d[0]), bi_and_d[1])
    for (n, bi_and_d) in BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT.items()
}

BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER = {}
ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER = {}
for _i in LENS:
    _bi = BIDICTS_BY_LEN[_i]
    _d = dict(_bi)
    _last, _secondlast = _d.popitem(), _d.popitem()
    _d[_last[0]] = _last[1]  # new second-last
    _d[_secondlast[0]] = _secondlast[1]  # new last
    BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[_i] = (_bi, _d)
    ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[_i] = (b.OrderedBidict(_bi), _d)


@pytest.mark.parametrize('n', LENS)
def test_bi_init_from_dict(n, benchmark):
    """Benchmark initializing a new bidict from a dict."""
    other = DICTS_BY_LEN[n]
    benchmark(b.bidict, other)


@pytest.mark.parametrize('n', LENS)
def test_bi_init_from_bi(n, benchmark):
    """Benchmark initializing a bidict from another bidict."""
    other = BIDICTS_BY_LEN[n]
    benchmark(b.bidict, other)


@pytest.mark.parametrize('n', LENS)
def test_bi_init_fail_worst_case(n, benchmark):
    """Benchmark initializing a bidict from a dict with a final duplicate value."""
    other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n]

    def expect_failing_init():
        try:
            b.bidict(other)
        except b.DuplicationError:
            pass
        else:
            raise Exception('Expected DuplicationError')

    benchmark(expect_failing_init)


@pytest.mark.parametrize('n', LENS)
def test_empty_bi_update_from_bi(n, benchmark):
    """Benchmark updating an empty bidict from another bidict."""
    bi = b.bidict()
    other = BIDICTS_BY_LEN[n]
    benchmark(bi.update, other)
    assert bi == other


@pytest.mark.parametrize('n', LENS)
def test_small_bi_large_update_fails_worst_case(n, benchmark):
    """Benchmark updating a small bidict with a large update that fails on the final item and then rolls back."""
    bi = b.bidict(zip(range(-9, 0), range(-9, 0)))
    other = DICTS_BY_LEN_LAST_ITEM_DUPVAL[n]

    def apply_failing_update():
        try:
            bi.update(other)
        except b.DuplicationError:
            pass  # Rollback should happen here.
        else:
            raise Exception('Expected DuplicationError')

    benchmark(apply_failing_update)
    assert list(bi.items()) == list(zip(range(-9, 0), range(-9, 0)))


@pytest.mark.parametrize('n', LENS)
def test_bi_iter(n, benchmark):
    """Benchmark iterating over a bidict."""
    bi = BIDICTS_BY_LEN[n]
    benchmark(consume, iter(bi))


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_iter(n, benchmark):
    """Benchmark iterating over an OrderedBidict."""
    ob = ORDERED_BIDICTS_BY_LEN[n]
    benchmark(consume, iter(ob))


@pytest.mark.parametrize('n', LENS)
def test_bi_contains_key_present(n, benchmark):
    """Benchmark bidict.__contains__ with a contained key."""
    bi = BIDICTS_BY_LEN[n]
    key = next(iter(bi))
    result = benchmark(bi.__contains__, key)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_bi_contains_key_missing(n, benchmark):
    """Benchmark bidict.__contains__ with a missing key."""
    bi = BIDICTS_BY_LEN[n]
    result = benchmark(bi.__contains__, object())
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_bi_equals_with_equal_dict(n, benchmark):
    """Benchmark bidict.__eq__ with an equivalent dict."""
    bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n]
    result = benchmark(bi.__eq__, d)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_equals_with_equal_dict(n, benchmark):
    """Benchmark OrderedBidict.__eq__ with an equivalent dict."""
    ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n]
    result = benchmark(ob.__eq__, d)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_items_equals_with_equal_dict_items(n, benchmark):
    """Benchmark OrderedBidict.items().__eq__ with an equivalent dict_items."""
    ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n]
    obi, di = ob.items(), d.items()
    result = benchmark(obi.__eq__, di)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_items_equals_with_unequal_dict_items(n, benchmark):
    """Benchmark OrderedBidict.items().__eq__ with an unequal dict_items."""
    ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n]
    obi, di = ob.items(), d.items()
    result = benchmark(obi.__eq__, di)
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_bi_equals_with_unequal_dict(n, benchmark):
    """Benchmark bidict.__eq__ with an unequal dict."""
    bi, d = BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n]
    result = benchmark(bi.__eq__, d)
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_equals_with_unequal_dict(n, benchmark):
    """Benchmark OrderedBidict.__eq__ with an unequal dict."""
    ob, d = ORDERED_BIDICT_AND_DICT_ONLY_LAST_ITEM_DIFFERENT[n]
    result = benchmark(ob.__eq__, d)
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_bi_order_sensitive_equals_dict(n, benchmark):
    """Benchmark bidict.equals_order_sensitive with an order-sensitive-equal dict."""
    bi, d = BIDICTS_BY_LEN[n], DICTS_BY_LEN[n]
    result = benchmark(bi.equals_order_sensitive, d)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_order_sensitive_equals_dict(n, benchmark):
    """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-equal dict."""
    ob, d = ORDERED_BIDICTS_BY_LEN[n], DICTS_BY_LEN[n]
    result = benchmark(ob.equals_order_sensitive, d)
    assert result


@pytest.mark.parametrize('n', LENS)
def test_bi_equals_order_sensitive_with_unequal_dict(n, benchmark):
    """Benchmark bidict.equals_order_sensitive with an order-sensitive-unequal dict."""
    bi, d = BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n]
    result = benchmark(bi.equals_order_sensitive, d)
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_orderedbi_equals_order_sensitive_with_unequal_dict(n, benchmark):
    """Benchmark OrderedBidict.equals_order_sensitive with an order-sensitive-unequal dict."""
    ob, d = ORDERED_BIDICT_AND_DICT_LAST_TWO_ITEMS_DIFFERENT_ORDER[n]
    result = benchmark(ob.equals_order_sensitive, d)
    assert not result


@pytest.mark.parametrize('n', LENS)
def test_copy(n, benchmark):
    """Benchmark creating a copy of a bidict."""
    bi = BIDICTS_BY_LEN[n]
    benchmark(bi.copy)


@pytest.mark.parametrize('n', LENS)
def test_pickle(n, benchmark):
    """Benchmark pickling a bidict."""
    bi = BIDICTS_BY_LEN[n]
    benchmark(pickle.dumps, bi)


@pytest.mark.parametrize('n', LENS)
def test_unpickle(n, benchmark):
    """Benchmark unpickling a bidict."""
    bp = pickle.dumps(BIDICTS_BY_LEN[n])
    benchmark(pickle.loads, bp)
