"""
Test cases for ldaptor.inmemory module.
"""
from io import BytesIO

from twisted.trial import unittest

from ldaptor import inmemory, delta, testutil
from ldaptor.protocols.ldap import distinguishedname, ldaperrors


class SubclassEntry(inmemory.ReadOnlyInMemoryLDAPEntry):
    pass


class TestInMemoryDatabase(unittest.TestCase):
    def setUp(self):
        self.root = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        self.meta = self.root.addChild(
            rdn="ou=metasyntactic",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["metasyntactic"],
            },
        )
        self.foo = self.meta.addChild(
            rdn="cn=foo",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["foo"],
            },
        )
        self.bar = self.meta.addChild(
            rdn="cn=bar",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["bar"],
            },
        )

        self.empty = self.root.addChild(
            rdn="ou=empty",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["empty"],
            },
        )

        self.oneChild = self.root.addChild(
            rdn="ou=oneChild",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["oneChild"],
            },
        )
        self.theChild = self.oneChild.addChild(
            rdn="cn=theChild",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["theChild"],
            },
        )

    def test_children_empty(self):
        d = self.empty.children()
        d.addCallback(lambda actual: self.assertCountEqual(actual, []))
        return d

    def test_children_oneChild(self):
        d = self.oneChild.children()

        def cb(children):
            self.assertEqual(len(children), 1)
            got = [e.dn for e in children]
            want = [
                distinguishedname.DistinguishedName(
                    "cn=theChild,ou=oneChild,dc=example,dc=com"
                )
            ]
            got.sort()
            want.sort()
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_children_repeat(self):
        """Test that .children() returns a copy of the data so that modifying it does not affect behaviour."""
        d = self.oneChild.children()

        def cb1(children1):
            self.assertEqual(len(children1), 1)

            children1.pop()

            d = self.oneChild.children()
            return d

        d.addCallback(cb1)

        def cb2(children2):
            self.assertEqual(len(children2), 1)

        d.addCallback(cb2)
        return d

    def test_children_twoChildren(self):
        d = self.meta.children()

        def cb(children):
            self.assertEqual(len(children), 2)
            want = [
                distinguishedname.DistinguishedName(
                    "cn=foo,ou=metasyntactic,dc=example,dc=com"
                ),
                distinguishedname.DistinguishedName(
                    "cn=bar,ou=metasyntactic,dc=example,dc=com"
                ),
            ]
            got = [e.dn for e in children]
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_addChild(self):
        self.empty.addChild(
            rdn="a=b",
            attributes={
                "objectClass": ["a", "b"],
                "a": "b",
            },
        )
        d = self.empty.children()

        def cb(children):
            self.assertEqual(len(children), 1)
            got = [e.dn for e in children]
            want = [
                distinguishedname.DistinguishedName("a=b,ou=empty,dc=example,dc=com"),
            ]
            got.sort()
            want.sort()
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_addChild_Exists(self):
        self.assertRaises(
            ldaperrors.LDAPEntryAlreadyExists,
            self.meta.addChild,
            rdn="cn=foo",
            attributes={
                "objectClass": ["a"],
                "cn": "foo",
            },
        )

    def test_addChild_subclass(self):
        """
        Adding child to ReadOnlyInMemoryLDAPEntry subclass instance
        creates entry of the same class
        """
        entry = SubclassEntry(dn="dc=example,dc=com")
        child = entry.addChild(rdn="ou=empty", attributes={"objectClass": ["a"]})
        self.assertIsInstance(child, SubclassEntry)

    def test_parent(self):
        self.assertEqual(self.foo.parent(), self.meta)
        self.assertEqual(self.meta.parent(), self.root)
        self.assertEqual(self.root.parent(), None)

    def test_subtree_empty(self):
        d = self.empty.subtree()

        def cb(entries):
            self.assertEqual(len(entries), 1)

        d.addCallback(cb)
        return d

    def test_subtree_oneChild(self):
        d = self.oneChild.subtree()
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.oneChild,
                    self.theChild,
                ],
            )
        )
        return d

    def test_subtree_oneChild_cb(self):
        got = []
        d = self.oneChild.subtree(got.append)
        d.addCallback(self.assertEqual, None)

        def cb(dummy):
            want = [
                self.oneChild,
                self.theChild,
            ]
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_subtree_many(self):
        d = self.root.subtree()

        def cb(results):
            got = results
            want = [
                self.root,
                self.oneChild,
                self.theChild,
                self.empty,
                self.meta,
                self.bar,
                self.foo,
            ]
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_subtree_many_cb(self):
        got = []
        d = self.root.subtree(callback=got.append)

        def cb(r):
            self.assertEqual(r, None)

            want = [
                self.root,
                self.oneChild,
                self.theChild,
                self.empty,
                self.meta,
                self.bar,
                self.foo,
            ]
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def test_lookup_fail(self):
        dn = distinguishedname.DistinguishedName(
            "cn=thud,ou=metasyntactic,dc=example,dc=com"
        )
        d = self.root.lookup(dn)

        def eb(fail):
            fail.trap(ldaperrors.LDAPNoSuchObject)
            self.assertEqual(fail.value.message, dn)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def test_lookup_fail_outOfTree(self):
        dn = distinguishedname.DistinguishedName("dc=invalid")
        d = self.root.lookup(dn)

        def eb(fail):
            fail.trap(ldaperrors.LDAPNoSuchObject)
            self.assertEqual(fail.value.message, dn)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def test_lookup_deep_dn(self):
        """Entry lookup with a DN instance returns entry instance with this DN"""
        dn = distinguishedname.DistinguishedName(
            "cn=bar,ou=metasyntactic,dc=example,dc=com"
        )
        d = self.root.lookup(dn)
        d.addCallback(self.assertEqual, self.bar)
        return d

    def test_lookup_deep_str(self):
        """Entry lookup with a DN as a string returns entry instance with this DN"""
        d = self.root.lookup("cn=bar,ou=metasyntactic,dc=example,dc=com")
        d.addCallback(self.assertEqual, self.bar)
        return d

    def test_delete_root(self):
        newRoot = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        d = newRoot.delete()

        def eb(fail):
            fail.trap(inmemory.LDAPCannotRemoveRootError)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def test_delete_nonLeaf(self):
        d = self.meta.delete()

        def eb(fail):
            fail.trap(ldaperrors.LDAPNotAllowedOnNonLeaf)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def test_delete(self):
        d = self.foo.delete()
        d.addCallback(self.assertEqual, self.foo)
        d.addCallback(lambda _: self.meta.children())
        d.addCallback(lambda actual: self.assertCountEqual(actual, [self.bar]))
        return d

    def test_deleteChild(self):
        d = self.meta.deleteChild("cn=bar")
        d.addCallback(self.assertEqual, self.bar)
        d.addCallback(lambda _: self.meta.children())
        d.addCallback(lambda actual: self.assertCountEqual(actual, [self.foo]))
        return d

    def test_deleteChild_NonExisting(self):
        d = self.root.deleteChild("cn=not-exist")

        def eb(fail):
            fail.trap(ldaperrors.LDAPNoSuchObject)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def test_setPassword(self):
        self.foo.setPassword(b"s3krit", salt=b"\xf2\x4a")
        self.assertEqual(
            self.foo["userPassword"], [b"{SSHA}0n/Iw1NhUOKyaI9gm9v5YsO3ZInySg=="]
        )

    def test_setPassword_noSalt(self):
        self.foo.setPassword(b"s3krit")
        self.failUnless("userPassword" in self.foo)
        d = self.foo.bind(b"s3krit")
        d.addCallback(self.assertIdentical, self.foo)
        d.addCallback(lambda _: self.foo.bind(b"s4krit"))

        def eb(fail):
            fail.trap(ldaperrors.LDAPInvalidCredentials)

        d.addCallbacks(testutil.mustRaise, eb)
        return d

    def testSearch_withCallback(self):
        got = []
        d = self.root.search(filterText="(|(cn=foo)(cn=bar))", callback=got.append)

        def cb(r):
            self.assertEqual(r, None)

            want = [
                self.bar,
                self.foo,
            ]
            self.assertCountEqual(got, want)

        d.addCallback(cb)
        return d

    def testSearch_withoutCallback(self):
        d = self.root.search(filterText="(|(cn=foo)(cn=bar))")
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.bar,
                    self.foo,
                ],
            )
        )
        return d

    def test_move_noChildren_sameSuperior(self):
        d = self.empty.move("ou=moved,dc=example,dc=com")

        def getChildren(dummy):
            return self.root.children()

        d.addCallback(getChildren)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.meta,
                    inmemory.ReadOnlyInMemoryLDAPEntry(
                        dn="ou=moved,dc=example,dc=com",
                        attributes={
                            "objectClass": ["a", "b"],
                            "ou": ["moved"],
                        },
                    ),
                    self.oneChild,
                ],
            )
        )
        return d

    def test_move_children_sameSuperior(self):
        d = self.meta.move("ou=moved,dc=example,dc=com")

        def getChildren(dummy):
            return self.root.children()

        d.addCallback(getChildren)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    inmemory.ReadOnlyInMemoryLDAPEntry(
                        dn="ou=moved,dc=example,dc=com",
                        attributes={
                            "objectClass": ["a", "b"],
                            "ou": ["moved"],
                        },
                    ),
                    self.empty,
                    self.oneChild,
                ],
            )
        )
        return d

    def test_move_noChildren_newSuperior(self):
        d = self.empty.move("ou=moved,ou=oneChild,dc=example,dc=com")

        def getChildren(dummy):
            return self.root.children()

        d.addCallback(getChildren)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.meta,
                    self.oneChild,
                ],
            )
        )

        def getChildren2(dummy):
            return self.oneChild.children()

        d.addCallback(getChildren2)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.theChild,
                    inmemory.ReadOnlyInMemoryLDAPEntry(
                        dn="ou=moved,ou=oneChild,dc=example,dc=com",
                        attributes={
                            "objectClass": ["a", "b"],
                            "ou": ["moved"],
                        },
                    ),
                ],
            )
        )
        return d

    def test_move_children_newSuperior(self):
        d = self.meta.move("ou=moved,ou=oneChild,dc=example,dc=com")

        def getChildren(dummy):
            return self.root.children()

        d.addCallback(getChildren)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.empty,
                    self.oneChild,
                ],
            )
        )

        def getChildren2(dummy):
            return self.oneChild.children()

        d.addCallback(getChildren2)
        d.addCallback(
            lambda actual: self.assertCountEqual(
                actual,
                [
                    self.theChild,
                    inmemory.ReadOnlyInMemoryLDAPEntry(
                        dn="ou=moved,ou=oneChild,dc=example,dc=com",
                        attributes={
                            "objectClass": ["a", "b"],
                            "ou": ["moved"],
                        },
                    ),
                ],
            )
        )
        return d

    def test_commit(self):
        """ReadOnlyInMemoryLDAPEntry.commit() succeeds immediately."""
        self.meta["foo"] = ["bar"]
        d = self.meta.commit()
        self.failUnless(d.called)


class FromLDIF(unittest.TestCase):
    def test_single(self):
        ldif = BytesIO(
            b"""\
dn: cn=foo,dc=example,dc=com
objectClass: a
objectClass: b
aValue: a
aValue: b
bValue: c

"""
        )
        d = inmemory.fromLDIFFile(ldif)

        def cb1(db):
            self.assertEqual(
                db.dn, distinguishedname.DistinguishedName("cn=foo,dc=example,dc=com")
            )
            return db.children()

        d.addCallback(cb1)
        d.addCallback(self.assertEqual, [])
        return d

    def test_two(self):
        ldif = BytesIO(
            b"""\
dn: dc=example,dc=com
objectClass: dcObject
dc: example

dn: cn=foo,dc=example,dc=com
objectClass: a
cn: foo

"""
        )
        d = inmemory.fromLDIFFile(ldif)

        def cb1(db):
            self.assertEqual(
                db.dn, distinguishedname.DistinguishedName("dc=example,dc=com")
            )
            return db.subtree()

        d.addCallback(cb1)

        def cb2(children):
            self.assertEqual(len(children), 2)
            want = [
                distinguishedname.DistinguishedName("dc=example,dc=com"),
                distinguishedname.DistinguishedName("cn=foo,dc=example,dc=com"),
            ]
            got = [e.dn for e in children]
            self.assertCountEqual(got, want)

        d.addCallback(cb2)
        return d

    def test_missingNode(self):
        ldif = BytesIO(
            b"""\
dn: dc=example,dc=com
objectClass: dcObject
dc: example

dn: cn=foo,ou=nonexisting,dc=example,dc=com
objectClass: a
cn: foo

"""
        )
        d = inmemory.fromLDIFFile(ldif)

        def eb(fail):
            fail.trap(ldaperrors.LDAPNoSuchObject)
            self.failUnlessEqual(
                fail.value.toWire(), b"noSuchObject: ou=nonexisting,dc=example,dc=com"
            )

        d.addCallbacks(testutil.mustRaise, eb)
        return d


class TestDiff(unittest.TestCase):
    def testNoChange(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
            },
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
            },
        )
        d = a.diffTree(b)
        d.addCallback(self.assertEqual, [])
        return d

    def testRootChange_Add(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
            },
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
                "foo": ["bar"],
            },
        )
        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.ModifyOp(
                    "dc=example,dc=com",
                    [
                        delta.Add("foo", ["bar"]),
                    ],
                ),
            ],
        )
        return d

    def testChildChange_Add(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
            },
        )
        a.addChild(
            "cn=foo",
            {
                "cn": ["foo"],
            },
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            "dc=example,dc=com",
            {
                "dc": ["example"],
            },
        )
        b.addChild(
            "cn=foo",
            {
                "cn": ["foo"],
                "foo": ["bar"],
            },
        )
        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.ModifyOp(
                    "cn=foo,dc=example,dc=com",
                    [
                        delta.Add("foo", ["bar"]),
                    ],
                ),
            ],
        )
        return d

    def testAddChild(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )

        foo = b.addChild(
            rdn="cn=foo",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["foo"],
            },
        )
        bar = b.addChild(
            rdn="cn=bar",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["bar"],
            },
        )

        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.AddOp(bar),
                delta.AddOp(foo),
            ],
        )
        return d

    def testAddSubtree(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )

        foo = b.addChild(
            rdn="ou=foo",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["foo"],
            },
        )
        baz = foo.addChild(
            rdn="cn=baz",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["baz"],
            },
        )
        bar = b.addChild(
            rdn="cn=bar",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["bar"],
            },
        )

        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.AddOp(bar),
                delta.AddOp(foo),
                delta.AddOp(baz),
            ],
        )
        return d

    def testDeleteChild(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )

        foo = a.addChild(
            rdn="cn=foo",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["foo"],
            },
        )
        bar = a.addChild(
            rdn="cn=bar",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["bar"],
            },
        )

        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.DeleteOp(bar),
                delta.DeleteOp(foo),
            ],
        )
        return d

    def testDeleteSubtree(self):
        a = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )
        b = inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )

        foo = a.addChild(
            rdn="ou=foo",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["foo"],
            },
        )
        baz = foo.addChild(
            rdn="cn=baz",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["baz"],
            },
        )
        bar = a.addChild(
            rdn="cn=bar",
            attributes={
                "objectClass": ["a", "b"],
                "cn": ["bar"],
            },
        )

        d = a.diffTree(b)
        d.addCallback(
            self.assertEqual,
            [
                delta.DeleteOp(bar),
                delta.DeleteOp(baz),
                delta.DeleteOp(foo),
            ],
        )
        return d
