"""
Boolean Algebra.

Tests

Copyright (c) Sebastian Kraemer, basti.kr@gmail.com and others
SPDX-License-Identifier: BSD-2-Clause
"""

import unittest
from unittest.case import expectedFailure

from boolean import (
    TOKEN_AND,
    TOKEN_FALSE,
    TOKEN_LPAR,
    TOKEN_NOT,
    TOKEN_OR,
    TOKEN_RPAR,
    TOKEN_SYMBOL,
    TOKEN_TRUE,
    BooleanAlgebra,
    ParseError,
    Symbol,
)
from boolean.boolean import (
    PARSE_INVALID_EXPRESSION,
    PARSE_INVALID_NESTING,
    PARSE_INVALID_OPERATOR_SEQUENCE,
    PARSE_INVALID_SYMBOL_SEQUENCE,
    PARSE_UNKNOWN_TOKEN,
)


class BooleanAlgebraTestCase(unittest.TestCase):
    def test_creation(self):
        algebra = BooleanAlgebra()
        expr_str = "(a|b|c)&d&(~e|(f&g))"
        expr = algebra.parse(expr_str)
        assert str(expr) == expr_str

    def test_parse_with_mixed_operators_multilines_and_custom_symbol(self):
        class MySymbol(Symbol):
            pass

        expr_str = """(a or ~ b +_c  ) and
                      d & ( ! e_
                      | (my * g OR 1 or 0) ) AND that """

        algebra = BooleanAlgebra(Symbol_class=MySymbol)
        expr = algebra.parse(expr_str)

        expected = algebra.AND(
            algebra.OR(
                algebra.Symbol("a"),
                algebra.NOT(algebra.Symbol("b")),
                algebra.Symbol("_c"),
            ),
            algebra.Symbol("d"),
            algebra.OR(
                algebra.NOT(algebra.Symbol("e_")),
                algebra.OR(
                    algebra.AND(
                        algebra.Symbol("my"),
                        algebra.Symbol("g"),
                    ),
                    algebra.TRUE,
                    algebra.FALSE,
                ),
            ),
            algebra.Symbol("that"),
        )

        assert expr.pretty() == expected.pretty()
        assert expr == expected

    def test_parse_recognizes_trueish_and_falsish_symbol_tokens(self):
        expr_str = "True or False or None or 0 or 1 or TRue or FalSE or NONe"
        algebra = BooleanAlgebra()
        expr = algebra.parse(expr_str)
        expected = algebra.OR(
            algebra.TRUE,
            algebra.FALSE,
            algebra.FALSE,
            algebra.FALSE,
            algebra.TRUE,
            algebra.TRUE,
            algebra.FALSE,
            algebra.FALSE,
        )
        assert expr == expected

    def test_parse_can_use_iterable_from_alternative_tokenizer(self):
        class CustomSymbol(Symbol):
            pass

        class CustomAlgebra(BooleanAlgebra):
            def __init__(self, Symbol_class=CustomSymbol):
                super(CustomAlgebra, self).__init__(Symbol_class=Symbol_class)

            def tokenize(self, s):
                "Sample tokenizer using custom operators and symbols"
                ops = {
                    "WHY_NOT": TOKEN_OR,
                    "ALSO": TOKEN_AND,
                    "NEITHER": TOKEN_NOT,
                    "(": TOKEN_LPAR,
                    ")": TOKEN_RPAR,
                }

                for row, line in enumerate(s.splitlines(False)):
                    for col, tok in enumerate(line.split()):
                        if tok in ops:
                            yield ops[tok], tok, (row, col)
                        elif tok == "Custom":
                            yield self.Symbol(tok), tok, (row, col)
                        else:
                            yield TOKEN_SYMBOL, tok, (row, col)

        expr_str = """( Custom WHY_NOT regular ) ALSO NEITHER  (
                      not_custom ALSO standard )
                   """

        algebra = CustomAlgebra()
        expr = algebra.parse(expr_str)
        expected = algebra.AND(
            algebra.OR(
                algebra.Symbol("Custom"),
                algebra.Symbol("regular"),
            ),
            algebra.NOT(
                algebra.AND(
                    algebra.Symbol("not_custom"),
                    algebra.Symbol("standard"),
                ),
            ),
        )
        assert expr == expected

    def test_parse_with_advanced_tokenizer_example(self):
        import tokenize
        from io import StringIO

        class PlainVar(Symbol):
            "Plain boolean variable"

        class ColonDotVar(Symbol):
            "Colon and dot-separated string boolean variable"

        class AdvancedAlgebra(BooleanAlgebra):
            def tokenize(self, expr):
                """
                Example custom tokenizer derived from the standard Python tokenizer
                with a few extra features: #-style comments are supported and a
                colon- and dot-separated string is recognized and stored in custom
                symbols. In contrast with the standard tokenizer, only these
                boolean operators are recognized : & | ! and or not.

                For more advanced tokenization you could also consider forking the
                `tokenize` standard library module.
                """

                if not isinstance(expr, str):
                    raise TypeError("expr must be string but it is %s." % type(expr))

                # mapping of lowercase token strings to a token object instance for
                # standard operators, parens and common true or false symbols
                TOKENS = {
                    "&": TOKEN_AND,
                    "and": TOKEN_AND,
                    "|": TOKEN_OR,
                    "or": TOKEN_OR,
                    "!": TOKEN_NOT,
                    "not": TOKEN_NOT,
                    "(": TOKEN_LPAR,
                    ")": TOKEN_RPAR,
                    "true": TOKEN_TRUE,
                    "1": TOKEN_TRUE,
                    "false": TOKEN_FALSE,
                    "0": TOKEN_FALSE,
                    "none": TOKEN_FALSE,
                }

                ignored_token_types = (
                    tokenize.NL,
                    tokenize.NEWLINE,
                    tokenize.COMMENT,
                    tokenize.INDENT,
                    tokenize.DEDENT,
                    tokenize.ENDMARKER,
                )

                # note: an unbalanced expression may raise a TokenError here.
                tokens = (
                    (
                        toktype,
                        tok,
                        row,
                        col,
                    )
                    for toktype, tok, (
                        row,
                        col,
                    ), _, _ in tokenize.generate_tokens(StringIO(expr).readline)
                    if tok and tok.strip()
                )

                COLON_DOT = (
                    ":",
                    ".",
                )

                def build_symbol(current_dotted):
                    if current_dotted:
                        if any(s in current_dotted for s in COLON_DOT):
                            sym = ColonDotVar(current_dotted)
                        else:
                            sym = PlainVar(current_dotted)
                        return sym

                # accumulator for dotted symbols that span several `tokenize` tokens
                dotted, srow, scol = "", None, None

                for toktype, tok, row, col in tokens:
                    if toktype in ignored_token_types:
                        # we reached a break point and should yield the current dotted
                        symbol = build_symbol(dotted)
                        if symbol is not None:
                            yield symbol, dotted, (srow, scol)
                            dotted, srow, scol = "", None, None

                        continue

                    std_token = TOKENS.get(tok.lower())
                    if std_token is not None:
                        # we reached a break point and should yield the current dotted
                        symbol = build_symbol(dotted)
                        if symbol is not None:
                            yield symbol, dotted, (srow, scol)
                            dotted, srow, scol = "", 0, 0

                        yield std_token, tok, (row, col)

                        continue

                    if toktype == tokenize.NAME or (toktype == tokenize.OP and tok in COLON_DOT):
                        if not dotted:
                            srow = row
                            scol = col
                        dotted += tok

                    else:
                        raise TypeError(
                            "Unknown token: %(tok)r at line: %(row)r, column: %(col)r" % locals()
                        )

        test_expr = """
            (colon1:dot1.dot2 or colon2_name:col_on3:do_t1.do_t2.do_t3 )
            and
            ( plain_symbol & !Custom )
        """

        algebra = AdvancedAlgebra()
        expr = algebra.parse(test_expr)
        expected = algebra.AND(
            algebra.OR(
                ColonDotVar("colon1:dot1.dot2"),
                ColonDotVar("colon2_name:col_on3:do_t1.do_t2.do_t3"),
            ),
            algebra.AND(PlainVar("plain_symbol"), algebra.NOT(PlainVar("Custom"))),
        )
        assert expr == expected

    def test_allowing_additional_characters_in_tokens(self):
        algebra = BooleanAlgebra(allowed_in_token=(".", "_", "-", "+"))
        test_expr = "l-a AND b+c"

        expr = algebra.parse(test_expr)
        expected = algebra.AND(algebra.Symbol("l-a"), algebra.Symbol("b+c"))
        assert expr == expected

    def test_parse_raise_ParseError1(self):
        algebra = BooleanAlgebra()
        expr = "l-a AND none"

        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError2(self):
        algebra = BooleanAlgebra()
        expr = "(l-a + AND l-b"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError3(self):
        algebra = BooleanAlgebra()
        expr = "(l-a + AND l-b)"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError4(self):
        algebra = BooleanAlgebra()
        expr = "(l-a AND l-b"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError5(self):
        algebra = BooleanAlgebra()
        expr = "(l-a + AND l-b))"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError6(self):
        algebra = BooleanAlgebra()
        expr = "(l-a  AND l-b))"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError7(self):
        algebra = BooleanAlgebra()
        expr = "l-a AND"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_UNKNOWN_TOKEN

    def test_parse_raise_ParseError8(self):
        algebra = BooleanAlgebra()
        expr = "OR l-a"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_OPERATOR_SEQUENCE

    def test_parse_raise_ParseError9(self):
        algebra = BooleanAlgebra()
        expr = "+ l-a"
        try:
            algebra.parse(expr)
            self.fail("Exception should be raised when parsing '%s'" % expr)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_OPERATOR_SEQUENCE

    def test_parse_side_by_side_symbols_should_raise_exception_but_not(self):
        algebra = BooleanAlgebra()
        expr_str = "a or b c"
        try:
            algebra.parse(expr_str)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_SYMBOL_SEQUENCE

    def test_parse_side_by_side_symbols_should_raise_exception_but_not2(self):
        algebra = BooleanAlgebra()
        expr_str = "(a or b) c"
        try:
            algebra.parse(expr_str)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_EXPRESSION

    def test_parse_side_by_side_symbols_raise_exception(self):
        algebra = BooleanAlgebra()
        expr_str = "a b"
        try:
            algebra.parse(expr_str)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_SYMBOL_SEQUENCE

    def test_parse_side_by_side_symbols_with_parens_raise_exception(self):
        algebra = BooleanAlgebra()
        expr_str = "(a) (b)"
        try:
            algebra.parse(expr_str)
        except ParseError as pe:
            assert pe.error_code == PARSE_INVALID_NESTING


class BaseElementTestCase(unittest.TestCase):
    def test_creation(self):
        from boolean.boolean import BaseElement

        algebra = BooleanAlgebra()
        assert algebra.TRUE == algebra.TRUE
        BaseElement()
        self.assertRaises(TypeError, BaseElement, 2)
        self.assertRaises(TypeError, BaseElement, "a")
        assert algebra.TRUE is algebra.TRUE
        assert algebra.TRUE is not algebra.FALSE
        assert algebra.FALSE is algebra.FALSE
        assert bool(algebra.TRUE) is True
        assert bool(algebra.FALSE) is False
        assert algebra.TRUE == True
        assert algebra.FALSE == False

    def test_literals(self):
        algebra = BooleanAlgebra()
        assert algebra.TRUE.literals == set()
        assert algebra.FALSE.literals == set()

    def test_literalize(self):
        algebra = BooleanAlgebra()
        assert algebra.TRUE.literalize() == algebra.TRUE
        assert algebra.FALSE.literalize() == algebra.FALSE

    def test_simplify(self):
        algebra = BooleanAlgebra()
        assert algebra.TRUE.simplify() == algebra.TRUE
        assert algebra.FALSE.simplify() == algebra.FALSE

    def test_simplify_two_algebra(self):
        algebra1 = BooleanAlgebra()
        algebra2 = BooleanAlgebra()
        assert algebra1.TRUE.simplify() == algebra2.TRUE
        assert algebra1.FALSE.simplify() == algebra2.FALSE

    def test_dual(self):
        algebra = BooleanAlgebra()
        assert algebra.TRUE.dual == algebra.FALSE
        assert algebra.FALSE.dual == algebra.TRUE

    def test_equality(self):
        algebra = BooleanAlgebra()
        assert algebra.TRUE == algebra.TRUE
        assert algebra.FALSE == algebra.FALSE
        assert algebra.TRUE != algebra.FALSE

    def test_order(self):
        algebra = BooleanAlgebra()
        assert algebra.FALSE < algebra.TRUE
        assert algebra.TRUE > algebra.FALSE

    def test_printing(self):
        algebra = BooleanAlgebra()
        assert str(algebra.TRUE) == "1"
        assert str(algebra.FALSE) == "0"
        assert repr(algebra.TRUE) == "TRUE"
        assert repr(algebra.FALSE) == "FALSE"


class SymbolTestCase(unittest.TestCase):
    def test_init(self):
        Symbol(1)
        Symbol("a")
        Symbol(None)
        Symbol(sum)
        Symbol((1, 2, 3))
        Symbol([1, 2])

    def test_isliteral(self):
        assert Symbol(1).isliteral is True

    def test_literals(self):
        l1 = Symbol(1)
        l2 = Symbol(1)
        assert l1 in l1.literals
        assert l1 in l2.literals
        assert l2 in l1.literals
        assert l2 in l2.literals
        self.assertRaises(AttributeError, setattr, l1, "literals", 1)

    def test_literalize(self):
        s = Symbol(1)
        assert s.literalize() == s

    def test_simplify(self):
        s = Symbol(1)
        assert s.simplify() == s

    def test_simplify_different_instances(self):
        s1 = Symbol(1)
        s2 = Symbol(1)
        assert s1.simplify() == s2.simplify()

    def test_equal_symbols(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        a2 = algebra.Symbol("a")

        c = algebra.Symbol("b")
        d = algebra.Symbol("d")
        e = algebra.Symbol("e")

        # Test __eq__.
        assert a == a
        assert a == a2
        assert not a == c
        assert not a2 == c
        assert d == d
        assert not d == e
        assert not a == d
        # Test __ne__.
        assert not a != a
        assert not a != a2
        assert a != c
        assert a2 != c

    def test_order(self):
        S = Symbol
        assert S("x") < S("y")
        assert S("y") > S("x")
        assert S(1) < S(2)
        assert S(2) > S(1)

    def test_printing(self):
        assert str(Symbol("a")) == "a"
        assert str(Symbol(1)) == "1"
        assert repr(Symbol("a")) == "Symbol('a')"
        assert repr(Symbol(1)) == "Symbol(1)"


class NOTTestCase(unittest.TestCase):
    def test_init(self):
        algebra = BooleanAlgebra()
        self.assertRaises(TypeError, algebra.NOT)
        self.assertRaises(TypeError, algebra.NOT, "a", "b")
        algebra.NOT(algebra.Symbol("a"))
        assert (algebra.NOT(algebra.TRUE)).simplify() == algebra.FALSE
        assert (algebra.NOT(algebra.FALSE)).simplify() == algebra.TRUE

    def test_isliteral(self):
        algebra = BooleanAlgebra()
        s = algebra.Symbol(1)
        assert algebra.NOT(s).isliteral
        assert not algebra.parse("~(a|b)").isliteral

    def test_literals(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        l = ~a
        assert l.isliteral
        assert l in l.literals
        assert len(l.literals) == 1

        l = algebra.parse("~(a&a)")
        assert not l.isliteral
        assert a in l.literals
        assert len(l.literals) == 1

        l = algebra.parse("~(a&a)", simplify=True)
        assert l.isliteral

    def test_literalize(self):
        parse = BooleanAlgebra().parse
        assert parse("~a") == parse("~a").literalize()
        assert parse("~a|~b") == parse("~(a&b)").literalize()
        assert parse("~a&~b") == parse("~(a|b)").literalize()

    def test_simplify(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        assert ~a == ~a
        assert algebra.Symbol("a") == algebra.Symbol("a")
        assert algebra.parse("~~a") != a
        assert (~~a).simplify() == a
        assert (~~~a).simplify() == ~a
        assert (~~~~a).simplify() == a
        assert (~(a & a & a)).simplify() == (~(a & a & a)).simplify()
        assert algebra.parse("~~a", simplify=True) == a
        algebra2 = BooleanAlgebra()
        assert algebra2.parse("~~a", simplify=True) == a

    def test_cancel(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        assert (~a).cancel() == ~a
        assert algebra.parse("~~a").cancel() == a
        assert algebra.parse("~~~a").cancel() == ~a
        assert algebra.parse("~~~~a").cancel() == a

    def test_demorgan(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        b = algebra.Symbol("b")
        c = algebra.Symbol("c")
        assert algebra.parse("~(a&b)").demorgan() == ~a | ~b
        assert algebra.parse("~(a|b|c)").demorgan() == algebra.parse("~a&~b&~c")
        assert algebra.parse("~(~a&b)").demorgan() == a | ~b
        assert (~~(a & b | c)).demorgan() == a & b | c
        assert (~~~(a & b | c)).demorgan() == ~(a & b) & ~c
        assert algebra.parse("~" * 10 + "(a&b|c)").demorgan() == a & b | c
        assert algebra.parse("~" * 11 + "(a&b|c)").demorgan() == (~(a & b | c)).demorgan()
        _0 = algebra.FALSE
        _1 = algebra.TRUE
        assert (~(_0)).demorgan() == _1
        assert (~(_1)).demorgan() == _0

    def test_order(self):
        algebra = BooleanAlgebra()
        x = algebra.Symbol(1)
        y = algebra.Symbol(2)
        assert x < ~x
        assert ~x > x
        assert ~x < y
        assert y > ~x

    def test_printing(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        assert str(~a) == "~a"
        assert repr(~a) == "NOT(Symbol('a'))"
        expr = algebra.parse("~(a&a)")
        assert str(expr) == "~(a&a)"
        assert repr(expr), "NOT(AND(Symbol('a') == Symbol('a')))"


class DualBaseTestCase(unittest.TestCase):

    maxDiff = None

    def test_init(self):
        from boolean.boolean import DualBase

        a, b, c = Symbol("a"), Symbol("b"), Symbol("c")
        t1 = DualBase(a, b)
        t2 = DualBase(a, b, c)
        t3 = DualBase(a, a)
        t4 = DualBase(a, b, c)

        self.assertRaises(TypeError, DualBase)
        for term in (t1, t2, t3, t4):
            assert isinstance(term, DualBase)

    def test_isliteral(self):
        from boolean.boolean import DualBase

        a, b, c = Symbol("a"), Symbol("b"), Symbol("c")
        t1 = DualBase(a, b)
        t2 = DualBase(a, b, c)

        assert not t1.isliteral
        assert not t2.isliteral

    def test_literals(self):
        from boolean.boolean import DualBase

        a, b, c = Symbol("a"), Symbol("b"), Symbol("c")
        t1 = DualBase(a, b)
        t2 = DualBase(a, b, c)
        t3 = DualBase(a, a)
        t4 = DualBase(a, b, c)

        for term in (t1, t2, t3, t4):
            assert a in term.literals
        for term in (t1, t2, t4):
            assert b in term.literals
        for term in (t2, t4):
            assert c in term.literals

    def test_literalize(self):
        parse = BooleanAlgebra().parse
        assert parse("a|~(b|c)").literalize() == parse("a|(~b&~c)")

    def test_annihilator(self):
        algebra = BooleanAlgebra()
        assert algebra.parse("a&a").annihilator == algebra.FALSE
        assert algebra.parse("a|a").annihilator == algebra.TRUE

    def test_identity(self):
        algebra = BooleanAlgebra()
        assert algebra.parse("a|b").identity == algebra.FALSE
        assert algebra.parse("a&b").identity == algebra.TRUE

    def test_dual(self):
        algebra = BooleanAlgebra()
        assert algebra.AND(algebra.Symbol("a"), algebra.Symbol("b")).dual == algebra.OR
        assert algebra.OR(algebra.Symbol("a"), algebra.Symbol("b")).dual == algebra.AND

        assert algebra.parse("a|b").dual == algebra.AND
        assert algebra.parse("a&b").dual == algebra.OR

    def test_simplify(self):
        algebra1 = BooleanAlgebra()
        algebra2 = BooleanAlgebra()
        a = algebra1.Symbol("a")
        b = algebra1.Symbol("b")
        c = algebra1.Symbol("c")

        _0 = algebra1.FALSE
        _1 = algebra1.TRUE
        # Idempotence
        assert (a & a).simplify() == a
        # Idempotence + Associativity
        assert (a | (a | b)).simplify() == a | b
        # Annihilation
        assert (a & _0).simplify() == _0
        assert (a | _1).simplify() == _1
        # Identity
        assert (a & _1).simplify() == a
        assert (a | _0).simplify() == a
        # Complementation
        assert (a & ~a).simplify() == _0
        assert (a | ~a).simplify() == _1
        # Absorption
        assert (a & (a | b)).simplify() == a
        assert (a | (a & b)).simplify() == a
        assert ((b & a) | (b & a & c)).simplify() == b & a

        # Elimination
        assert ((a & ~b) | (a & b)).simplify() == a

        # Commutativity + Non-Commutativity
        sorted_expression = (b & b & a).simplify()
        unsorted_expression = (b & b & a).simplify(sort=False)
        assert unsorted_expression == sorted_expression
        assert sorted_expression.pretty() != unsorted_expression.pretty()

        sorted_expression = (b | b | a).simplify()
        unsorted_expression = (b | b | a).simplify(sort=False)
        assert unsorted_expression == sorted_expression
        assert sorted_expression.pretty() != unsorted_expression.pretty()

        expected = algebra1.parse("(a&b)|(b&c)|(a&c)")
        result = algebra1.parse("(~a&b&c) | (a&~b&c) | (a&b&~c) | (a&b&c)", simplify=True)
        assert result == expected

        expected = algebra1.parse("(a&b)|(b&c)|(a&c)")
        result = algebra2.parse("(~a&b&c) | (a&~b&c) | (a&b&~c) | (a&b&c)", simplify=True)
        assert result == expected

        expected = algebra1.parse("b&d")
        result = algebra1.parse("(a&b&c&d) | (b&d)", simplify=True)
        assert result == expected

        expected = algebra1.parse("b&d")
        result = algebra2.parse("(a&b&c&d) | (b&d)", simplify=True)
        assert result == expected

        expected = algebra1.parse("(~b&~d&a) | (~c&~d&b) | (a&c&d)", simplify=True)
        result = algebra1.parse(
            """(~a&b&~c&~d) | (a&~b&~c&~d) | (a&~b&c&~d) |
                          (a&~b&c&d) | (a&b&~c&~d) | (a&b&c&d)""",
            simplify=True,
        )
        assert result.pretty() == expected.pretty()

        expected = algebra1.parse("(~b&~d&a) | (~c&~d&b) | (a&c&d)", simplify=True)
        result = algebra2.parse(
            """(~a&b&~c&~d) | (a&~b&~c&~d) | (a&~b&c&~d) |
                          (a&~b&c&d) | (a&b&~c&~d) | (a&b&c&d)""",
            simplify=True,
        )
        assert result.pretty() == expected.pretty()

    @expectedFailure
    def test_parse_complex_expression_should_create_same_expression_as_python(self):
        algebra = BooleanAlgebra()
        a, b, c = algebra.symbols(*"abc")

        test_expression_str = """(~a | ~b | ~c)"""
        parsed = algebra.parse(test_expression_str)
        test_expression = ~a | ~b | ~c  # & ~d
        # print()
        # print('parsed')
        # print(parsed.pretty())
        # print('python')
        # print(test_expression.pretty())
        # we have a different behavior for expressions built from python expressions
        # vs. expression built from an object tree vs. expression built from a parse
        assert parsed.pretty() == test_expression.pretty()
        assert parsed == test_expression

    @expectedFailure
    def test_simplify_complex_expression_parsed_with_simplify(self):
        # FIXME: THIS SHOULD NOT FAIL
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        b = algebra.Symbol("b")
        c = algebra.Symbol("c")
        d = algebra.Symbol("d")

        test_expression_str = """
            (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) |
            (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) |
            (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d)
            """

        parsed = algebra.parse(test_expression_str, simplify=True)

        test_expression = (
            (~a & ~b & ~c & ~d)
            | (~a & ~b & ~c & d)
            | (~a & b & ~c & ~d)
            | (~a & b & c & d)
            | (~a & b & ~c & d)
            | (~a & b & c & ~d)
            | (a & ~b & ~c & d)
            | (~a & b & c & d)
            | (a & ~b & c & d)
            | (a & b & c & d)
        ).simplify()

        # we have a different simplify behavior for expressions built from python expressions
        # vs. expression built from an object tree vs. expression built from a parse
        assert parsed.pretty() == test_expression.pretty()

    @expectedFailure
    def test_complex_expression_without_parens_parsed_or_built_in_python_should_be_identical(self):
        # FIXME: THIS SHOULD NOT FAIL
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        b = algebra.Symbol("b")
        c = algebra.Symbol("c")
        d = algebra.Symbol("d")

        test_expression_str = """
            ~a&~b&~c&~d | ~a&~b&~c&d | ~a&b&~c&~d |
            ~a&b&c&d | ~a&b&~c&d | ~a&b&c&~d |
            a&~b&~c&d | ~a&b&c&d | a&~b&c&d | a&b&c&d
            """

        parsed = algebra.parse(test_expression_str)

        test_expression = (
            ~a & ~b & ~c & ~d
            | ~a & ~b & ~c & d
            | ~a & b & ~c & ~d
            | ~a & b & c & d
            | ~a & b & ~c & d
            | ~a & b & c & ~d
            | a & ~b & ~c & d
            | ~a & b & c & d
            | a & ~b & c & d
            | a & b & c & d
        )

        assert parsed.pretty() == test_expression.pretty()

    @expectedFailure
    def test_simplify_complex_expression_parsed_then_simplified(self):
        # FIXME: THIS SHOULD NOT FAIL

        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        b = algebra.Symbol("b")
        c = algebra.Symbol("c")
        d = algebra.Symbol("d")
        parse = algebra.parse

        test_expression_str = "".join(
            """
            (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) |
            (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) |
            (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d)
        """.split()
        )

        test_expression = (
            (~a & ~b & ~c & ~d)
            | (~a & ~b & ~c & d)
            | (~a & b & ~c & ~d)
            | (~a & b & c & d)
            | (~a & b & ~c & d)
            | (~a & b & c & ~d)
            | (a & ~b & ~c & d)
            | (~a & b & c & d)
            | (a & ~b & c & d)
            | (a & b & c & d)
        )

        parsed = parse(test_expression_str)
        assert test_expression_str == str(parsed)

        expected = (a & ~b & d) | (~a & b) | (~a & ~c) | (b & c & d)
        assert test_expression.simplify().pretty() == expected.pretty()

        parsed = parse(test_expression_str, simplify=True)

        # FIXME: THIS SHOULD NOT FAIL
        # we have a different simplify behavior for expressions built from python expressions
        # vs. expression built from an object tree vs. expression built from a parse
        assert parsed.simplify().pretty() == expected.simplify().pretty()

        expected_str = "(a&~b&d)|(~a&b)|(~a&~c)|(b&c&d)"
        assert str(parsed) == expected_str

        parsed2 = parse(test_expression_str)
        assert parsed2.simplify().pretty() == expected.pretty()

        assert str(parsed2.simplify()) == expected_str

        expected = algebra.OR(
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.NOT(algebra.Symbol("b")),
                algebra.NOT(algebra.Symbol("c")),
                algebra.NOT(algebra.Symbol("d")),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.NOT(algebra.Symbol("b")),
                algebra.NOT(algebra.Symbol("c")),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.Symbol("b"),
                algebra.NOT(algebra.Symbol("c")),
                algebra.NOT(algebra.Symbol("d")),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.Symbol("b"),
                algebra.Symbol("c"),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.Symbol("b"),
                algebra.NOT(algebra.Symbol("c")),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.Symbol("b"),
                algebra.Symbol("c"),
                algebra.NOT(algebra.Symbol("d")),
            ),
            algebra.AND(
                algebra.Symbol("a"),
                algebra.NOT(algebra.Symbol("b")),
                algebra.NOT(algebra.Symbol("c")),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.NOT(algebra.Symbol("a")),
                algebra.Symbol("b"),
                algebra.Symbol("c"),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.Symbol("a"),
                algebra.NOT(algebra.Symbol("b")),
                algebra.Symbol("c"),
                algebra.Symbol("d"),
            ),
            algebra.AND(
                algebra.Symbol("a"), algebra.Symbol("b"), algebra.Symbol("c"), algebra.Symbol("d")
            ),
        )

        result = parse(test_expression_str)
        result = result.simplify()
        assert result == expected

    def test_parse_invalid_nested_and_should_raise_a_proper_exception(self):
        algebra = BooleanAlgebra()
        expr = """a (and b)"""

        with self.assertRaises(ParseError) as context:
            algebra.parse(expr)

            assert context.exception.error_code == PARSE_INVALID_NESTING

    def test_subtract(self):
        parse = BooleanAlgebra().parse
        expr = parse("a&b&c")
        p1 = parse("b&d")
        p2 = parse("a&c")
        result = parse("b")
        assert expr.subtract(p1, simplify=True) == expr
        assert expr.subtract(p2, simplify=True) == result

    def test_flatten(self):
        parse = BooleanAlgebra().parse

        t1 = parse("a & (b&c)")
        t2 = parse("a&b&c")
        assert t1 != t2
        assert t1.flatten() == t2

        t1 = parse("a | ((b&c) | (a&c)) | b")
        t2 = parse("a | (b&c) | (a&c) | b")
        assert t1 != t2
        assert t1.flatten() == t2

    def test_distributive(self):
        algebra = BooleanAlgebra()
        a = algebra.Symbol("a")
        b = algebra.Symbol("b")
        c = algebra.Symbol("c")
        d = algebra.Symbol("d")
        e = algebra.Symbol("e")
        assert (a & (b | c)).distributive() == (a & b) | (a & c)
        t1 = algebra.AND(a, (b | c), (d | e))
        t2 = algebra.OR(
            algebra.AND(a, b, d), algebra.AND(a, b, e), algebra.AND(a, c, d), algebra.AND(a, c, e)
        )
        assert t1.distributive() == t2

    def test_equal(self):
        from boolean.boolean import DualBase

        a, b, c = Symbol("a"), Symbol("b"), Symbol("c")
        t1 = DualBase(a, b)
        t1_2 = DualBase(b, a)

        t2 = DualBase(a, b, c)
        t2_2 = DualBase(b, c, a)

        # Test __eq__.
        assert t1 == t1
        assert t1_2 == t1
        assert t2_2 == t2
        assert not t1 == t2
        assert not t1 == 1
        assert not t1 is True
        assert not t1 is None

        # Test __ne__.
        assert not t1 != t1
        assert not t1_2 != t1
        assert not t2_2 != t2
        assert t1 != t2
        assert t1 != 1
        assert t1 is not True
        assert t1 is not None

    def test_order(self):
        algebra = BooleanAlgebra()
        x, y, z = algebra.Symbol(1), algebra.Symbol(2), algebra.Symbol(3)
        assert algebra.AND(x, y) < algebra.AND(x, y, z)
        assert not algebra.AND(x, y) > algebra.AND(x, y, z)
        assert algebra.AND(x, y) < algebra.AND(x, z)
        assert not algebra.AND(x, y) > algebra.AND(x, z)
        assert algebra.AND(x, y) < algebra.AND(y, z)
        assert not algebra.AND(x, y) > algebra.AND(y, z)
        assert not algebra.AND(x, y) < algebra.AND(x, y)
        assert not algebra.AND(x, y) > algebra.AND(x, y)

    def test_printing(self):
        parse = BooleanAlgebra().parse
        assert str(parse("a&a")) == "a&a"
        assert repr(parse("a&a")), "AND(Symbol('a') == Symbol('a'))"
        assert str(parse("a|a")) == "a|a"
        assert repr(parse("a|a")), "OR(Symbol('a') == Symbol('a'))"
        assert str(parse("(a|b)&c")) == "(a|b)&c"
        assert repr(parse("(a|b)&c")), "AND(OR(Symbol('a'), Symbol('b')) == Symbol('c'))"


class OtherTestCase(unittest.TestCase):
    def test_class_order(self):
        # FIXME: this test is cryptic: what does it do?
        algebra = BooleanAlgebra()
        order = (
            (algebra.TRUE, algebra.FALSE),
            (algebra.Symbol("y"), algebra.Symbol("x")),
            (algebra.parse("x&y"),),
            (algebra.parse("x|y"),),
        )
        for i, tests in enumerate(order):
            for case1 in tests:
                for j in range(i + 1, len(order)):
                    for case2 in order[j]:
                        assert case1 < case2
                        assert case2 > case1

    def test_parse(self):
        algebra = BooleanAlgebra()
        a, b, c = algebra.Symbol("a"), algebra.Symbol("b"), algebra.Symbol("c")
        assert algebra.parse("0") == algebra.FALSE
        assert algebra.parse("(0)") == algebra.FALSE
        assert algebra.parse("1") == algebra.TRUE
        assert algebra.parse("(1)") == algebra.TRUE
        assert algebra.parse("a") == a
        assert algebra.parse("(a)") == a
        assert algebra.parse("(a)") == a
        assert algebra.parse("~a") == algebra.parse("~(a)")
        assert algebra.parse("~(a)") == algebra.parse("(~a)")
        assert algebra.parse("~a") == ~a
        assert algebra.parse("(~a)") == ~a
        assert algebra.parse("~~a", simplify=True) == (~~a).simplify()
        assert algebra.parse("a&b") == a & b
        assert algebra.parse("~a&b") == ~a & b
        assert algebra.parse("a&~b") == a & ~b
        assert algebra.parse("a&b&c") == algebra.parse("a&b&c")
        assert algebra.parse("a&b&c") == algebra.AND(a, b, c)
        assert algebra.parse("~a&~b&~c") == algebra.parse("~a&~b&~c")
        assert algebra.parse("~a&~b&~c") == algebra.AND(~a, ~b, ~c)
        assert algebra.parse("a|b") == a | b
        assert algebra.parse("~a|b") == ~a | b
        assert algebra.parse("a|~b") == a | ~b
        assert algebra.parse("a|b|c") == algebra.parse("a|b|c")
        assert algebra.parse("a|b|c") == algebra.OR(a, b, c)
        assert algebra.parse("~a|~b|~c") == algebra.OR(~a, ~b, ~c)
        assert algebra.parse("(a|b)") == a | b
        assert algebra.parse("a&(a|b)", simplify=True) == (a & (a | b)).simplify()
        assert algebra.parse("a&(a|~b)", simplify=True) == (a & (a | ~b)).simplify()
        assert (
            algebra.parse("(a&b)|(b&((c|a)&(b|(c&a))))", simplify=True)
            == ((a & b) | (b & ((c | a) & (b | (c & a))))).simplify()
        )
        assert algebra.parse("(a&b)|(b&((c|a)&(b|(c&a))))", simplify=True) == algebra.parse(
            "a&b | b&(c|a)&(b|c&a)", simplify=True
        )
        assert algebra.Symbol("1abc") == algebra.parse("1abc")
        assert algebra.Symbol("_abc") == algebra.parse("_abc")

    def test_subs(self):
        algebra = BooleanAlgebra()
        a, b, c = algebra.Symbol("a"), algebra.Symbol("b"), algebra.Symbol("c")
        expr = a & b | c
        assert expr.subs({a: b}).simplify() == b | c
        assert expr.subs({a: a}).simplify() == expr
        assert expr.subs({a: b | c}).simplify() == algebra.parse("(b|c)&b|c").simplify()
        assert expr.subs({a & b: a}).simplify() == a | c
        assert expr.subs({c: algebra.TRUE}).simplify() == algebra.TRUE

    def test_subs_default(self):
        algebra = BooleanAlgebra()
        a, b, c = algebra.Symbol("a"), algebra.Symbol("b"), algebra.Symbol("c")
        expr = a & b | c
        assert expr.subs({}, default=algebra.TRUE).simplify() == algebra.TRUE
        assert (
            expr.subs({a: algebra.FALSE, c: algebra.FALSE}, default=algebra.TRUE).simplify()
            == algebra.FALSE
        )
        assert algebra.TRUE.subs({}, default=algebra.FALSE).simplify() == algebra.TRUE
        assert algebra.FALSE.subs({}, default=algebra.TRUE).simplify() == algebra.FALSE

    def test_normalize(self):
        algebra = BooleanAlgebra()

        expr = algebra.parse("a&b")
        assert algebra.dnf(expr) == expr
        assert algebra.cnf(expr) == expr

        expr = algebra.parse("a|b")
        assert algebra.dnf(expr) == expr
        assert algebra.cnf(expr) == expr

        expr = algebra.parse("(a&b)|(c&b)")
        result_dnf = algebra.parse("(a&b)|(b&c)")
        result_cnf = algebra.parse("b&(a|c)")
        assert algebra.dnf(expr) == result_dnf
        assert algebra.cnf(expr) == result_cnf

        expr = algebra.parse("(a|b)&(c|b)")
        result_dnf = algebra.parse("b|(a&c)")
        result_cnf = algebra.parse("(a|b)&(b|c)")
        assert algebra.dnf(expr) == result_dnf
        assert algebra.cnf(expr) == result_cnf

        expr = algebra.parse("((s|a)&(s|b)&(s|c)&(s|d)&(e|c|d))|(a&e&d)")
        result = algebra.normalize(expr, expr.AND)
        expected = algebra.parse("(a|s)&(b|e|s)&(c|d|e)&(c|e|s)&(d|s)")
        assert expected == result

    def test_get_literals_return_all_literals_in_original_order(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and b or a and c")
        assert [
            alg.Symbol("a"),
            alg.Symbol("b"),
            alg.Symbol("a"),
            alg.Symbol("c"),
        ] == exp.get_literals()

    def test_get_symbols_return_all_symbols_in_original_order(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and b or True and a and c")
        assert [
            alg.Symbol("a"),
            alg.Symbol("b"),
            alg.Symbol("a"),
            alg.Symbol("c"),
        ] == exp.get_symbols()

    def test_literals_return_set_of_unique_literals(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and b or a and c")
        assert set([alg.Symbol("a"), alg.Symbol("b"), alg.Symbol("c")]) == exp.literals

    def test_literals_and_negation(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and not b and not not c")
        assert set([alg.Symbol("a"), alg.parse("not b"), alg.parse("not c")]) == exp.literals

    def test_symbols_and_negation(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and not b and not not c")
        assert set([alg.Symbol("a"), alg.Symbol("b"), alg.Symbol("c")]) == exp.symbols

    def test_objects_return_set_of_unique_Symbol_objs(self):
        alg = BooleanAlgebra()
        exp = alg.parse("a and b or a and c")
        assert set(["a", "b", "c"]) == exp.objects

    def test_normalize_blowup(self):
        from boolean import AND, NOT, OR
        from collections import defaultdict

        # Subclasses to count calls to simplify
        class CountingNot(NOT):
            def simplify(self):
                counts["CountingNot"] += 1
                return super().simplify()

        class CountingAnd(AND):
            def simplify(self, sort=True):
                counts["CountingAnd"] += 1
                return super().simplify(sort=sort)

        class CountingOr(OR):
            def simplify(self, sort=True):
                counts["CountingOr"] += 1
                return super().simplify(sort=sort)

        counts = defaultdict(int)

        # Real-world example of a complex expression with simple CNF/DNF form.
        # Note this is a more reduced, milder version of the problem, for rapid
        # testing.
        formula = """
        a & (
            (b & c & d & e & f & g)
            | (c & f & g & h & i & j)
            | (c & d & f & g & i & l & o & u)
            | (c & e & f & g & i & p & y & ~v)
            | (c & f & g & i & j & z & ~(c & f & g & i & j & k))
            | (c & f & g & t & ~(b & c & d & e & f & g))
            | (c & f & g & ~t & ~(b & c & d & e & f & g))
        )
        """
        algebra = BooleanAlgebra(
            NOT_class=CountingNot,
            AND_class=CountingAnd,
            OR_class=CountingOr,
        )

        expr = algebra.parse(formula)
        cnf = algebra.cnf(expr)
        assert str(cnf) == "a&c&f&g"
        # We should get exactly this count of calls.
        # before we had a combinatorial explosion
        assert counts == {"CountingAnd": 44, "CountingNot": 193, "CountingOr": 2490}


class BooleanBoolTestCase(unittest.TestCase):
    def test_bool(self):
        algebra = BooleanAlgebra()
        a, b, c = algebra.Symbol("a"), algebra.Symbol("b"), algebra.Symbol("c")
        expr = a & b | c
        self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE}))
        self.assertRaises(TypeError, bool, expr.subs({b: algebra.TRUE}))
        self.assertRaises(TypeError, bool, expr.subs({c: algebra.TRUE}))
        self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE, b: algebra.TRUE}))
        result = expr.subs({c: algebra.TRUE}, simplify=True)
        result = result.simplify()
        assert result == algebra.TRUE

        result = expr.subs({a: algebra.TRUE, b: algebra.TRUE}, simplify=True)
        result = result.simplify()
        assert result == algebra.TRUE


class CustomSymbolTestCase(unittest.TestCase):
    def test_custom_symbol(self):
        class CustomSymbol(Symbol):
            def __init__(self, name, value="value"):
                self.var = value
                super(CustomSymbol, self).__init__(name)

        try:
            CustomSymbol("a", value="This is A")
        except TypeError as e:
            self.fail(e)


class CallabilityTestCase(unittest.TestCase):
    def test_and(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("a&b&c")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    assert exp(a=a, b=b, c=c) == (a and b and c)

    def test_or(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("a|b|c")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    assert exp(a=a, b=b, c=c) == (a or b or c)

    def test_not(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("!a")
        for a in [True, False]:
            assert exp(a=a) == (not a)

    def test_symbol(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("a")
        for a in [True, False]:
            assert exp(a=a) == a

    def test_composite(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("!(a|b&(a|!c))")
        for a in [True, False]:
            for b in [True, False]:
                for c in [True, False]:
                    assert exp(a=a, b=b, c=c) == (not (a or b and (a or not c)))

    def test_negate_A_or_B(self):
        algebra = BooleanAlgebra()
        exp = algebra.parse("!(a|b)")
        for a in [True, False]:
            for b in [True, False]:
                assert exp(a=a, b=b) == (not (a or b))
