"""
Test cases for ldaptor.protocols.ldap.delta
"""

from twisted.trial import unittest
from ldaptor import delta, entry, attributeset, inmemory
from ldaptor.protocols.ldap import ldapsyntax, distinguishedname, ldaperrors


class TestModifications(unittest.TestCase):
    def setUp(self):
        self.foo = ldapsyntax.LDAPEntry(
            None,
            dn="cn=foo,dc=example,dc=com",
            attributes={
                "objectClass": ["person"],
                "cn": ["foo", "thud"],
                "sn": ["bar"],
                "more": ["junk"],
            },
        )

    def testAddOld(self):
        mod = delta.Add("cn", ["quux"])
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["cn"], ["foo", "thud", "quux"])

    def testAddNew(self):
        mod = delta.Add("stuff", ["val1", "val2"])
        mod.patch(self.foo)

        self.failUnlessEqual(self.foo["stuff"], ["val1", "val2"])
        self.failUnlessEqual(self.foo["cn"], ["foo", "thud"])

    def testDelete(self):
        mod = delta.Delete("cn", ["thud"])
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["cn"], ["foo"])

    def testDeleteAll(self):
        mod = delta.Delete("more")
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["cn"], ["foo", "thud"])

    def testDelete_FailOnNonExistingAttributeType_All(self):
        mod = delta.Delete("notexist", [])
        self.assertRaises(KeyError, mod.patch, self.foo)

    def testDelete_FailOnNonExistingAttributeType_OneValue(self):
        mod = delta.Delete("notexist", ["a"])
        self.assertRaises(KeyError, mod.patch, self.foo)

    def testDelete_FailOnNonExistingAttributeValue(self):
        mod = delta.Delete("cn", ["notexist"])
        self.assertRaises(LookupError, mod.patch, self.foo)

    def testReplace_Add(self):
        mod = delta.Replace("stuff", ["val1", "val2"])
        mod.patch(self.foo)

        self.failUnlessEqual(self.foo["stuff"], ["val1", "val2"])
        self.failUnlessEqual(self.foo["sn"], ["bar"])
        self.failUnlessEqual(self.foo["more"], ["junk"])

    def testReplace_Modify(self):
        mod = delta.Replace("sn", ["baz"])
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["sn"], ["baz"])
        self.failUnlessEqual(self.foo["more"], ["junk"])

    def testReplace_Delete_Existing(self):
        mod = delta.Replace("more", [])
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["sn"], ["bar"])
        self.failIf("more" in self.foo)

    def testReplace_Delete_NonExisting(self):
        mod = delta.Replace("nonExisting", [])
        mod.patch(self.foo)

        self.failIf("stuff" in self.foo)
        self.failUnlessEqual(self.foo["sn"], ["bar"])
        self.failUnlessEqual(self.foo["more"], ["junk"])


class TestModificationOpLDIF(unittest.TestCase):
    def testAdd(self):
        m = delta.Add("foo", ["bar", "baz"])
        self.assertEqual(
            m.asLDIF(),
            b"""\
add: foo
foo: bar
foo: baz
-
""",
        )

    def testDelete(self):
        m = delta.Delete("foo", ["bar", "baz"])
        self.assertEqual(
            m.asLDIF(),
            b"""\
delete: foo
foo: bar
foo: baz
-
""",
        )

    def testDeleteAll(self):
        m = delta.Delete("foo")
        self.assertEqual(
            m.asLDIF(),
            b"""\
delete: foo
-
""",
        )

    def testReplace(self):
        m = delta.Replace("foo", ["bar", "baz"])
        self.assertEqual(
            m.asLDIF(),
            b"""\
replace: foo
foo: bar
foo: baz
-
""",
        )

    def testReplaceAll(self):
        m = delta.Replace("thud")
        self.assertEqual(
            m.asLDIF(),
            b"""\
replace: thud
-
""",
        )

    def testAddBase64(self):
        """
        LDIF attribute representation is base64 encoded
        if attribute value contains nonprintable characters
        or starts with reserved characters
        """
        m = delta.Add("attr", [":value1", "value\n\r2"])
        self.assertEqual(
            m.asLDIF(),
            b"""\
add: attr
attr:: OnZhbHVlMQ==
attr:: dmFsdWUKDTI=
-
""",
        )


class OperationTestCase(unittest.TestCase):
    """
    Test case for operations on a LDAP tree.
    """

    def getRoot(self):
        """
        Returns a new LDAP root for dc=example,dc=com.
        """
        return inmemory.ReadOnlyInMemoryLDAPEntry(
            dn=distinguishedname.DistinguishedName("dc=example,dc=com")
        )


class TestAddOpLDIF(OperationTestCase):
    """
    Unit tests for `AddOp`.
    """

    def testAsLDIF(self):
        """
        It will return the LDIF representation of the operation.
        """
        sut = delta.AddOp(
            entry.BaseLDAPEntry(
                dn="dc=example,dc=com",
                attributes={
                    "foo": ["bar", "baz"],
                    "quux": ["thud"],
                },
            )
        )

        result = sut.asLDIF()

        self.assertEqual(
            b"""dn: dc=example,dc=com
changetype: add
foo: bar
foo: baz
quux: thud

""",
            result,
        )

    def testAddOpEqualitySameEntry(self):
        """
        Objects are equal when the have the same LDAP entry.
        """
        first_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )
        second_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )

        first = delta.AddOp(first_entry)
        second = delta.AddOp(second_entry)

        self.assertEqual(first, second)

    def testAddOpInequalityDifferentEntry(self):
        """
        Objects are not equal when the have different LDAP entries.
        """
        first_entry = entry.BaseLDAPEntry(
            dn="ou=First Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )
        second_entry = entry.BaseLDAPEntry(
            dn="ou=First Team, dc=example,dc=com",
            attributes={"foo": ["other", "attributes"]},
        )

        first = delta.AddOp(first_entry)
        second = delta.AddOp(second_entry)

        self.assertNotEqual(first, second)

    def testAddOpInequalityNoEntryObject(self):
        """
        Objects is not equal with random objects.
        """
        team_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )
        sut = delta.AddOp(team_entry)

        self.assertNotEqual(sut, {"foo": ["same", "attributes"]})

    def testAddOpHashSimilar(self):
        """
        Objects which are equal have the same hash.
        """
        first_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )
        second_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["same", "attributes"]},
        )

        first = delta.AddOp(first_entry)
        second = delta.AddOp(second_entry)

        self.assertEqual(hash(first), hash(second))

    def testAddOpHashDifferent(self):
        """
        Objects which are not equal have different hash.
        """
        first_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["one", "attributes"]},
        )
        second_entry = entry.BaseLDAPEntry(
            dn="ou=Duplicate Team, dc=example,dc=com",
            attributes={"foo": ["other", "attributes"]},
        )

        first = delta.AddOp(first_entry)
        second = delta.AddOp(second_entry)

        self.assertNotEqual(hash(first), hash(second))

    def testAddOp_DNExists(self):
        """
        It fails to perform the `add` operation for an existing entry.
        """
        root = self.getRoot()
        root.addChild(
            rdn="ou=Existing Team",
            attributes={
                "objectClass": ["a", "b"],
                "ou": ["HR"],
            },
        )

        hr_entry = entry.BaseLDAPEntry(
            dn="ou=Existing Team, dc=example,dc=com",
            attributes={"foo": ["dont", "care"]},
        )
        sut = delta.AddOp(hr_entry)

        deferred = sut.patch(root)

        failure = self.failureResultOf(deferred)
        self.assertIsInstance(failure.value, ldaperrors.LDAPEntryAlreadyExists)

    def testRepr(self):
        """
        Getting string representation
        """
        sut = delta.AddOp(
            entry.BaseLDAPEntry(
                dn="dc=example,dc=com",
                attributes={
                    "bar": ["foo"],
                    "foo": ["bar"],
                },
            )
        )

        self.assertEqual(
            repr(sut),
            "AddOp(BaseLDAPEntry('dc=example,dc=com', "
            "{'bar': ['foo'], 'foo': ['bar']}))",
        )


class TestDeleteOpLDIF(OperationTestCase):
    """
    Unit tests for DeleteOp.
    """

    def testAsLDIF(self):
        """
        It return the LDIF representation of the delete operation.
        """
        sut = delta.DeleteOp("dc=example,dc=com")

        result = sut.asLDIF()
        self.assertEqual(
            b"""dn: dc=example,dc=com
changetype: delete

""",
            result,
        )

    def testDeleteOpEqualitySameDN(self):
        """
        Objects are equal when the have the same DN.
        """
        first_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")
        second_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")

        first = delta.DeleteOp(first_entry)
        second = delta.DeleteOp(second_entry)

        self.assertEqual(first, second)

    def testDeleteOpEqualityEqualDN(self):
        """
        DeleteOp objects are equal if their DNs are equal.
        """
        first_dn = distinguishedname.DistinguishedName(
            stringValue="ou=Team,dc=example,dc=com"
        )
        first = delta.DeleteOp(first_dn)

        second_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example, dc=com")
        second = delta.DeleteOp(second_entry)

        third = delta.DeleteOp("ou=Team, dc=example,dc=com")

        self.assertEqual(first, second)
        self.assertEqual(first, third)

    def testDeleteOpInequalityDifferentEntry(self):
        """
        DeleteOp objects are not equal when the have different LDAP entries.
        """
        first_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")
        second_entry = entry.BaseLDAPEntry(dn="ou=Cowboys, dc=example,dc=com")

        first = delta.DeleteOp(first_entry)
        second = delta.DeleteOp(second_entry)

        self.assertNotEqual(first, second)

    def testDeleteOpInequalityNoEntryObject(self):
        """
        DeleteOp objects is not equal with random objects.
        """
        team_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")

        sut = delta.DeleteOp(team_entry)

        self.assertNotEqual(sut, "ou=Team, dc=example,dc=com")

    def testDeleteOpHashSimilar(self):
        """
        Objects which are equal have the same hash.
        """
        first_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")
        second_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")

        first = delta.DeleteOp(first_entry)
        second = delta.DeleteOp(second_entry)

        self.assertEqual(hash(first), hash(second))

    def testDeleteOpHashDifferent(self):
        """
        Objects which are not equal have different hash.
        """
        first_entry = entry.BaseLDAPEntry(dn="ou=Team, dc=example,dc=com")
        second_entry = entry.BaseLDAPEntry(dn="ou=Cowboys, dc=example,dc=com")

        first = delta.DeleteOp(first_entry)
        second = delta.DeleteOp(second_entry)

        self.assertNotEqual(hash(first), hash(second))

    def testDeleteOp_DNNotFound(self):
        """
        If fail to delete when the RDN does not exists.
        """
        root = self.getRoot()
        sut = delta.DeleteOp("cn=nope,dc=example,dc=com")

        deferred = sut.patch(root)

        failure = self.failureResultOf(deferred)
        self.assertIsInstance(failure.value, ldaperrors.LDAPNoSuchObject)

    def testDeleteOpInvalidDN(self):
        """
        Invalid type of DN raises AssertionError
        """
        self.assertRaises(AssertionError, delta.DeleteOp, 0)

    def testRepr(self):
        """
        Getting string representation
        """
        sut = delta.DeleteOp("dc=example,dc=com")

        self.assertEqual(repr(sut), "DeleteOp('dc=example,dc=com')")


class TestModifyOp(OperationTestCase):
    """
    Unit tests for ModifyOp.
    """

    def testAsLDIF(self):
        """
        It will return a LDIF representation of the contained operations.
        """
        sut = delta.ModifyOp(
            "cn=Paula Jensen, ou=Dev Ops, dc=airius, dc=com",
            [
                delta.Add(
                    "postaladdress",
                    ["123 Anystreet $ Sunnyvale, CA $ 94086"],
                ),
                delta.Delete("description"),
                delta.Replace(
                    "telephonenumber",
                    ["+1 408 555 1234", "+1 408 555 5678"],
                ),
                delta.Delete("facsimiletelephonenumber", ["+1 408 555 9876"]),
            ],
        )

        result = sut.asLDIF()

        self.assertEqual(
            b"""dn: cn=Paula Jensen,ou=Dev Ops,dc=airius,dc=com
changetype: modify
add: postaladdress
postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086
-
delete: description
-
replace: telephonenumber
telephonenumber: +1 408 555 1234
telephonenumber: +1 408 555 5678
-
delete: facsimiletelephonenumber
facsimiletelephonenumber: +1 408 555 9876
-

""",
            result,
        )

    def testInequalityDiffertnDN(self):
        """
        Modify operations for different DN are not equal.
        """
        first = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )

        second = delta.ModifyOp(
            "cn=doe,dc=example,dc=com", [delta.Delete("description")]
        )

        self.assertNotEqual(first, second)

    def testInequalityDifferentModifications(self):
        """
        Modify operations with different modifications are not equal
        """
        first = delta.ModifyOp("cn=john,dc=example,dc=com", [delta.Add("description")])

        second = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )

        self.assertNotEqual(first, second)

    def testInequalityNotModifyOP(self):
        """
        Modify operations are not equal with other object types.
        """
        sut = delta.ModifyOp("cn=john,dc=example,dc=com", [delta.Delete("description")])

        self.assertNotEqual("cn=john,dc=example,dc=com", sut)

    def testInequalityDiffertnOperations(self):
        """
        Modify operations for same DN but different operations are not equal.
        """
        first = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )
        second = delta.ModifyOp(
            "cn=doe,dc=example,dc=com", [delta.Delete("homeDirectory")]
        )

        self.assertNotEqual(first, second)

    def testHashEquality(self):
        """
        Modify operations can be hashed and equal objects have the same
        hash.
        """
        first = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )

        second = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )

        self.assertEqual(first, second)
        self.assertEqual(
            first.asLDIF(),
            second.asLDIF(),
            "LDIF equality is a precondition for valid hash values",
        )
        self.assertEqual(hash(first), hash(second))

    def testHashInequality(self):
        """
        Different modify operations have different hash values.
        """
        first = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("description")]
        )

        second = delta.ModifyOp(
            "cn=john,dc=example,dc=com", [delta.Delete("homeDirectory")]
        )

        self.assertNotEqual(first.asLDIF(), second.asLDIF())
        self.assertNotEqual(hash(first), hash(second))

    def testModifyOp_DNNotFound(self):
        """
        If fail to modify when the RDN does not exists.
        """
        root = self.getRoot()
        sut = delta.ModifyOp(
            "cn=nope,dc=example,dc=com",
            [delta.Add("foo", ["bar"])],
        )

        deferred = sut.patch(root)

        failure = self.failureResultOf(deferred)
        self.assertIsInstance(failure.value, ldaperrors.LDAPNoSuchObject)

    def testRepr(self):
        """
        Getting string representation
        """
        sut = delta.ModifyOp("cn=john,dc=example,dc=com", [delta.Delete("description")])

        self.assertEqual(
            repr(sut),
            "ModifyOp(dn='cn=john,dc=example,dc=com', "
            "modifications=[Delete('description', [])])",
        )


class TestModificationComparison(unittest.TestCase):
    def testEquality_Add_True(self):
        a = delta.Add("k", ["b", "c", "d"])
        b = delta.Add("k", ["b", "c", "d"])
        self.assertEqual(a, b)

    def testEquality_AddVsDelete_False(self):
        a = delta.Add("k", ["b", "c", "d"])
        b = delta.Delete("k", ["b", "c", "d"])
        self.assertNotEquals(a, b)

    def testEquality_AttributeSet_False(self):
        a = delta.Add("k", ["b", "c", "d"])
        b = attributeset.LDAPAttributeSet("k", ["b", "c", "d"])
        self.assertNotEquals(a, b)

    def testEquality_List_False(self):
        a = delta.Add("k", ["b", "c", "d"])
        b = ["b", "c", "d"]
        self.assertNotEquals(a, b)
