import pytest

from pkgcore.restrictions import boolean, restriction

true = restriction.AlwaysBool(node_type="foo", negate=True)
false = restriction.AlwaysBool(node_type="foo", negate=False)


class AlwaysForcableBool(boolean.base):
    __slots__ = ()

    def force_True(self, action, *args):
        yield True

    match = force_False = force_True


class base:
    kls = None

    def test_invalid_restrictions(self):
        with pytest.raises(TypeError):
            self.kls(42, node_type="foo")
        base = self.kls(node_type="foo")
        with pytest.raises(TypeError):
            base.add_restriction(42)
        with pytest.raises(TypeError):
            base.add_restriction()

    def test_init_finalize(self):
        final = self.kls(true, node_type="foo", finalize=True)
        # since it becomes a tuple, throws a AttributeError
        with pytest.raises(TypeError):
            final.add_restriction(false)

        final = self.kls(true, node_type="foo")
        # since it becomes a tuple, throws a AttributeError
        with pytest.raises(TypeError):
            final.add_restriction(false)

    def test_finalize(self):
        base = self.kls(true, node_type="foo", finalize=False)
        base.add_restriction(false)
        base.finalize()
        with pytest.raises(TypeError):
            base.add_restriction(true)

    def test_change_restrictions(self):
        base = self.kls(true, false)
        assert self.kls(false, true) == base.change_restrictions(false, true)
        assert self.kls(false, true) != base.change_restrictions(
            false, true, negate=True
        )
        assert self.kls(false, true, negate=True) == base.change_restrictions(
            false, true, negate=True
        )

    def test_add_restriction(self):
        with pytest.raises(TypeError):
            self.kls(true, finalize=True).add_restriction(false)
        with pytest.raises(TypeError):
            self.kls(node_type="foon").add_restriction(false)
        k = self.kls(finalize=False)
        k.add_restriction(false)
        assert k.restrictions == [false]

    # TODO total_len? what does it do?


# TODO these tests are way too limited
class TestAndRestriction(base):
    kls = boolean.AndRestriction

    def test_match(self):
        assert self.kls(true, true, node_type="foo").match(None)
        assert not self.kls(false, true, true, node_type="foo").match(None)
        assert not self.kls(true, false, true, node_type="foo").match(None)

    def test_negate_match(self):
        assert self.kls(false, true, node_type="foo", negate=True).match(None)
        assert self.kls(true, false, node_type="foo", negate=True).match(None)
        assert self.kls(false, false, node_type="foo", negate=True).match(None)
        assert not self.kls(true, true, node_type="foo", negate=True).match(None)

    def test_dnf_solutions(self):
        assert self.kls(true, true).dnf_solutions() == [[true, true]]
        assert self.kls(self.kls(true, true), true).dnf_solutions() == [
            [true, true, true]
        ]
        assert list(
            map(
                set,
                self.kls(
                    true, true, boolean.OrRestriction(false, true)
                ).dnf_solutions(),
            )
        ) == [{true, true, false}, {true, true, true}]
        assert self.kls().dnf_solutions() == [[]]

    def test_cnf_solutions(self):
        assert self.kls(true, true).cnf_solutions() == [[true], [true]]
        assert self.kls(self.kls(true, true), true).cnf_solutions() == [
            [true],
            [true],
            [true],
        ]
        assert list(
            self.kls(true, true, boolean.OrRestriction(false, true)).cnf_solutions()
        ) == [[true], [true], [false, true]]
        assert not self.kls().cnf_solutions()


class TestOrRestriction(base):
    kls = boolean.OrRestriction

    def test_match(self):
        assert self.kls(true, true, node_type="foo").match(None)
        assert self.kls(false, true, false, node_type="foo").match(None)
        assert self.kls(true, false, false, node_type="foo").match(None)
        assert self.kls(false, false, true, node_type="foo").match(None)
        assert not self.kls(false, false, node_type="foo").match(None)

    def test_negate_match(self):
        for x in ((true, false), (false, true), (true, true)):
            assert not self.kls(*x, node_type="foo", negate=True).match(None)
        assert self.kls(false, false, node_type="foo", negate=True).match(None)

    def test_dnf_solutions(self):
        assert self.kls(true, true).dnf_solutions() == [[true], [true]]
        assert list(
            map(
                set,
                self.kls(
                    true, true, boolean.AndRestriction(false, true)
                ).dnf_solutions(),
            )
        ) == list(map(set, [[true], [true], [false, true]]))
        assert self.kls(self.kls(true, false), true).dnf_solutions() == [
            [true],
            [false],
            [true],
        ]
        assert self.kls().dnf_solutions() == [[]]

    def test_cnf_solutions(self):
        assert self.kls(true, true).cnf_solutions() == [[true, true]]
        assert [
            set(x)
            for x in self.kls(
                true, true, boolean.AndRestriction(false, true)
            ).cnf_solutions()
        ] == [{true, false}, {true, true}]

        assert [
            set(x)
            for x in self.kls(
                self.kls(true, true, boolean.AndRestriction(false, true))
            ).cnf_solutions()
        ] == [{true, false}, {true, true}]

        assert set(self.kls(self.kls(true, false), true).cnf_solutions()[0]) == {
            true,
            false,
            true,
        }

        assert not self.kls().cnf_solutions()


class TestJustOneRestriction(base):
    kls = boolean.JustOneRestriction

    def test_match(self):
        assert self.kls(true, false, node_type="foo").match(None)
        assert self.kls(false, true, false, node_type="foo").match(None)
        assert not self.kls(false, false, node_type="foo").match(None)
        assert not self.kls(true, false, true, node_type="foo").match(None)
        assert not self.kls(true, true, true, node_type="foo").match(None)


class TestOperatorIdentity:
    """Distinct boolean operators must not compare or hash alike"""

    classes = (
        boolean.AndRestriction,
        boolean.OrRestriction,
        boolean.JustOneRestriction,
        boolean.AtMostOneOfRestriction,
    )

    def test_operators_are_distinct(self):
        built = [kls(true, false, node_type="foo") for kls in self.classes]
        for index, first in enumerate(built):
            for second in built[index + 1 :]:
                assert first != second, f"{first!r} == {second!r}"
                assert hash(first) != hash(second), f"{first!r} hashes as {second!r}"

    def test_same_operator_still_matches(self):
        for kls in self.classes:
            a = kls(true, false, node_type="foo")
            b = kls(true, false, node_type="foo")
            assert a == b
            assert hash(a) == hash(b)
            assert len({a, b}) == 1

    def test_negate_and_children_still_compared(self):
        for kls in self.classes:
            plain = kls(true, false, node_type="foo")
            assert plain != kls(true, false, node_type="foo", negate=True)
            assert plain != kls(false, true, node_type="foo")
