from odoo import Command
from odoo.exceptions import ValidationError
from odoo.tests import common
from odoo.tools import SetDefinitions


@common.tagged('at_install', 'groups')
class TestGroupsObject(common.BaseCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.definitions = SetDefinitions({
            1: {'ref': 'A'},
            2: {'ref': 'A1', 'supersets': [1]},         # A1 <= A
            3: {'ref': 'A11', 'supersets': [2]},        # A11 <= A1
            4: {'ref': 'A2', 'supersets': [1]},         # A2 <= A
            5: {'ref': 'A21', 'supersets': [4]},        # A21 <= A2
            6: {'ref': 'A22', 'supersets': [4]},        # A22 <= A2
            7: {'ref': 'B'},
            8: {'ref': 'B1', 'supersets': [7]},         # B1 <= B
            9: {'ref': 'B11', 'supersets': [8]},        # B11 <= B1
            10: {'ref': 'B2', 'supersets': [7]},        # B2 <= B
            11: {'ref': 'BX',
                 'supersets': [7],                      # BX <= B
                 'disjoints': [8, 10]},                 # BX disjoint from B1, B2
            12: {'ref': 'A1B1', 'supersets': [2, 8]},   # A1B1 <= A1, B1
            13: {'ref': 'C'},
            14: {'ref': 'D', 'disjoints': [1, 7]},      # D disjoint from A, B
            15: {'ref': 'E', 'disjoints': [1, 7, 14]},  # E disjoint from A, B, D
            16: {'ref': 'E1', 'supersets': [15]},       # E1 <= E (and thus disjoint from A, B, D)
        })

    def test_groups_1_base(self):
        A = self.definitions.parse('A')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')

        self.assertTrue(hash(A), "'Group object must be hashable'")
        self.assertEqual(str(A), "'A'")
        self.assertEqual(str(B), "'B'")
        self.assertEqual(str(B1), "'B1'")

    def test_groups_2_and(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B11 = self.definitions.parse('B11')
        BX = self.definitions.parse('BX')
        universe = self.definitions.universe
        empty = self.definitions.empty

        self.assertEqual(str(A & B), "'A' & 'B'")
        self.assertEqual(str(B & A), "'A' & 'B'")
        self.assertEqual(str(B & BX), "'BX'")
        self.assertEqual(str(B1 & BX), "~*")
        self.assertEqual(str(B11 & BX), "~*")
        self.assertEqual(str(empty & empty), "~*")
        self.assertEqual(str(A & universe), "'A'")
        self.assertEqual(str(A & empty), "~*")
        self.assertEqual(str(A1 & ~A), "~*")
        self.assertEqual(str(A & A1 & universe), "'A1'")

    def test_groups_3_or(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B11 = self.definitions.parse('B11')
        B2 = self.definitions.parse('B2')
        BX = self.definitions.parse('BX')
        universe = self.definitions.universe
        empty = self.definitions.empty

        self.assertEqual(str(A | A), "'A'")
        self.assertEqual(str(A | B), "'A' | 'B'")
        self.assertEqual(str(A1 | A), "'A'")
        self.assertEqual(str(A | A1), "'A'")
        self.assertEqual(str(A | B1), "'A' | 'B1'")
        self.assertEqual(str(B | A), "'A' | 'B'")
        self.assertEqual(str(B | BX), "'B'")
        self.assertEqual(str(B1 | BX), "'B1' | 'BX'")
        self.assertEqual(str(B11 | BX), "'B11' | 'BX'")
        self.assertEqual(str(empty | empty), "~*")
        self.assertEqual(str(A | B11 | B2), "'A' | 'B11' | 'B2'")
        self.assertEqual(str(A | B2 | B11), "'A' | 'B11' | 'B2'")
        self.assertEqual(str(A | empty), "'A'")
        self.assertEqual(str(A | universe), "*")
        self.assertEqual(str((A | A1) | empty), "'A'")

    def test_groups_3_or_and(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A2 = self.definitions.parse('A2')
        B1 = self.definitions.parse('B1')
        B2 = self.definitions.parse('B2')
        universe = self.definitions.universe
        empty = self.definitions.empty

        self.assertEqual(str((A & B1) | B2), "('A' & 'B1') | 'B2'")
        self.assertEqual(str(A | B1 & B2), "'A' | ('B1' & 'B2')")
        self.assertEqual(str(A | A1 & universe), "'A'")
        self.assertEqual(str((A1 | A2) & (B1 | B2)), "('A1' & 'B1') | ('A1' & 'B2') | ('A2' & 'B1') | ('A2' & 'B2')")
        self.assertEqual(str(A | (A1 | empty)), "'A'")
        self.assertEqual(str((A & A1) | empty), "'A1'")
        self.assertEqual(str(A & (A1 | empty)), "'A1'")

    def test_groups_4_gt_lt(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        A2 = self.definitions.parse('A2')
        A21 = self.definitions.parse('A21')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B11 = self.definitions.parse('B11')
        B2 = self.definitions.parse('B2')
        A1B1 = self.definitions.parse('A1B1')

        self.assertEqual(A == A, True)
        self.assertEqual(A == B, False)

        self.assertEqual(A >= A1, True)
        self.assertEqual(A >= A, True)
        self.assertEqual((A & B) >= B, False)
        self.assertEqual(B1 >= A1B1, True)
        self.assertEqual(B1 >= (A1 | A1B1), False)  # noqa: SIM300
        self.assertEqual(B >= (A & B), True)  # noqa: SIM300

        self.assertEqual(A > B, False)
        self.assertEqual(A > A1, True)
        self.assertEqual(A1 > A, False)
        self.assertEqual(A > A, False)
        self.assertEqual(A > A11, True)
        self.assertEqual(A > A2, True)
        self.assertEqual(A > A21, True)
        self.assertEqual(A1 > A11, True)
        self.assertEqual(A2 > A11, False)
        self.assertEqual(A2 > A21, True)
        self.assertEqual(A > B1, False)
        self.assertEqual(A > B11, False)
        self.assertEqual(A > B2, False)

        self.assertEqual(A <= A, True)
        self.assertEqual(A1 <= A, True)
        self.assertEqual((A & B) <= B, True)
        self.assertEqual((A & B) <= A, True)
        self.assertEqual(B1 <= (A1 | A1B1), False)  # noqa: SIM300
        self.assertEqual(B <= (A & B), False)  # noqa: SIM300
        self.assertEqual(A <= (A & B), False)  # noqa: SIM300
        self.assertEqual(A <= (A | B), True)  # noqa: SIM300

        self.assertEqual(A < B, False)
        self.assertEqual(A < A1, False)
        self.assertEqual(A1 < A, True)
        self.assertEqual(A < A1, False)
        self.assertEqual(A < A11, False)
        self.assertEqual(A < A2, False)
        self.assertEqual(A < A21, False)
        self.assertEqual(A < B1, False)
        self.assertEqual(A < B11, False)
        self.assertEqual(A < B2, False)
        self.assertEqual(A < (A | B), True)  # noqa: SIM300

    def test_groups_5_invert(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A2 = self.definitions.parse('A2')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B11 = self.definitions.parse('B11')
        B2 = self.definitions.parse('B2')
        BX = self.definitions.parse('BX')
        universe = self.definitions.universe
        empty = self.definitions.empty

        self.assertEqual(str(~A), "~'A'")
        self.assertEqual(str(~A1), "~'A1'")
        self.assertEqual(str(~B), "~'B'")
        self.assertEqual(str(~universe), "~*")
        self.assertEqual(str(~empty), "*")

        self.assertEqual(str(~(A & B)), "~'A' | ~'B'")
        self.assertEqual(str(~(A | B)), "~'A' & ~'B'")
        self.assertEqual(str(~A & ~A1), "~'A'")

        self.assertEqual(str(A | ~A), "*")
        self.assertEqual(str(~A | ~A1), "~'A1'")
        self.assertEqual(str(~(A | A1)), "~'A'")
        self.assertEqual(~(A | A1), ~A & ~A1)
        self.assertEqual(str(~(A & A1)), "~'A1'")
        self.assertEqual(~(A & A1), ~A | ~A1)
        self.assertEqual(str(~(~B1 & ~B2)), "'B1' | 'B2'")

        self.assertEqual(str(A & ~A), "~*")
        self.assertEqual(str(A & ~A1), "'A' & ~'A1'")
        self.assertEqual(str(~A & A), "~*")
        self.assertEqual(str(~A & A1), "~*")
        self.assertEqual(str(~A1 & A), "'A' & ~'A1'")
        self.assertEqual(str(B11 & ~BX), "'B11'")
        self.assertEqual(str(~B1 & BX), "'BX'")
        self.assertEqual(str(~B11 & BX), "'BX'")

        self.assertEqual(str(~((A & B1) | B2)), "(~'A' & ~'B2') | (~'B1' & ~'B2')")
        self.assertEqual(str(~(A | (B1 & B2))), "(~'A' & ~'B1') | (~'A' & ~'B2')")
        self.assertEqual(str(~(A | (B2 & B1))), "(~'A' & ~'B1') | (~'A' & ~'B2')")
        self.assertEqual(str(~((A1 & A2) | (B1 & B2))), "(~'A1' & ~'B1') | (~'A1' & ~'B2') | (~'A2' & ~'B1') | (~'A2' & ~'B2')")
        self.assertEqual(str(~A & ~B2), "~'A' & ~'B2'")
        self.assertEqual(str(~(~B1 & ~B2)), "'B1' | 'B2'")
        self.assertEqual(str(~((A & B) | A1)), "~'A' | (~'A1' & ~'B')")
        self.assertEqual(str(~(~A | (~A1 & ~B))), "('A' & 'B') | 'A1'")
        self.assertEqual(str(~~((A & B) | A1)), "('A' & 'B') | 'A1'")

    def test_groups_6_invert_gt_lt(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')

        self.assertEqual(A < A1, False)
        self.assertEqual(~A < ~A1, True)
        self.assertEqual(A > A1, True)
        self.assertEqual(~A > ~A1, False)
        self.assertEqual(~A1 > ~A, True)
        self.assertEqual(A < ~A, False)  # noqa: SIM300
        self.assertEqual(A < ~A1, False)  # noqa: SIM300
        self.assertEqual(~A < ~A, False)
        self.assertEqual(~A < ~A1, True)

    def test_groups_7_various(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        B = self.definitions.parse('B')

        self.assertEqual(str(~A & (A | B)), "~'A' & 'B'")
        self.assertEqual(str(A1 & B & ~A), "~*")
        self.assertEqual(str(A1 & ~A & B), "~*")
        self.assertEqual(str(~A1 & A & B), "'A' & ~'A1' & 'B'")

    def test_groups_8_reduce(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B2 = self.definitions.parse('B2')
        universe = self.definitions.universe
        empty = self.definitions.empty

        self.assertEqual(str((A | B) & B), "'B'")
        self.assertEqual(str((A & B) | (A & ~B)), "'A'")
        self.assertEqual(str((A & B1 & B2) | (A & B1 & ~B2)), "'A' & 'B1'")
        self.assertEqual(str((A & ~B2 & B1) | (A & B1 & B2)), "'A' & 'B1'")
        self.assertEqual(str((A & B1 & ~B2) | (A & ~B1 & B2)), "('A' & 'B1' & ~'B2') | ('A' & ~'B1' & 'B2')")
        self.assertEqual(str(((B2 & A1) | (B2 & A1 & A11)) | ((B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11))), "'A1'")
        self.assertEqual(str(~(((B2 & A1) | (B2 & A1 & A11)) | ((B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11)))), "~'A1'")
        self.assertEqual(str(~((~A & B) | (A & B) | (A & ~B))), "~'A' & ~'B'")
        self.assertEqual(str((~A & ~B2) & (B1 | B2)), "~'A' & 'B1' & ~'B2'")
        self.assertEqual(str((~A & ~B2) & ~(~B1 & ~B2)), "~'A' & 'B1' & ~'B2'")
        self.assertEqual(str(~A & ~B2 & universe), "~'A' & ~'B2'")
        self.assertEqual(str((~A & ~B2 & universe) & ~(~B1 & ~B2)), "~'A' & 'B1' & ~'B2'")
        self.assertEqual(str((~A & ~B2 & empty) & ~(~B1 & ~B2)), "~*")
        self.assertEqual(str((~A & ~B2) & ~(~B1 & ~B2 & empty)), "~'A' & ~'B2'")
        self.assertEqual(str((~A & B1 & A) & B), "~*")

    def test_groups_9_distinct(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        A1B1 = self.definitions.parse('A1B1')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B11 = self.definitions.parse('B11')
        E = self.definitions.parse('E')
        E1 = self.definitions.parse('E1')

        self.assertEqual(A <= E, False)
        self.assertEqual(A >= E, False)
        self.assertEqual(A <= ~E, True)  # noqa: SIM300
        self.assertEqual(A >= ~E, False)  # noqa: SIM300
        self.assertEqual(A11 <= ~E, True)  # noqa: SIM300
        self.assertEqual(A11 >= ~E, False)  # noqa: SIM300
        self.assertEqual(~A >= E, True)
        self.assertEqual(~A11 >= E, True)
        self.assertEqual(~A >= ~E, False)
        self.assertEqual(~A11 >= ~E, False)
        self.assertEqual(A <= E1, False)
        self.assertEqual(A >= E1, False)
        self.assertEqual(A <= ~E1, True)  # noqa: SIM300
        self.assertEqual(A >= ~E1, False)  # noqa: SIM300
        self.assertEqual(A11 <= ~E1, True)  # noqa: SIM300
        self.assertEqual(A11 >= ~E1, False)  # noqa: SIM300
        self.assertEqual(~A >= E1, True)
        self.assertEqual(~A11 >= E1, True)
        self.assertEqual(~A >= ~E1, False)
        self.assertEqual(~A <= ~E1, False)
        self.assertEqual(~A11 >= ~E1, False)

        self.assertEqual(str(B11 & ~E), "'B11'")
        self.assertEqual(str(~A11 | E), "~'A11'")
        self.assertEqual(str(~(A1 & A11 & ~E)), "~'A11'")
        self.assertEqual(str(B1 & E), "~*")
        self.assertEqual(str(B11 & E), "~*")
        self.assertEqual(str(B1 | E), "'B1' | 'E'")
        self.assertEqual(str((B1 & E) | A1B1), "'A1B1'")
        self.assertEqual(str(A1 & A11 & ~E), "'A11'")
        self.assertEqual(str(~E & (E | B)), "'B'")
        self.assertEqual(str((~E & E) | B), "'B'")

    def test_groups_10_hudge_combine(self):
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B2 = self.definitions.parse('B2')
        A1B1 = self.definitions.parse('A1B1')
        C = self.definitions.parse('C')
        D = self.definitions.parse('D')
        E = self.definitions.parse('E')

        Z1 = C | B2 | A1 | A11
        Z2 = (C) | (C & B2) | (C & B2 & A1) | (C & B2 & A11) | (C & ~B2) | (C & ~B2 & A1)
        Z3 = (C & ~B2 & A11) | (C & A1) | (C & A1 & B1) | (C & A11) | (C & A11 & B1) | (C & B1)
        Z4 = (B2 & A1) | (B2 & A1 & A11) | (B2 & A11) | (~B2 & A1) | (~B2 & A1 & A11)
        Z5 = (~B2 & A11) | (A1) | (A1 & A11) | (A1 & A11 & B1) | (A1 & B1) | (A11) | (A11 & B1)
        group1 = Z1 & (Z2 | Z3 | Z4 | Z5)

        self.assertEqual(str(group1), "'A1' | 'C'")
        self.assertEqual(str(~group1), "~'A1' & ~'C'")
        self.assertEqual(str(~~group1), "'A1' | 'C'")
        self.assertEqual(str((~group1).invert_intersect(~A1)), "~'C'")

        self.assertEqual(str(group1 & B), "('A1' & 'B') | ('B' & 'C')")
        self.assertEqual(str(~(group1 & B)), "(~'A1' & ~'C') | ~'B'")
        self.assertEqual(str(~~(group1 & B)), "('A1' & 'B') | ('B' & 'C')")
        self.assertEqual(str((group1 & B).invert_intersect(B)), "'A1' | 'C'")

        self.assertFalse((group1 & B).invert_intersect(A1))

        self.assertEqual(str(A1 & D), "~*")
        self.assertEqual(str(group1 & (C | B | D)), "('A1' & 'B') | 'C'")
        self.assertEqual(str(~(group1 & (C | B | D))), "(~'A1' & ~'C') | (~'B' & ~'C')")

        group2 = (B1 | D) & (A1B1 | (A1B1 & D) | (A1B1 & D & E) | (A1B1 & E) | E)
        self.assertEqual(str(group2), "'A1B1'")

    def test_groups_11_invert_intersect(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        A2 = self.definitions.parse('A2')
        A21 = self.definitions.parse('A21')
        A22 = self.definitions.parse('A22')
        B = self.definitions.parse('B')
        B1 = self.definitions.parse('B1')
        B2 = self.definitions.parse('B2')
        D = self.definitions.parse('D')

        self.assertEqual(str((A1 & A2).invert_intersect(A2)), "'A1'")
        self.assertEqual(str((A1 & B1 | A1 & B2).invert_intersect(A1)), "'B1' | 'B2'")
        self.assertEqual(str((A1 & B1 | A1 & B2 | A1 & A2).invert_intersect(A1)), "'A2' | 'B1' | 'B2'")
        self.assertEqual(str((A1 & B1 | A2 & B1).invert_intersect(A1 | A2)), "'B1'")
        self.assertEqual(str((A1 & B1 | A1 & B2 | A2 & B1 | A2 & B2).invert_intersect(A1 | A2)), "'B1' | 'B2'")
        self.assertEqual(A.invert_intersect(A | B), None)
        self.assertEqual(A.invert_intersect(A1 | A2), None)
        self.assertEqual(A.invert_intersect(A | D), None)

        tests = [
            (A2, A1),
            (B1 | B2, A1),
            (A2 | B1 | B2, A1),
            (B1, A1 | A2),
            (B1 | B2, A1 | A2),
            (B1 & B2, A1),
            (A2 & B1 & B2, A1),
            (B1 & B2, A1 | A2),
            (A1, B1 & B2),
            (A1 | A2, B1 & B2),
            (A1, A2 | B1 & B2),
            (A11 | A21, A22 | B1 & B2),
            (A11 & A21, A22 | B1 & B2),
            (A, A1 | B),
            (A1 | B, A),
        ]
        for a, b in tests:
            self.assertEqual(str((a & b).invert_intersect(b)), str(a), f'Should invert_intersect: {a & b}\nby: ({b})')

    def test_groups_matches(self):
        A = self.definitions.parse('A')
        A1 = self.definitions.parse('A1')
        A11 = self.definitions.parse('A11')
        B = self.definitions.parse('B')
        C = self.definitions.parse('C')
        D = self.definitions.parse('D')

        matching = [
            (A, {1, 13}),
            (A, {1, 2, 3, 13}),
            (A1, {1, 2, 13}),
            (A11, {1, 2, 3, 13}),
            (A | B, {1, 13}),
            (B | C, {1, 13}),
            (A1 | B, {1, 2, 13}),
            (A11 | B, {1, 2, 3, 13}),
            ((A11 | B) & ~D, {1, 2, 3, 13}),
            (A & ~A11, {1, 13}),
            (A & ~A11, {1, 2, 13}),
        ]
        for spec, group_ids in matching:
            self.assertTrue(
                spec.matches(group_ids),
                f"user with groups {self.definitions.from_ids(group_ids, keep_subsets=True)} should match {spec}",
            )

        non_matching = [
            (A, {13}),
            (A1, {13}),
            (A11, {13}),
            (A | B, {13}),
            (A & ~C, {13}),
            (A & ~B & ~C, {13}),
            ((A11 | B) & ~C, {1, 2, 3, 13}),
            (A & ~A11, {1, 2, 3, 13}),
        ]
        for spec, group_ids in non_matching:
            self.assertFalse(
                spec.matches(group_ids),
                f"user with groups {self.definitions.from_ids(group_ids, keep_subsets=True)} should not match {spec}",
            )

    def test_groups_unknown(self):
        A = self.definitions.parse('A')
        U1 = self.definitions.parse('unknown.group1', raise_if_not_found=False)
        U2 = self.definitions.parse('unknown.group2', raise_if_not_found=False)

        self.assertEqual(U1, U1)
        self.assertNotEqual(U1, U2)

        self.assertEqual(A | U1, U1 | A)
        self.assertEqual(U1 | U2, U2 | U1)
        self.assertEqual(A & U1, U1 & A)
        self.assertEqual(U1 & U2, U2 & U1)

        self.assertEqual(A | U1 | U2, A | U1 | U2)
        self.assertEqual(A | U2 | U1, A | U1 | U2)
        self.assertEqual(U1 | A | U2, A | U1 | U2)
        self.assertEqual(U1 | A | U2, A | U1 | U2)
        self.assertEqual(U2 | A | U1, A | U1 | U2)
        self.assertEqual(U2 | U1 | A, A | U1 | U2)

        self.assertEqual(A & U1 & U2, A & U1 & U2)
        self.assertEqual(A & U2 & U1, A & U1 & U2)
        self.assertEqual(U1 & A & U2, A & U1 & U2)
        self.assertEqual(U1 & A & U2, A & U1 & U2)
        self.assertEqual(U2 & A & U1, A & U1 & U2)
        self.assertEqual(U2 & U1 & A, A & U1 & U2)

    def test_groups_key(self):
        A = self.definitions.parse('A')
        B = self.definitions.parse('B')
        C = self.definitions.parse('C')
        U = self.definitions.parse('unknown.group', raise_if_not_found=False)

        test_cases = [
            A,
            A | B,
            A & B,
            A & ~B,
            (A | B) & ~C,
            U,
            A | U | B,
        ]

        for groups in test_cases:
            self.assertIsInstance(groups.key, str)
            groups1 = self.definitions.from_key(groups.key)
            self.assertEqual(groups1, groups)
            self.assertEqual(groups1.key, groups.key)


@common.tagged('at_install', 'groups')
class TestGroupsOdoo(common.TransactionCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.test_group = cls.env['res.groups'].create({
            'name': 'test with implied user',
            'implied_ids': [Command.link(cls.env.ref('base.group_user').id)]
        })
        cls.env["ir.model.data"].create({
            "module": "base",
            "name": "base_test_group",
            "model": "res.groups",
            "res_id": cls.test_group.id,
        })
        cls.definitions = cls.env['res.groups']._get_group_definitions()

    def parse_repr(self, group_repr):
        """ Return the group object from the string (given by the repr of the group object).

        :param group_repr: str
            Use | (union) and & (intersection) separator like the python object.
                intersection it's apply before union.
                Can use an invertion with ~.
        """
        if not group_repr:
            return self.definitions.universe
        res = None
        for union in group_repr.split('|'):
            union = union.strip()
            intersection = None
            if union.startswith('(') and union.endswith(')'):
                union = union[1:-1]
            for xmlid in union.split('&'):
                xmlid = xmlid.strip()
                leaf = ~self.definitions.parse(xmlid[1:]) if xmlid.startswith('~') else self.definitions.parse(xmlid)
                if intersection is None:
                    intersection = leaf
                else:
                    intersection &= leaf
            if intersection is None:
                return self.definitions.universe
            elif res is None:
                res = intersection
            else:
                res |= intersection
        return self.definitions.empty if res is None else res

    def test_groups_1_base(self):
        parse = self.definitions.parse

        self.assertEqual(str(parse('base.group_user') & parse('base.group_user')), "'base.group_user'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_system') & parse('base.group_user')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_erp_manager') & parse('base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_system') & parse('base.group_allow_export')), "'base.group_system' & 'base.group_allow_export'")
        self.assertEqual(str(parse('base.group_user') | parse('base.group_user')), "'base.group_user'")
        self.assertEqual(str(parse('base.group_user') | parse('base.group_system')), "'base.group_user'")
        self.assertEqual(str(parse('base.group_system') | parse('base.group_public')), "'base.group_system' | 'base.group_public'")
        self.assertEqual(parse('base.group_system') < parse('base.group_erp_manager'), True)
        self.assertEqual(parse('base.group_system') < parse('base.group_sanitize_override'), True)
        self.assertEqual(parse('base.group_erp_manager') < parse('base.group_user'), True)
        self.assertEqual(parse('!base.group_portal') < parse('!base.group_public'), False)
        self.assertEqual(parse('base.base_test_group') == parse('base.base_test_group'), True)
        self.assertEqual(parse('base.group_system') <= parse('base.group_system'), True)
        self.assertEqual(parse('base.group_public') <= parse('base.group_system'), False)  # None ?
        self.assertEqual(parse('base.group_user') <= parse('base.group_system'), False)
        self.assertEqual(parse('base.group_system') <= parse('base.group_user'), True)
        self.assertEqual(parse('base.group_user') <= parse('base.group_portal'), False)
        self.assertEqual(parse('!base.group_portal') <= parse('!base.group_public'), False)

    def test_groups_2_from_commat_separator(self):
        parse = self.definitions.parse

        self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user,base.group_erp_manager') & parse('base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user,base.group_portal') & parse('base.group_portal')), "'base.group_portal'")
        self.assertEqual(str(parse('base.group_user,base.group_portal,base.group_public,base.group_multi_company') & parse('base.group_portal,base.group_public')), "'base.group_portal' | 'base.group_public'")
        self.assertEqual(str(parse('base.group_system,base.base_test_group') & parse('base.group_user')), "'base.group_system' | 'base.base_test_group'")
        self.assertEqual(str(parse('base.group_system,base.group_portal') & parse('base.group_user')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user') & parse('!base.group_portal,base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('!base.group_portal') & parse('base.group_portal,base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_portal,!base.group_user') & parse('base.group_user')), "~*")
        self.assertEqual(str(parse('!base.group_user') & parse('base.group_portal,base.group_user')), "'base.group_portal'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,!base.group_user')), "~*")
        self.assertEqual(str(parse('!base.group_user') & parse('base.group_portal,!base.group_system')), "'base.group_portal'")
        self.assertEqual(str(parse('!base.group_user,base.group_allow_export') & parse('base.group_allow_export,!base.group_system')), "~'base.group_user' & 'base.group_allow_export'")
        self.assertEqual(str(parse('!base.group_user,base.group_portal') & parse('base.group_portal,!base.group_system')), "'base.group_portal'")
        self.assertEqual(str(parse('!*') & parse('base.group_portal')), "~*")
        self.assertEqual(str(parse('*') & parse('base.group_portal')), "'base.group_portal'")
        self.assertEqual(str(parse('base.group_user,!base.group_system') & parse('base.group_erp_manager,base.group_portal')), "'base.group_erp_manager' & ~'base.group_system'")
        self.assertEqual(str(parse('base.group_user,!base.group_system') & parse('base.group_portal,base.group_erp_manager')), "'base.group_erp_manager' & ~'base.group_system'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_erp_manager,!base.group_system')), "'base.group_erp_manager' & ~'base.group_system'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_portal,base.group_system')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,base.group_erp_manager')), "'base.group_erp_manager'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_portal,!base.group_system')), "~*")
        self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_system,base.group_portal')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user') & parse('base.group_system,base.group_portal')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_user,base.group_system') & parse('base.group_allow_export')), "'base.group_user' & 'base.group_allow_export'")
        self.assertEqual(str(parse('base.group_user,base.group_erp_manager') | parse('base.group_system')), "'base.group_user'")
        self.assertEqual(str(parse('base.group_user') | parse('base.group_portal,base.group_system')), "'base.group_user' | 'base.group_portal'")
        self.assertEqual(str(parse('!*') | parse('base.group_user')), "'base.group_user'")
        self.assertEqual(str(parse('base.group_user') | parse('!*')), "'base.group_user'")
        self.assertEqual(str(parse('!*') | parse('base.group_user,base.group_portal')), "'base.group_user' | 'base.group_portal'")
        self.assertEqual(str(parse('*') | parse('base.group_user')), "*")
        self.assertEqual(str(parse('base.group_user') | parse('*')), "*")
        self.assertEqual(str(parse('base.group_user,base.group_erp_manager') | parse('base.group_system,base.group_public')), "'base.group_user' | 'base.group_public'")
        self.assertEqual(parse('base.group_system') < parse('base.group_erp_manager,base.group_sanitize_override'), True)
        self.assertEqual(parse('!base.group_public,!base.group_portal') < parse('!base.group_public'), True)
        self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.group_system,base.base_test_group'), True)
        self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group,base.group_system'), True)
        self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group,base.group_public'), False)
        self.assertEqual(parse('base.group_system,base.base_test_group') == parse('base.base_test_group'), False)
        self.assertEqual(parse('base.group_user') <= parse('base.group_system,base.group_public'), False)
        self.assertEqual(parse('base.group_system') <= parse('base.group_user,base.group_public'), True)
        self.assertEqual(parse('base.group_public') <= parse('base.group_system,base.group_public'), True)
        self.assertEqual(parse('base.group_system,base.group_public') <= parse('base.group_system,base.group_public'), True)
        self.assertEqual(parse('base.group_system,base.group_public') <= parse('base.group_user,base.group_public'), True)
        self.assertEqual(parse('base.group_system,!base.group_public') <= parse('base.group_system'), True)
        self.assertEqual(parse('base.group_system,!base.group_allow_export') <= parse('base.group_system'), True)
        self.assertEqual(parse('base.group_system') <= parse('base.group_system,!base.group_allow_export'), False)
        self.assertEqual(parse('base.group_system') <= parse('base.group_system,!base.group_public'), True)
        self.assertEqual(parse('base.group_system') == parse('base.group_system,!base.group_public'), True)
        self.assertEqual(parse('!base.group_public,!base.group_portal') <= parse('!base.group_public'), True)
        self.assertEqual(parse('base.group_user,!base.group_allow_export') <= parse('base.group_user,!base.group_system,!base.group_allow_export'), False)
        self.assertEqual(parse('base.group_system,!base.group_portal,!base.group_public') <= parse('base.group_system,!base.group_public'), True)

    def test_groups_3_from_ref(self):
        parse = self.parse_repr

        self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('base.group_public')), "~*")
        self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('~base.group_user')), "~*")
        self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & ~base.group_system') & parse('~base.group_user & base.group_portal')), "~*")
        self.assertEqual(str(parse('base.group_user & base.group_portal | base.group_user & base.group_system') & parse('base.group_user & ~base.group_portal')), "'base.group_system'")
        self.assertEqual(str(parse('base.group_public & base.group_erp_manager | base.group_public & base.group_portal') & parse('*')), "~*")
        self.assertEqual(str(parse('base.group_system & base.group_allow_export') & parse('base.group_portal | base.group_system')), "'base.group_system' & 'base.group_allow_export'")
        self.assertEqual(str(parse('base.group_portal & base.group_erp_manager') | parse('base.group_erp_manager')), "'base.group_erp_manager'")
        self.assertEqual(parse('base.group_system & base.group_allow_export') < parse('base.group_system'), True)
        self.assertEqual(parse('base.base_test_group') == parse('base.base_test_group & base.group_user'), True)
        self.assertEqual(parse('base.group_system | base.base_test_group') == parse('base.group_system & base.group_user | base.base_test_group & base.group_user'), True)
        self.assertEqual(parse('base.group_public & base.group_allow_export') <= parse('base.group_public'), True)
        self.assertEqual(parse('base.group_public') <= parse('base.group_public & base.group_allow_export'), False)
        self.assertEqual(parse('base.group_public & base.group_user') <= parse('base.group_portal'), True)
        self.assertEqual(parse('base.group_public & base.group_user') <= parse('base.group_public | base.group_user'), True)
        self.assertEqual(parse('base.group_public & base.group_system') <= parse('base.group_user'), True)
        self.assertEqual(parse('base.group_public & base.group_system') <= parse('base.group_portal | base.group_user'), True)
        self.assertEqual(parse('base.group_public & base.group_allow_export') <= parse('~base.group_public'), False)
        self.assertEqual(parse('base.group_portal & base.group_public | base.group_system & base.group_public') <= parse('base.group_public'), True)
        self.assertEqual(parse('base.group_portal & base.group_user | base.group_system & base.group_user') <= parse('base.group_user'), True)
        self.assertEqual(parse('base.group_portal & base.group_system | base.group_user & base.group_system') <= parse('base.group_system'), True)
        self.assertEqual(parse('base.group_portal & base.group_user | base.group_user & base.group_user') <= parse('base.group_user'), True)
        self.assertEqual(parse('base.group_portal & base.group_user | base.group_user & base.group_user') <= parse('base.group_user'), True)
        self.assertEqual(parse('base.group_public') <= parse('base.group_portal & base.group_public | base.group_system & base.group_public'), False)
        self.assertEqual(parse('base.group_user & base.group_allow_export') <= parse('base.group_user & base.group_system & base.group_allow_export'), False)
        self.assertEqual(parse('base.group_system & base.group_allow_export') <= parse('base.group_user & base.group_system & base.group_allow_export'), True)
        self.assertEqual(parse('base.group_system & base.group_allow_export') <= parse('base.group_system'), True)
        self.assertEqual(parse('base.group_public') >= parse('base.group_portal & base.group_public | base.group_system & base.group_public'), True)
        self.assertEqual(parse('base.group_user & base.group_public') >= parse('base.group_user & base.group_portal & base.group_public | base.group_user & base.group_system & base.group_public'), True)
        self.assertEqual(parse('base.group_system & base.group_allow_export') >= parse('base.group_system'), False)
        self.assertEqual(parse('base.group_system & base.group_allow_export') > parse('base.group_system'), False)

    def test_groups_4_full_empty(self):
        user_group_ids = self.env.user._get_group_ids()
        self.assertFalse(self.definitions.parse('base.group_public').matches(user_group_ids))
        self.assertTrue(self.definitions.parse('*').matches(user_group_ids))
        self.assertFalse((~self.definitions.parse('*')).matches(user_group_ids))

    def test_groups_5_contains_user(self):
        # user is included into the defined group of users

        user = self.env['res.users'].create({
            'name': 'A User',
            'login': 'a_user',
            'email': 'a@user.com',
        })

        tests = [
            # group on the user, # groups access, access
            ('base.group_public', 'base.group_system | base.group_public', True),
            ('base.group_public,base.group_allow_export', 'base.group_user | base.group_public', True),
            ('base.group_public', 'base.group_system & base.group_public', False),
            ('base.group_public', 'base.group_system | base.group_portal', False),
            ('base.group_public', 'base.group_system & base.group_portal', False),
            ('base.group_system', 'base.group_system | base.group_public', True),
            ('base.group_system', 'base.group_system & base.group_public', False),
            ('base.group_system', 'base.group_user | base.group_system', True),
            ('base.group_system', 'base.group_user & base.group_system', True),
            ('base.group_public', 'base.group_user | base.group_system', False),
            ('base.group_public', 'base.group_user & base.group_system', False),
            ('base.group_system', 'base.group_system & ~base.group_user', False),
            ('base.group_portal', 'base.group_system & ~base.group_user', False),
            ('base.group_user', 'base.group_user & ~base.group_system', True),
            ('base.group_user', '~base.group_system & base.group_user', True),
            ('base.group_system', 'base.group_user & ~base.group_system', False),
            ('base.group_portal', 'base.group_portal & ~base.group_user', True),
            ('base.group_system', '~base.group_system & base.group_user', False),
            ('base.group_system', '~base.group_system & ~base.group_user', False),
            ('base.group_user', 'base.group_user & base.group_sanitize_override & base.group_allow_export', False),
            ('base.group_system', 'base.group_user & base.group_sanitize_override & base.group_allow_export', False),
            ('base.group_system,base.group_allow_export', 'base.group_user & base.group_sanitize_override & base.group_allow_export', True),
            ('base.group_user,base.group_sanitize_override,base.group_allow_export', 'base.group_user & base.group_sanitize_override & base.group_allow_export', True),
            ('base.group_user', 'base.group_erp_manager | base.group_multi_company', False),
            ('base.group_user,base.group_erp_manager', 'base.group_erp_manager | base.group_multi_company', True),
        ]
        for user_groups, groups, result in tests:
            user.groups_id = [(6, 0, [self.env.ref(xmlid).id for xmlid in user_groups.split(',')])]
            self.assertEqual(self.parse_repr(groups).matches(user._get_group_ids()), result, f'User ({user_groups!r}) should {"" if result else "not "}have access to groups: ({groups!r})')

    def test_groups_6_distinct(self):
        user = self.env['res.users'].create({
            'name': 'A User',
            'login': 'a_user',
            'email': 'a@user.com',
            'groups_id': self.env.ref('base.group_user').ids,
        })
        with self.assertRaisesRegex(ValidationError, "The user cannot have more than one user types."):
            user.groups_id = [(4, self.env.ref('base.group_public').id)]
        with self.assertRaisesRegex(ValidationError, "The user cannot have more than one user types."):
            user.groups_id = [(4, self.env.ref('base.group_portal').id)]
