from itertools import chain

import pytest

from snakeoil import containers


class TestInvertedContains:
    def setup_method(self, method):
        self.set = containers.InvertedContains(range(12))

    def test_basic(self):
        assert 7 not in self.set
        assert -7 in self.set
        with pytest.raises(TypeError):
            iter(self.set)


class BasicSet(containers.SetMixin):
    __slots__ = ("_data",)

    def __init__(self, data):
        self._data = set(data)

    def __iter__(self):
        return iter(self._data)

    def __contains__(self, other):
        return other in self._data

    # def __str__(self):
    #    return 'BasicSet([%s])' % ', '.join((str(x) for x in self._data))

    def __eq__(self, other):
        if isinstance(other, BasicSet):
            return self._data == other._data
        elif isinstance(other, (set, frozenset)):
            return self._data == other
        return False

    def __ne__(self, other):
        return not self == other


class TestSetMethods:
    def test_and(self):
        c = BasicSet(range(100))
        s = set(range(25, 75))
        r = BasicSet(range(25, 75))
        assert c & s == r
        assert s & c == r._data

    def test_xor(self):
        c = BasicSet(range(100))
        s = set(range(25, 75))
        r = BasicSet(chain(range(25), range(75, 100)))
        assert c ^ s == r
        assert s ^ c == r._data

    def test_or(self):
        c = BasicSet(range(50))
        s = set(range(50, 100))
        r = BasicSet(range(100))
        assert c | s == r
        assert s | c == r._data

    def test_add(self):
        c = BasicSet(range(50))
        s = set(range(50, 100))
        r = BasicSet(range(100))
        assert c + s == r
        assert s + c == r._data

    def test_sub(self):
        c = BasicSet(range(100))
        s = set(range(50, 150))
        r1 = BasicSet(range(50))
        r2 = set(range(100, 150))
        assert c - s == r1
        assert s - c == r2


class TestLimitedChangeSet:
    def setup_method(self, method):
        self.set = containers.LimitedChangeSet(range(12))

    def test_validator(self):
        def f(val):
            assert isinstance(val, int)
            return val

        self.set = containers.LimitedChangeSet(range(12), key_validator=f)
        self.set.add(13)
        self.set.add(14)
        self.set.remove(11)
        assert 5 in self.set
        with pytest.raises(AssertionError):
            self.set.add("2")
        with pytest.raises(AssertionError):
            self.set.remove("2")
        with pytest.raises(AssertionError):
            self.set.__contains__("2")

    def test_basic(self, changes=0):
        # this should be a no-op
        self.set.rollback(changes)
        # and this is invalid
        with pytest.raises(TypeError):
            self.set.rollback(changes + 1)
        assert 0 in self.set
        assert 12 not in self.set
        assert 12 == len(self.set)
        assert sorted(list(self.set)) == list(range(12))
        assert changes == self.set.changes_count()
        with pytest.raises(TypeError):
            self.set.rollback(-1)

    def test_dummy_commit(self):
        # this should be a no-op
        self.set.commit()
        # so this should should run just as before
        self.test_basic()

    def test_adding(self):
        self.set.add(13)
        assert 13 in self.set
        assert 13 == len(self.set)
        assert sorted(list(self.set)) == list(range(12)) + [13]
        assert 1 == self.set.changes_count()
        self.set.add(13)
        with pytest.raises(containers.Unchangable):
            self.set.remove(13)

    def test_add_rollback(self):
        self.set.add(13)
        self.set.rollback(0)
        # this should run just as before
        self.test_basic()

    def test_add_commit_remove_commit(self):
        self.set.add(13)
        self.set.commit()
        # should look like right before commit
        assert 13 == len(self.set)
        assert sorted(list(self.set)) == list(range(12)) + [13]
        assert 0 == self.set.changes_count()
        # and remove...
        self.set.remove(13)
        # should be back to basic, but with 1 change
        self.test_basic(1)
        self.set.commit()
        self.test_basic()

    def test_removing(self):
        self.set.remove(0)
        assert 0 not in self.set
        assert 11 == len(self.set)
        assert sorted(list(self.set)) == list(range(1, 12))
        assert 1 == self.set.changes_count()
        with pytest.raises(containers.Unchangable):
            self.set.add(0)
        with pytest.raises(KeyError):
            self.set.remove(0)

    def test_remove_rollback(self):
        self.set.remove(0)
        self.set.rollback(0)
        self.test_basic()

    def test_remove_commit_add_commit(self):
        self.set.remove(0)
        self.set.commit()
        assert 0 not in self.set
        assert 11 == len(self.set)
        assert sorted(list(self.set)) == list(range(1, 12))
        assert 0 == self.set.changes_count()
        self.set.add(0)
        self.test_basic(1)
        self.set.commit()
        self.test_basic()

    def test_longer_transaction(self):
        self.set.add(12)
        self.set.remove(7)
        self.set.rollback(1)
        self.set.add(-1)
        self.set.commit()
        assert sorted(list(self.set)) == list(range(-1, 13))

    def test_str(self):
        assert str(containers.LimitedChangeSet([7])) == "LimitedChangeSet([7])"

    def test__eq__(self):
        c = containers.LimitedChangeSet(range(99))
        c.add(99)
        assert c == containers.LimitedChangeSet(range(100))
        assert containers.LimitedChangeSet(range(100)) == set(range(100))
        assert containers.LimitedChangeSet([]) != object()


class TestLimitedChangeSetWithBlacklist:
    def setup_method(self, method):
        self.set = containers.LimitedChangeSet(range(12), [3, 13])

    def test_basic(self):
        assert 0 in self.set
        assert 12 not in self.set
        assert 12 == len(self.set)
        assert sorted(list(self.set)) == list(range(12))
        assert 0 == self.set.changes_count()
        with pytest.raises(TypeError):
            self.set.rollback(-1)

    def test_adding_blacklisted(self):
        with pytest.raises(containers.Unchangable):
            self.set.add(13)

    def test_removing_blacklisted(self):
        with pytest.raises(containers.Unchangable):
            self.set.remove(3)


class TestProtectedSet:
    def setup_method(self, method):
        self.set = containers.ProtectedSet(set(range(12)))

    def test_contains(self):
        assert 0 in self.set
        assert 15 not in self.set
        self.set.add(15)
        assert 15 in self.set

    def test_iter(self):
        assert list(range(12)) == sorted(self.set)
        self.set.add(5)
        assert list(range(12)) == sorted(self.set)
        self.set.add(12)
        assert list(range(13)) == sorted(self.set)

    def test_len(self):
        assert 12 == len(self.set)
        self.set.add(5)
        self.set.add(13)
        assert 13 == len(self.set)


class TestRefCountingSet:
    kls = containers.RefCountingSet

    def test_it(self):
        c = self.kls((1, 2))
        assert 1 in c
        assert 2 in c
        c.remove(1)
        assert 1 not in c
        with pytest.raises(KeyError):
            c.remove(1)
        c.add(2)
        assert 2 in c
        c.remove(2)
        assert 2 in c
        c.remove(2)
        assert 2 not in c
        c.add(3)
        assert 3 in c
        c.remove(3)
        assert 3 not in c

        c.discard(4)
        c.discard(5)
        c.discard(4)
        c.add(4)
        c.add(4)
        assert 4 in c
        c.discard(4)
        assert 4 in c
        c.discard(4)
        assert 4 not in c

    def test_init(self):
        assert self.kls(range(5))[4] == 1
        c = self.kls([1, 2, 3, 1])
        assert c[2] == 1
        assert c[1] == 2
