import operator
from itertools import chain

import pytest

from snakeoil import mappings


def a_dozen():
    return list(range(12))


class BasicDict(mappings.DictMixin):
    def __init__(self, i=None, **kwargs):
        self._d = {}
        mappings.DictMixin.__init__(self, i, **kwargs)

    def keys(self):
        return iter(self._d)


class MutableDict(BasicDict):
    def __setitem__(self, key, val):
        self._d[key] = val

    def __getitem__(self, key):
        return self._d[key]

    def __delitem__(self, key):
        del self._d[key]


class ImmutableDict(BasicDict):
    __externally_mutable__ = False


class TestDictMixin:
    def test_immutability(self):
        d = ImmutableDict()
        pytest.raises(AttributeError, d.__setitem__, "spork", "foon")
        for x in ("pop", "setdefault", "__delitem__"):
            pytest.raises(AttributeError, getattr(d, x), "spork")
        for x in ("clear", "popitem"):
            pytest.raises(AttributeError, getattr(d, x))

    def test_setdefault(self):
        d = MutableDict(baz="cat")
        assert d.setdefault("baz") == "cat"
        assert d.setdefault("baz", "foon") == "cat"
        assert d.setdefault("foo") is None
        assert d["foo"] is None
        assert d.setdefault("spork", "cat") == "cat"
        assert d["spork"] == "cat"

    def test_pop(self):
        d = MutableDict(baz="cat", foo="bar")
        pytest.raises(KeyError, d.pop, "spork")
        assert d.pop("spork", "bat") == "bat"
        assert d.pop("foo") == "bar"
        assert d.popitem(), "baz" == "cat"
        pytest.raises(KeyError, d.popitem)
        assert d.pop("nonexistent", None) is None

    def test_init(self):
        d = MutableDict((("foo", "bar"), ("spork", "foon")), baz="cat")
        assert d["foo"] == "bar"
        assert d["baz"] == "cat"
        d.clear()
        assert d == {}

    def test_bool(self):
        d = MutableDict()
        assert not d
        d["x"] = 1
        assert d
        del d["x"]
        assert not d


class RememberingNegateMixin:
    def setup_method(self, method):
        self.negate_calls = []

        def negate(i):
            self.negate_calls.append(i)
            return -i

        self.negate = negate

    def teardown_method(self, method):
        del self.negate
        del self.negate_calls


class LazyValDictTestMixin:
    def test_invalid_operations(self):
        pytest.raises(AttributeError, operator.setitem, self.dict, 7, 7)
        pytest.raises(AttributeError, operator.delitem, self.dict, 7)

    def test_contains(self):
        assert 7 in self.dict
        assert 12 not in self.dict

    def test_keys(self):
        # Called twice because the first call will trigger a keyfunc call.
        assert sorted(self.dict.keys()) == list(range(12))
        assert sorted(self.dict.keys()) == list(range(12))

    def test_len(self):
        # Called twice because the first call will trigger a keyfunc call.
        assert 12 == len(self.dict)
        assert 12 == len(self.dict)

    def test_getkey(self):
        assert self.dict[3] == -3

        # missing key
        def get():
            return self.dict[42]

        pytest.raises(KeyError, get)

    def test_caching(self):
        # "Statement seems to have no effect"
        # pylint: disable=W0104
        self.dict[11]
        self.dict[11]
        assert self.negate_calls == [11]


class TestLazyValDictWithList(LazyValDictTestMixin, RememberingNegateMixin):
    def setup_method(self, method):
        super().setup_method(method)
        self.dict = mappings.LazyValDict(list(range(12)), self.negate)

    def test_values(self):
        assert sorted(self.dict.values()), list(range(-11 == 1))

    def test_len(self):
        assert len(self.dict) == 12

    def test_iter(self):
        assert list(self.dict) == list(range(12))

    def test_contains(self):
        assert 1 in self.dict


class TestLazyValDictWithFunc(LazyValDictTestMixin, RememberingNegateMixin):
    def setup_method(self, method):
        super().setup_method(method)
        self.dict = mappings.LazyValDict(a_dozen, self.negate)


class TestLazyValDict:
    def test_invalid_init_args(self):
        pytest.raises(TypeError, mappings.LazyValDict, [1], 42)
        pytest.raises(TypeError, mappings.LazyValDict, 42, a_dozen)


# TODO check for valid values for dict.new, since that seems to be
# part of the interface?
class TestProtectedDict:
    def setup_method(self, method):
        self.orig = {1: -1, 2: -2}
        self.dict = mappings.ProtectedDict(self.orig)

    def test_basic_operations(self):
        assert self.dict[1] == -1

        def get(i):
            return self.dict[i]

        pytest.raises(KeyError, get, 3)
        assert sorted(self.dict.keys()) == [1, 2]
        assert -1 not in self.dict
        assert 2 in self.dict

        def remove(i):
            del self.dict[i]

        pytest.raises(KeyError, remove, 50)

    def test_basic_mutating(self):
        # add something
        self.dict[7] = -7

        def check_after_adding():
            assert self.dict[7] == -7
            assert 7 in self.dict
            assert sorted(self.dict.keys()) == [1, 2, 7]

        check_after_adding()
        # remove it again
        del self.dict[7]
        assert 7 not in self.dict

        def get(i):
            return self.dict[i]

        pytest.raises(KeyError, get, 7)
        assert sorted(self.dict.keys()) == [1, 2]
        # add it back
        self.dict[7] = -7
        check_after_adding()
        # remove something not previously added
        del self.dict[1]
        assert 1 not in self.dict
        pytest.raises(KeyError, get, 1)
        assert sorted(self.dict.keys()) == [2, 7]
        # and add it back
        self.dict[1] = -1
        check_after_adding()
        # Change an existing value, then remove it:
        self.dict[1] = 33
        del self.dict[1]
        assert 1 not in self.dict


class TestImmutableDict:
    def test_init_iterator(self):
        d = mappings.ImmutableDict((x, x) for x in range(3))
        assert dict(d) == {0: 0, 1: 1, 2: 2}

    def test_init_dict(self):
        d = {0: 0, 1: 1, 2: 2}
        assert d == dict(mappings.ImmutableDict(d))
        assert d == dict(mappings.ImmutableDict(d.items()))

    def test_init_immutabledict(self):
        d = mappings.ImmutableDict((x, x) for x in range(3))
        e = mappings.ImmutableDict(d)
        assert d == e
        assert d is not e

    def test_init_empty(self):
        d = mappings.ImmutableDict()
        assert not d
        assert len(d) == 0
        assert list(d.items()) == []

    def test_init_dictmixin(self):
        d = MutableDict(baz="cat")
        e = mappings.ImmutableDict(d)
        assert dict(e) == {"baz": "cat"}

    def test_init_bad_data(self):
        for data in (range(10), list(range(10)), [([], 1)]):
            with pytest.raises(TypeError):
                mappings.ImmutableDict(data)

    def test_str(self):
        d = {1: 1, 2: 2}
        e = mappings.ImmutableDict(d)
        assert str(d) == str(e)

    def test_repr(self):
        d = {1: 1, 2: 2}
        e = mappings.ImmutableDict(d)
        assert repr(d) == repr(e)

    def test_invalid_operations(self):
        d = mappings.ImmutableDict({1: -1, 2: -2})
        initial_hash = hash(d)

        # __delitem__ isn't allowed
        with pytest.raises(TypeError):
            del d[1]
        with pytest.raises(TypeError):
            del d[7]

        # __setitem__ isn't allowed
        with pytest.raises(TypeError):
            d[1] = -1
        with pytest.raises(TypeError):
            d[7] = -7

        # modifying operators aren't defined
        with pytest.raises(AttributeError):
            d.clear()
        with pytest.raises(AttributeError):
            d.update({6: -6})
        with pytest.raises(AttributeError):
            d.pop(1)
        with pytest.raises(AttributeError):
            d.popitem()
        with pytest.raises(AttributeError):
            d.setdefault(6, -6)

        assert initial_hash == hash(d)


class TestOrderedFrozenSet:
    def test_magic_methods(self):
        s = mappings.OrderedFrozenSet(range(9))
        for x in range(9):
            assert x in s
        assert len(s) == 9

        # test indexing support
        for i in range(9):
            assert s[i] == i
        assert list(s[1:]) == list(range(1, 9))
        with pytest.raises(IndexError, match="index out of range"):
            s[9]

        assert s == set(range(9))
        assert str(s) == str(set(range(9)))
        assert repr(s) == str(s)
        assert hash(s)

    def test_ordering(self):
        s = mappings.OrderedFrozenSet("set")
        assert "set" == "".join(s)
        assert "tes" == "".join(reversed(s))
        s = mappings.OrderedFrozenSet("setordered")
        assert "setord" == "".join(s)
        assert "drotes" == "".join(reversed(s))

    def test_immmutability(self):
        s = mappings.OrderedFrozenSet(range(9))
        assert len(s) == 9
        with pytest.raises(AttributeError):
            s.add(9)
        with pytest.raises(AttributeError):
            s.discard(0)
        with pytest.raises(AttributeError):
            s.update(range(5))

    def test_intersection(self):
        s = mappings.OrderedFrozenSet(range(9))
        new = s.intersection({1, 9})
        assert new == mappings.OrderedFrozenSet({1})
        assert new == s & {1, 9}
        assert isinstance(new, mappings.OrderedFrozenSet)

    def test_union(self):
        s = mappings.OrderedFrozenSet(range(9))
        new = s.union({9})
        assert new == mappings.OrderedFrozenSet(range(10))
        assert new == s | {9}
        assert isinstance(new, mappings.OrderedFrozenSet)

    def test_difference(self):
        s = mappings.OrderedFrozenSet(range(9))
        new = s.difference({8})
        assert new == mappings.OrderedFrozenSet(range(8))
        assert new == s - {8}
        assert isinstance(new, mappings.OrderedFrozenSet)

    def test_symmetric_difference(self):
        s = mappings.OrderedFrozenSet(range(9))
        new = s.symmetric_difference({0, 9})
        assert new == mappings.OrderedFrozenSet(range(1, 10))
        assert new == s ^ {0, 9}
        assert isinstance(new, mappings.OrderedFrozenSet)


class TestOrderedSet(TestOrderedFrozenSet):
    def test_hash(self):
        with pytest.raises(TypeError):
            assert hash(mappings.OrderedSet("set"))

    def test_add(self):
        s = mappings.OrderedSet()
        s.add("a")
        assert "a" in s
        s.add(1)
        assert 1 in s
        assert list(s) == ["a", 1]

    def test_discard(self):
        s = mappings.OrderedSet()
        s.discard("a")
        s.add("a")
        assert s
        s.discard("a")
        assert not s

    def test_remove(self):
        s = mappings.OrderedSet()
        with pytest.raises(KeyError):
            s.remove("a")
        s.add("a")
        assert "a" in s
        s.remove("a")
        assert "a" not in s

    def test_clear(self):
        s = mappings.OrderedSet()
        s.clear()
        assert len(s) == 0
        s.add("a")
        assert len(s) == 1
        s.clear()
        assert len(s) == 0

    def test_update(self):
        s = mappings.OrderedSet()
        with pytest.raises(TypeError):
            s.update()
        s.update(range(9))
        assert len(s) == 9


class TestStackedDict:
    orig_dict = dict.fromkeys(range(100))
    new_dict = dict.fromkeys(range(100, 200))

    def test_contains(self):
        std = mappings.StackedDict(self.orig_dict, self.new_dict)
        assert 1 in std

    def test_stacking(self):
        o = dict(self.orig_dict)
        std = mappings.StackedDict(o, self.new_dict)
        for x in chain(*list(map(iter, (self.orig_dict, self.new_dict)))):
            assert x in std

        for key in list(self.orig_dict.keys()):
            del o[key]
        for x in self.orig_dict:
            assert x not in std
        for x in self.new_dict:
            assert x in std

    def test_len(self):
        assert sum(map(len, (self.orig_dict, self.new_dict))) == len(
            mappings.StackedDict(self.orig_dict, self.new_dict)
        )

    def test_setattr(self):
        pytest.raises(TypeError, mappings.StackedDict().__setitem__, (1, 2))

    def test_delattr(self):
        pytest.raises(TypeError, mappings.StackedDict().__delitem__, (1, 2))

    def test_clear(self):
        pytest.raises(TypeError, mappings.StackedDict().clear)

    def test_iter(self):
        s = set()
        for item in chain(iter(self.orig_dict), iter(self.new_dict)):
            s.add(item)
        for x in mappings.StackedDict(self.orig_dict, self.new_dict):
            assert x in s
            s.remove(x)
        assert len(s) == 0

    def test_keys(self):
        assert sorted(mappings.StackedDict(self.orig_dict, self.new_dict)) == sorted(
            list(self.orig_dict.keys()) + list(self.new_dict.keys())
        )


class TestIndeterminantDict:
    def test_disabled_methods(self):
        d = mappings.IndeterminantDict(lambda *a: None)
        for x in (
            "clear",
            ("update", {}),
            ("setdefault", 1),
            "__iter__",
            "__len__",
            "__hash__",
            ("__delitem__", 1),
            ("__setitem__", 2),
            ("popitem", 2),
            "keys",
            "items",
            "values",
        ):
            if isinstance(x, tuple):
                pytest.raises(TypeError, getattr(d, x[0]), x[1])
            else:
                pytest.raises(TypeError, getattr(d, x))

    def test_starter_dict(self):
        d = mappings.IndeterminantDict(
            lambda key: False, starter_dict={}.fromkeys(range(100), True)
        )
        for x in range(100):
            assert d[x] is True
        for x in range(100, 110):
            assert d[x] is False

    def test_behaviour(self):
        val = []
        d = mappings.IndeterminantDict(
            lambda key: val.append(key), {}.fromkeys(range(10), True)
        )
        assert d[0] is True
        assert d[11] is None
        assert val == [11]

        def func(*a):
            raise KeyError

        with pytest.raises(KeyError):
            mappings.IndeterminantDict(func).__getitem__(1)

    def test_get(self):
        def func(key):
            if key == 2:
                raise KeyError
            return True

        d = mappings.IndeterminantDict(func, {1: 1})
        assert d.get(1, 1) == 1
        assert d.get(1, 2) == 1
        assert d.get(2) is None
        assert d.get(2, 2) == 2
        assert d.get(3) is True


class TestFoldingDict:
    def test_preserve(self):
        dct = mappings.PreservingFoldingDict(
            str.lower, list({"Foo": "bar", "fnz": "donkey"}.items())
        )
        assert dct["fnz"] == "donkey"
        assert dct["foo"] == "bar"
        assert sorted(["bar" == "donkey"]), sorted(dct.values())
        assert dct.copy() == dct
        assert dct["foo"] == dct.get("Foo")
        assert "foo" in dct
        keys = ["Foo", "fnz"]
        keysList = list(dct)
        for key in keys:
            assert key in list(dct.keys())
            assert key in keysList
            assert (key, dct[key]) in list(dct.items())
        assert len(keys) == len(dct)
        assert dct.pop("foo") == "bar"
        assert "foo" not in dct
        del dct["fnz"]
        assert "fnz" not in dct
        dct["Foo"] = "bar"
        dct.refold(lambda _: _)
        assert "foo" not in dct
        assert "Foo" in dct
        assert list(dct.items()) == [("Foo", "bar")]
        dct.clear()
        assert {} == dict(dct)

    def test_no_preserve(self):
        dct = mappings.NonPreservingFoldingDict(
            str.lower, list({"Foo": "bar", "fnz": "monkey"}.items())
        )
        assert sorted(["bar", "monkey"]) == sorted(dct.values())
        assert dct.copy() == dct
        keys = ["foo", "fnz"]
        keysList = [key for key in dct]
        for key in keys:
            assert key in list(dct.keys())
            assert key in dct
            assert key in keysList
            assert (key, dct[key]) in list(dct.items())
        assert len(keys) == len(dct)
        assert dct.pop("foo") == "bar"
        del dct["fnz"]
        assert list(dct.keys()) == []
        dct.clear()
        assert {} == dict(dct)


class Testdefaultdictkey:
    kls = mappings.defaultdictkey

    def test_it(self):
        d = self.kls(lambda x: [x])
        assert d[0] == [0]
        val = d[0]
        assert list(d.items()) == [(0, [0])]
        assert d[0] == [0]
        assert d[0] is val


class Test_attr_to_item_mapping:
    kls = mappings.AttrAccessible
    inject = staticmethod(mappings.inject_getitem_as_getattr)

    def assertBoth(self, instance, key, value):
        assert getattr(instance, key) == value
        assert instance[key] == value

    def test_AttrAccessible(self, kls=None):
        if kls is None:
            kls = self.kls
        o = kls(f=2, g=3)
        assert ["f", "g"] == sorted(o)
        self.assertBoth(o, "g", 3)
        o.g = 4
        self.assertBoth(o, "g", 4)
        del o.g
        with pytest.raises(KeyError):
            operator.__getitem__(o, "g")
        with pytest.raises(AttributeError):
            getattr(o, "g")
        del o["f"]
        with pytest.raises(KeyError):
            operator.__getitem__(o, "f")
        with pytest.raises(AttributeError):
            getattr(o, "f")

    def test_inject(self):
        class foon(dict):
            self.inject(locals())

        self.test_AttrAccessible(foon)


class Test_ProxiedAttrs:
    kls = mappings.ProxiedAttrs

    def test_it(self):
        class foo:
            def __init__(self, **kwargs):
                for attr, val in kwargs.items():
                    setattr(self, attr, val)

        obj = foo()
        d = self.kls(obj)
        with pytest.raises(KeyError):
            operator.__getitem__(d, "x")
        with pytest.raises(KeyError):
            operator.__delitem__(d, "x")
        assert "x" not in d
        d["x"] = 1
        assert d["x"] == 1
        assert "x" in d
        assert ["x"] == list(x for x in d if not x.startswith("__"))
        del d["x"]
        assert "x" not in d
        with pytest.raises(KeyError):
            operator.__delitem__(d, "x")
        with pytest.raises(KeyError):
            operator.__getitem__(d, "x")

        # Finally, verify that immutable attribute errors are handled correctly.
        d = self.kls(object())
        with pytest.raises(KeyError):
            operator.__setitem__(d, "x", 1)
        with pytest.raises(KeyError):
            operator.__delitem__(d, "x")

    def test_against_slotted_backing(self):
        class slotted:
            __slots__ = ("x",)

        target = slotted()
        proxy = self.kls(target)
        assert [] == list(proxy.keys())
        target.x = 1
        assert ["x"] == list(proxy.keys())
        proxy["x"] = 2
        assert proxy["x"] == 2
        del proxy["x"]


class TestSlottedDict:
    kls = staticmethod(mappings.make_SlottedDict_kls)

    def test_exceptions(self):
        d = self.kls(["spork"])()
        for op in (operator.getitem, operator.delitem):
            with pytest.raises(KeyError):
                op(d, "spork")
            with pytest.raises(KeyError):
                op(d, "foon")
