/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS.
 *      Portions copyright 2013 Manuel Gaupp
 */
package org.opends.server.core;

import java.net.Socket;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;

import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.TestCaseUtils;
import org.opends.server.controls.ProxiedAuthV1Control;
import org.opends.server.controls.ProxiedAuthV2Control;
import org.opends.server.plugins.InvocationCounterPlugin;
import org.opends.server.plugins.ShortCircuitPlugin;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.BindRequestProtocolOp;
import org.opends.server.protocols.ldap.BindResponseProtocolOp;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.protocols.ldap.LDAPMessage;
import org.opends.server.protocols.ldap.LDAPResultCode;
import org.opends.server.protocols.ldap.ModifyDNRequestProtocolOp;
import org.opends.server.protocols.ldap.ModifyDNResponseProtocolOp;
import org.opends.server.tools.LDAPModify;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.LockManager.DNLock;
import org.opends.server.types.Operation;
import org.opends.server.types.RDN;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.forgerock.opendj.ldap.ResultCode.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.*;

@SuppressWarnings("javadoc")
public class TestModifyDNOperation extends OperationTestCase
{

  private Entry entry;
  private InternalClientConnection proxyUserConn;

  @BeforeClass
  public void setUp() throws Exception
  {
    TestCaseUtils.startServer();
    TestCaseUtils.initializeTestBackend(true);
    TestCaseUtils.clearBackend("userRoot");

    // Add the example.com entry
    TestCaseUtils.addEntry(
      "dn: dc=example,dc=com",
      "objectclass: top",
      "objectclass: domain",
      "dc: example",
      "aci: (targetattr=\"*\")(version 3.0; acl \"Proxy Rights\"; " +
           "allow(proxy) userdn=\"ldap:///uid=proxy.user,o=test\";)"
    );

    // Add the people entry
    TestCaseUtils.addEntry(
      "dn: ou=People,dc=example,dc=com",
      "objectclass: top",
      "objectclass: organizationalUnit",
      "ou: People"
    );

    // Add a test entry.
    entry = TestCaseUtils.addEntry(
      "dn: uid=user.0,ou=People,dc=example,dc=com",
      "objectClass: top",
      "objectClass: person",
      "objectClass: organizationalPerson",
      "objectClass: inetOrgPerson",
      "givenName: Aaccf",
      "sn: Amar",
      "cn: Aaccf Amar",
      "initials: AQA",
      "employeeNumber: 0",
      "uid: user.0",
      "mail: user.0@example.com",
      "userPassword: password",
      "telephoneNumber: 380-535-2354",
      "homePhone: 707-626-3913",
      "pager: 456-345-7750",
      "mobile: 366-674-7274",
      "street: 99262 Eleventh Street",
      "l: Salem",
      "st: NM",
      "postalCode: 36530",
      "postalAddress: Aaccf Amar$99262 Eleventh Street$Salem, NM  36530",
      "description: This is the description for Aaccf Amar."
    );

    // Add a user capable of using the proxied authorization control.
    TestCaseUtils.addEntry(
         "dn: uid=proxy.user,o=test",
         "objectClass: top",
         "objectClass: person",
         "objectClass: organizationalPerson",
         "objectClass: inetOrgPerson",
         "uid: proxy.user",
         "givenName: Proxy",
         "sn: User",
         "cn: Proxy User",
         "userPassword: password",
         "ds-privilege-name: bypass-acl",
         "ds-privilege-name: proxied-auth");

    proxyUserConn = new InternalClientConnection(dn("uid=proxy.user,o=test"));
  }

  /**
   * Invokes a number of operation methods on the provided modify operation
   * for which all processing has been completed. This method is used for
   * tests that bypass the referential integrity plugin for whatever reason.
   *
   * @param  modifyDNOperation  The operation to be tested.
   */
  private void
  examineCompletedOPNoExtraPluginCounts(ModifyDNOperation modifyDNOperation)
  {
    assertTrue(modifyDNOperation.getProcessingStartTime() > 0);
    assertTrue(modifyDNOperation.getProcessingStopTime() > 0);
    assertTrue(modifyDNOperation.getProcessingTime() >= 0);

//    assertEquals(InvocationCounterPlugin.getPreParseCount(), 1);
//    assertEquals(InvocationCounterPlugin.getPreOperationCount(), 1);
//    assertEquals(InvocationCounterPlugin.getPostOperationCount(), 1);
    ensurePostReponseHasRun();
//    assertEquals(InvocationCounterPlugin.getPostResponseCount(), 1);
  }

  /**
   * Invokes a number of operation methods on the provided modify operation
   * for which all processing has been completed.  The counters
   * postResponseCount and preParseCount are incremented twice when
   * referential integrity plugin is enabled.
   *
   * @param  modifyDNOperation  The operation to be tested.
   */
  private void examineCompletedOperation(ModifyDNOperation modifyDNOperation)
  {
    assertTrue(modifyDNOperation.getProcessingStartTime() > 0);
    assertTrue(modifyDNOperation.getProcessingStopTime() > 0);
    assertTrue(modifyDNOperation.getProcessingTime() >= 0);

//    assertEquals(InvocationCounterPlugin.getPreParseCount(), 2);
//    assertEquals(InvocationCounterPlugin.getPreOperationCount(), 1);
//    assertEquals(InvocationCounterPlugin.getPostOperationCount(), 1);
    ensurePostReponseHasRun();
//    assertEquals(InvocationCounterPlugin.getPostResponseCount(), 2);
  }

  /**
   * Invokes a number of operation methods on the provided modify operation
   * for which the pre-operation plugin was not called.
   *
   * @param  modifyDNOperation  The operation to be tested.
   */
  private void examineIncompleteOperation(ModifyDNOperation modifyDNOperation,
      ResultCode resultCode)
  {
    assertEquals(modifyDNOperation.getResultCode(), resultCode);
    assertTrue(modifyDNOperation.getErrorMessage().length() > 0);

    assertTrue(modifyDNOperation.getProcessingStartTime() > 0);
    assertTrue(modifyDNOperation.getProcessingStopTime() > 0);
    assertTrue(modifyDNOperation.getProcessingTime() >= 0);
    assertTrue(modifyDNOperation.getErrorMessage().length() > 0);

//    assertEquals(InvocationCounterPlugin.getPreParseCount(), 1);
//    assertEquals(InvocationCounterPlugin.getPreOperationCount(), 0);
//    assertEquals(InvocationCounterPlugin.getPostOperationCount(), 1);
    ensurePostReponseHasRun();
//    assertEquals(InvocationCounterPlugin.getPostResponseCount(), 1);
  }

  /**
   * Invokes a number of operation methods on the provided modify operation
   * for which an error was found during parsing.
   *
   * @param  modifyDNOperation  The operation to be tested.
   */
  private void examineUnparsedOperation(ModifyDNOperation modifyDNOperation, ResultCode resultCode)
  {
    assertEquals(modifyDNOperation.getResultCode(), resultCode);
    assertTrue(modifyDNOperation.getErrorMessage().length() > 0);

    assertTrue(modifyDNOperation.getProcessingStartTime() > 0);
    assertTrue(modifyDNOperation.getProcessingStopTime() > 0);
    assertTrue(modifyDNOperation.getProcessingTime() >= 0);
    assertTrue(modifyDNOperation.getErrorMessage().length() > 0);

//    assertEquals(InvocationCounterPlugin.getPreParseCount(), 1);
//    assertEquals(InvocationCounterPlugin.getPreOperationCount(), 0);
//    assertEquals(InvocationCounterPlugin.getPostOperationCount(), 0);
    ensurePostReponseHasRun();
//    assertEquals(InvocationCounterPlugin.getPostResponseCount(), 1);
  }

  @Override
  protected Operation[] createTestOperations() throws Exception
  {
    return new ModifyDNOperation[] {
      newModifyDNOperationRaw("cn=test,ou=test", "cn=test2", true, "dc=example,dc=com"),
      newModifyDNOperation("cn=test,ou=test", "cn=test2", true, "dc=example,dc=com")
    };
  }

  private ModifyDNOperation runModifyDNOperation(
      String entryDN, String newRDN, boolean deleteOldRDN, String newSuperior) throws DirectoryException
  {
    ModifyDNOperation op = newModifyDNOperation(entryDN, newRDN, deleteOldRDN, newSuperior);
    op.run();
    return op;
  }

  private ModifyDNOperation runModifyDNOperationRaw(
      String entryDN, String newRDN, boolean deleteOldRDN, String newSuperior)
  {
    ModifyDNOperation op = newModifyDNOperationRaw(entryDN, newRDN, deleteOldRDN, newSuperior);
    op.run();
    return op;
  }

  private ModifyDNOperationBasis newModifyDNOperation(
      String entryDN, String newRDN, boolean deleteOldRDN, String newSuperior) throws DirectoryException
  {
    return new ModifyDNOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(),
        Collections.<Control> emptyList(), dn(entryDN), rdn(newRDN), deleteOldRDN, dn(newSuperior));
  }

  private ModifyDNOperationBasis newModifyDNOperationRaw(
      String entryDN, String newRDN, boolean deleteOldRDN, String newSuperior)
  {
    return new ModifyDNOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(),
        Collections.<Control> emptyList(), b(entryDN), b(newRDN), deleteOldRDN, b(newSuperior));
  }

  private ModifyDNOperation runModifyDNOperation(
      InternalClientConnection conn, String entryDN, String newRDN, boolean deleteOldRDN, Control control) throws DirectoryException
  {
    ModifyDNOperation op = new ModifyDNOperationBasis(conn, nextOperationID(), nextMessageID(),
        Collections.singletonList(control), dn(entryDN), rdn(newRDN), deleteOldRDN, null);
    op.run();
    return op;
  }

  private ModifyDNOperation runModifyDNOperationRaw(
      String entryDN, String newRDN, boolean deleteOldRDN, String newSuperior, Control control)
  {
    ModifyDNOperation op = new ModifyDNOperationBasis(proxyUserConn, nextOperationID(), nextMessageID(),
        Collections.singletonList(control), b(entryDN), b(newRDN), deleteOldRDN, b(newSuperior));
    op.run();
    return op;
  }

  private ByteString b(String s)
  {
    return s != null ? ByteString.valueOfUtf8(s) : null;
  }

  private DN dn(String s) throws DirectoryException
  {
    return s != null ? DN.valueOf(s) : null;
  }

  private RDN rdn(String s) throws DirectoryException
  {
    return s != null ? RDN.decode(s) : null;
  }

  private void assertSuccessAndEntryExists(ModifyDNOperation modifyDNOperation,
      String entryDN, boolean user0Exists, boolean userTest0Exists)
      throws DirectoryException
  {
    assertSuccess(modifyDNOperation);
    final Entry newEntry = DirectoryServer.getEntry(dn(entryDN));
    assertNotNull(newEntry);

    final RDN rdn = newEntry.getName().rdn();
    for (int i = 0; i < rdn.getNumValues(); i++)
    {
      AttributeType attribute = rdn.getAttributeType(i);
      assertEquals(newEntry.hasValue(attribute, null, b("user.0")), user0Exists);
      assertEquals(newEntry.hasValue(attribute, null, b("user.test0")), userTest0Exists);
    }
  }

  @Test
  public void testRawModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperationRaw(oldEntryDN, "uid=user.test0", false, null);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, true, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperationRaw(newEntryDN, "uid=user.0", true, null);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testProcessedModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=user.test0", false, null);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, true, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(newEntryDN, "uid=user.0", true, null);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  /**
   * Test if it's possible to modify an rdn to a value that matches the current value
   * by changing the case of some characters.
   */
  @Test
  public void testModifySameDN() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=USER.0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=USER.0", true, null);
    assertSuccess(modifyDNOperation);

    Entry newEntry = DirectoryServer.getEntry(dn(oldEntryDN));
    assertNotNull(newEntry);
    assertEquals(newEntry.getName().toString(), newEntryDN);
    assertAttrValue(newEntry, "uid", "USER.0");

    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(newEntryDN, "uid=user.0", true, null);
    assertSuccess(modifyDNOperation);
    assertNotNull(DirectoryServer.getEntry(dn(oldEntryDN)));
    examineCompletedOperation(modifyDNOperation);
  }

  /** Add another attribute to the RDN and change case of the existing value. */
  @Test
  public void testModifyDNchangeCaseAndAddValue() throws Exception
  {
    TestCaseUtils.addEntry(
         "dn: uid=userid.0,ou=People,dc=example,dc=com",
         "objectClass: top",
         "objectClass: person",
         "objectClass: organizationalPerson",
         "objectClass: inetOrgPerson",
         "uid: userid.0",
         "givenName: Babs",
         "sn: Jensen",
         "cn: Babs Jensen");

    String oldEntryDN = "uid=userid.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=UserID.0+cn=Test,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=UserID.0+cn=Test", false, null);
    assertSuccess(modifyDNOperation);
    assertEntryAttrValue(newEntryDN, "uid", "UserID.0");
    examineCompletedOperation(modifyDNOperation);

    TestCaseUtils.deleteEntry(dn(newEntryDN));
  }

  private void assertEntryAttrValue(String entryDN, String attrName, String expectedAttrValue)
      throws DirectoryException
  {
    Entry newEntry = DirectoryServer.getEntry(dn(entryDN));
    assertNotNull(newEntry);
    assertEquals(newEntry.getName().toString(), entryDN);

    assertAttrValue(newEntry, attrName, expectedAttrValue);
  }

  private void assertAttrValue(Entry newEntry, String attrName, String expectedAttrValue)
  {
    AttributeType at = DirectoryServer.getAttributeTypeOrNull(attrName);
    List<Attribute> attrList = newEntry.getAttribute(at);
    assertEquals(attrList.size(), 1);

    // Because deleteOldRDN is true, the values from RDN and the entry have to be identical
    ByteString valueFromEntry = attrList.get(0).iterator().next();
    ByteString valueFromRDN = newEntry.getName().rdn().getAttributeValue(at);
    assertEquals(valueFromEntry, valueFromRDN);
    assertEquals(valueFromEntry, b(expectedAttrValue));
  }

  /**
   * Add a value to the RDN which is already part of the entry, but with another string representation.
   */
  @Test
  public void testModifyDNchangeCaseOfExistingEntryValue() throws Exception
  {
    TestCaseUtils.addEntry(
         "dn: uid=userid.0,ou=People,dc=example,dc=com",
         "objectClass: top",
         "objectClass: person",
         "objectClass: organizationalPerson",
         "objectClass: inetOrgPerson",
         "uid: userid.0",
         "givenName: Babs",
         "sn: Jensen",
         "cn: Babs Jensen");

    String oldEntryDN = "uid=userid.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=userid.0+sn=JENSEN,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=userid.0+sn=JENSEN", false, null);
    assertSuccess(modifyDNOperation);
    assertEntryAttrValue(newEntryDN, "sn", "JENSEN");
    examineCompletedOperation(modifyDNOperation);

    TestCaseUtils.deleteEntry(dn(newEntryDN));
  }

  private void assertSuccess(ModifyDNOperation op)
  {
    assertEquals(op.getResultCode(), SUCCESS);
    assertEquals(op.getErrorMessage().length(), 0);
  }

  @Test
  public void testRawDeleteOldRDNModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperationRaw(oldEntryDN, "uid=user.test0", true, null);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, false, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperationRaw(newEntryDN, "uid=user.0", true, null);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testProcessedDeleteOldRDNModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=user.test0", true, null);

    CancelRequest cancelRequest = new CancelRequest(false, LocalizableMessage.raw("testCancelBeforeStartup"));
    CancelResult cancelResult = modifyDNOperation.cancel(cancelRequest);
    assertEquals(cancelResult.getResultCode(), TOO_LATE);

    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, false, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(newEntryDN, "uid=user.0", true, null);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testRawNewSuperiorModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperationRaw(oldEntryDN, "uid=user.test0", true, "dc=example,dc=com");
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, false, true);
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperationRaw(newEntryDN, "uid=user.0", true, "ou=People,dc=example,dc=com");
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);
  }

  @Test
  public void testProcessedNewSuperiorModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(oldEntryDN, "uid=user.test0", true, "dc=example,dc=com");
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, false, true);
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(newEntryDN, "uid=user.0", true, "ou=People,dc=example,dc=com");
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);
  }

  @Test
  public void testRawRDNModify() throws Exception
  {
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "cn=Aaccf Amar Test,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(oldEntryDN, "cn=Aaccf Amar Test", true, "dc=example,dc=com");
    assertSuccess(modifyDNOperation);
    Entry entry = assertCnAttrValues(newEntryDN, oldEntryDN);
    assertNull(entry.getAttribute("uid"));
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(newEntryDN, "uid=user.0", false, "ou=People,dc=example,dc=com");
    assertSuccess(modifyDNOperation);
    Entry newOldEntry = assertCnAttrValues(oldEntryDN, newEntryDN);
    for(Attribute attribute : newOldEntry.getAttribute("uid"))
    {
      assertTrue(attribute.contains(b("user.0")));
    }
    examineCompletedOPNoExtraPluginCounts(modifyDNOperation);
  }

  private Entry assertCnAttrValues(String entryDN1, String entryDN2) throws DirectoryException
  {
    Entry entry = DirectoryServer.getEntry(dn(entryDN1));
    assertNotNull(entry);
    assertNull(DirectoryServer.getEntry(dn(entryDN2)));

    for (Attribute attribute : entry.getAttribute("cn"))
    {
      assertTrue(attribute.contains(b("Aaccf Amar Test")));
      assertTrue(attribute.contains(b("Aaccf Amar")));
    }
    return entry;
  }

  @Test
  public void testInvalidEntryModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(
        "uid=user.invalid,ou=People,dc=example,dc=com", "uid=user.test0", true, "dc=example,dc=com");
    examineIncompleteOperation(modifyDNOperation, NO_SUCH_OBJECT);
  }

  @Test
  public void testInvalidRDNModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(
        "uid=user.0,ou=People,dc=example,dc=com", "invalid=invalid", true, "dc=example,dc=com");
    examineIncompleteOperation(modifyDNOperation, OBJECTCLASS_VIOLATION);
  }

  @Test
  public void testInvalidSuperiorModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation(
        "uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", true, "dc=invalid,dc=com");
    examineIncompleteOperation(modifyDNOperation, NO_SUCH_OBJECT);
  }

  @Test
  public void testRawNoSuchDNModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperationRaw("invalid DN", "uid=user.test0", true, "dc=example,dc=com");
    examineUnparsedOperation(modifyDNOperation, INVALID_DN_SYNTAX);
  }

  @Test
  public void testRawNoSuchRDNModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperationRaw("uid=user.0,ou=People,dc=example,dc=com", "invalid RDN", true, "dc=example,dc=com");
    examineUnparsedOperation(modifyDNOperation, INVALID_DN_SYNTAX);
  }

  @Test
  public void testRawInvalidSuperiorModify() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperationRaw("uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", true, "invalid superior");
    examineUnparsedOperation(modifyDNOperation, INVALID_DN_SYNTAX);
  }

  @Test
  public void testModifySuffix() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperation("dc=example,dc=com", "dc=exampletest", true, null);
    examineIncompleteOperation(modifyDNOperation, UNWILLING_TO_PERFORM);
  }

  @Test
  public void testRawProxyAuthV1Modify() throws Exception
  {
    ProxiedAuthV1Control authV1Control = new ProxiedAuthV1Control(b("cn=Directory Manager,cn=Root DNs,cn=config"));
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation = runModifyDNOperationRaw(oldEntryDN, "uid=user.test0", false, null, authV1Control);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, true, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperationRaw(newEntryDN, "uid=user.0", true, null, authV1Control);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testProcessedProxyAuthV1Modify() throws Exception
  {
    ProxiedAuthV1Control authV1Control = new ProxiedAuthV1Control(b("cn=Directory Manager,cn=Root DNs,cn=config"));
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(proxyUserConn, oldEntryDN, "uid=user.test0", false, authV1Control);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, true, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(proxyUserConn, newEntryDN, "uid=user.0", true, authV1Control);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testProcessedProxyAuthV1DeniedModify() throws Exception
  {
    ProxiedAuthV1Control authV1Control = new ProxiedAuthV1Control(b("cn=nonexistent,o=test"));
    InvocationCounterPlugin.resetAllCounters();

    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(proxyUserConn, "uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", false, authV1Control);
    examineIncompleteOperation(modifyDNOperation, AUTHORIZATION_DENIED);
  }

  @Test
  public void testProcessedProxyAuthV2Modify() throws Exception
  {
    ProxiedAuthV2Control authV2Control = new ProxiedAuthV2Control(b("dn:cn=Directory Manager,cn=Root DNs,cn=config"));
    String oldEntryDN = "uid=user.0,ou=People,dc=example,dc=com";
    String newEntryDN = "uid=user.test0,ou=People,dc=example,dc=com";

    InvocationCounterPlugin.resetAllCounters();
    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(proxyUserConn, oldEntryDN, "uid=user.test0", false, authV2Control);
    assertSuccessAndEntryExists(modifyDNOperation, newEntryDN, true, true);
    examineCompletedOperation(modifyDNOperation);

    InvocationCounterPlugin.resetAllCounters();
    modifyDNOperation = runModifyDNOperation(proxyUserConn, newEntryDN, "uid=user.0", true, authV2Control);
    assertSuccessAndEntryExists(modifyDNOperation, oldEntryDN, true, false);
    examineCompletedOperation(modifyDNOperation);
  }

  @Test
  public void testProcessedProxyAuthV2DeniedModify() throws Exception
  {
    ProxiedAuthV2Control authV2Control = new ProxiedAuthV2Control(b("dn:cn=nonexistent,o=test"));
    InvocationCounterPlugin.resetAllCounters();

    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(proxyUserConn, "uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", false, authV2Control);
    examineIncompleteOperation(modifyDNOperation, AUTHORIZATION_DENIED);
  }

  @Test
  public void testProcessedProxyAuthV2CriticalityModify() throws Exception
  {
    Control authV2Control = new LDAPControl(OID_PROXIED_AUTH_V2, false, b("dn:cn=nonexistent,o=test"));
    InvocationCounterPlugin.resetAllCounters();

    ModifyDNOperation modifyDNOperation =
        runModifyDNOperation(proxyUserConn, "uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", false, authV2Control);
    examineIncompleteOperation(modifyDNOperation, PROTOCOL_ERROR);
  }

  @Test
  public void testProcessedUnsupportedControlModify() throws Exception
  {
    LDAPControl assertControl = new LDAPControl("1.1.1.1.1.1", true);
    InvocationCounterPlugin.resetAllCounters();

    ModifyDNOperation modifyDNOperation = runModifyDNOperation(getRootConnection(), "uid=user.0,ou=People,dc=example,dc=com", "uid=user.test0", false, assertControl);
    examineIncompleteOperation(modifyDNOperation, UNAVAILABLE_CRITICAL_EXTENSION);
  }

  @Test
  public void testShortCircuitModify() throws Exception
  {
    // Since we are going to be watching the post-response count, we need to
    // wait for the server to become idle before kicking off the next request to
    // ensure that any remaining post-response processing from the previous
    // operation has completed.
    assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000));

    // Establish a connection to the server.
    try (Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()))
    {
    InvocationCounterPlugin.resetAllCounters();

    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    LDAPWriter w = new LDAPWriter(s);
    TestCaseUtils.configureSocket(s);
    BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(b("cn=Directory Manager"), 3, b("password"));
    LDAPMessage bindMessage = new LDAPMessage(1, bindRequest);
    w.writeMessage(bindMessage);

    bindMessage = r.readMessage();
    BindResponseProtocolOp bindResponse = bindMessage.getBindResponseProtocolOp();
    assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);

    assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000));
    InvocationCounterPlugin.resetAllCounters();
    ModifyDNRequestProtocolOp modifyRequest =
        new ModifyDNRequestProtocolOp(b(entry.getName().toString()), b("uid=user.test0"), false);
    LDAPMessage message = new LDAPMessage(2, modifyRequest,
                                          ShortCircuitPlugin.createShortCircuitControlList(80, "PreOperation"));
    w.writeMessage(message);

    message = r.readMessage();
    ModifyDNResponseProtocolOp modifyResponse = message.getModifyDNResponseProtocolOp();

    assertEquals(modifyResponse.getResultCode(), 80);
//    assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);
    }
  }

  @Test(groups = "slow")
  public void testWriteLockModify() throws Exception
  {
    // We need the operation to be run in a separate thread because we are going
    // to write lock the entry in the test case thread and check that the
    // modify DN operation does not proceed.

    // Establish a connection to the server.
    try (Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()))
    {
      org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
      LDAPWriter w = new LDAPWriter(s);
      TestCaseUtils.configureSocket(s);

      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(b("cn=Directory Manager"), 3, b("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);

      // Since we are going to be watching the post-response count, we need to
      // wait for the server to become idle before kicking off the next request
      // to ensure that any remaining post-response processing from the previous
      // operation has completed.
      assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000));

      final DNLock writeLock = DirectoryServer.getLockManager().tryWriteLockEntry(entry.getName());
      assertNotNull(writeLock);

      try
      {
        InvocationCounterPlugin.resetAllCounters();

        //long modifyDNRequests  = ldapStatistics.getModifyDNRequests();
        //long modifyDNResponses = ldapStatistics.getModifyDNResponses();

        ModifyDNRequestProtocolOp modifyRequest =
            new ModifyDNRequestProtocolOp(b(entry.getName().toString()), b("uid=user.test0"), false);
        message = new LDAPMessage(2, modifyRequest);
        w.writeMessage(message);

        message = r.readMessage();
        ModifyDNResponseProtocolOp modifyResponse = message.getModifyDNResponseProtocolOp();

        assertEquals(modifyResponse.getResultCode(), LDAPResultCode.BUSY);

//        assertEquals(InvocationCounterPlugin.getPreParseCount(), 1);
//        assertEquals(InvocationCounterPlugin.getPreOperationCount(), 0);
//        assertEquals(InvocationCounterPlugin.getPostOperationCount(), 0);
//        // The post response might not have been called yet.
//        assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);

//        assertEquals(ldapStatistics.getModifyDNRequests(), modifyDNRequests+1);
//        assertEquals(ldapStatistics.getModifyDNResponses(),
//                     modifyDNResponses+1);
      } finally
      {
        writeLock.unlock();
      }
    }
  }

  /**
   * Tests performing a modify DN operation in which the new RDN contains an
   * attribute type marked OBSOLETE in the server schema.
   */
  @Test
  public void testModifyDNWithObsoleteAttribute()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    String path = TestCaseUtils.createTempFile(
         "dn: cn=schema",
         "changetype: modify",
         "add: attributeTypes",
         "attributeTypes: ( testmodifydnwithobsoleteattribute-oid " +
              "NAME 'testModifyDNWithObsoleteAttribute' OBSOLETE " +
              "SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE " +
              "X-ORGIN 'SchemaBackendTestCase' )");

    String attrName = "testmodifydnwithobsoleteattribute";
    assertFalse(DirectoryServer.getSchema().hasAttributeType(attrName));

    String[] args =
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D", "cn=Directory Manager",
      "-w", "password",
      "-f", path
    };

    assertEquals(LDAPModify.mainModify(args, false, null, System.err), 0);
    assertTrue(DirectoryServer.getSchema().hasAttributeType(attrName));

    path = TestCaseUtils.createTempFile(
         "dn: cn=oldrdn,o=test",
         "changetype: add",
         "objectClass: top",
         "objectClass: device",
         "objectClass: extensibleObject",
         "cn: oldrdn",
         "",
         "dn: cn=oldrdn,o=test",
         "changetype: moddn",
         "newRDN: testModifyDNWithObsoleteAttribute=foo",
         "deleteOldRDN: 0"
    );

    args = new String[]
    {
      "-h", "127.0.0.1",
      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
      "-D", "cn=Directory Manager",
      "-w", "password",
      "-f", path
    };

    assertFalse(LDAPModify.mainModify(args, false, null, null) == 0);
  }

  /**
   * Tests a subtree rename operation to ensure that subordinate modify DN
   * plugins will be invoked as expected.
   */
  @Test
  public void testSubordinateModifyDNPluginsForSubtreeRename()
         throws Exception
  {
    try
    {
      InvocationCounterPlugin.resetAllCounters();
      TestCaseUtils.clearBackend("userRoot", "dc=example,dc=com");

      TestCaseUtils.addEntries(
        "dn: ou=People,dc=example,dc=com",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: People",
        "",
        "dn: uid=first.test,ou=People,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: first.test",
        "givenName: First",
        "sn: Test",
        "cn: First Test",
        "userPassword: Password",
        "ou: People",
        "",
        "dn: uid=second.test,ou=People,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: second.test",
        "givenName: Second",
        "sn: Test",
        "cn: Second Test",
        "userPassword: Password");

      assertTrue(DirectoryServer.entryExists(
                      dn("uid=first.test,ou=People,dc=example,dc=com")));
      assertFalse(DirectoryServer.entryExists(
                      dn("uid=first.test,ou=Users,dc=example,dc=com")));

      ModifyDNOperation modifyDNOperation =
          getRootConnection().processModifyDN("ou=People,dc=example,dc=com", "ou=Users", true);
      assertSuccess(modifyDNOperation);
//      assertEquals(InvocationCounterPlugin.getSubordinateModifyDNCount(), 2);

      assertFalse(DirectoryServer.entryExists(
                       dn("uid=first.test,ou=People,dc=example,dc=com")));
      assertTrue(DirectoryServer.entryExists(
                      dn("uid=first.test,ou=Users,dc=example,dc=com")));
    }
    finally
    {
      // Other tests in this class rely on a predefined structure, so we need to
      // make sure to put it back to the way it should be.
      setUp();
      InvocationCounterPlugin.resetAllCounters();
    }
  }

  /**
   * Tests a subtree move operation to ensure that subordinate modify DN
   * plugins will be invoked as expected.
   */
  @Test
  public void testSubordinateModifyDNPluginsForSubtreeMove()
         throws Exception
  {
    try
    {
      InvocationCounterPlugin.resetAllCounters();
      TestCaseUtils.clearBackend("userRoot", "dc=example,dc=com");

      TestCaseUtils.addEntries(
        "dn: ou=Org 1,dc=example,dc=com",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: Org 1",
        "",
        "dn: ou=Org 2,dc=example,dc=com",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: Org 2",
        "",
        "dn: ou=Org 1.1,ou=Org 1,dc=example,dc=com",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: Org 1.1",
        "",
        "dn: uid=first.test,ou=Org 1.1,ou=Org 1,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: first.test",
        "givenName: First",
        "sn: Test",
        "cn: First Test",
        "userPassword: Password",
        "ou: Org 1.1",
        "",
        "dn: uid=second.test,ou=Org 1.1,ou=Org 1,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: second.test",
        "givenName: Second",
        "sn: Test",
        "cn: Second Test",
        "userPassword: Password");

      assertTrue(DirectoryServer.entryExists(
           dn("uid=first.test,ou=Org 1.1,ou=Org 1,dc=example,dc=com")));
      assertFalse(DirectoryServer.entryExists(
           dn("uid=first.test,ou=Org 2.1,ou=Org 2,dc=example,dc=com")));

      ModifyDNOperation modifyDNOperation =
          getRootConnection().processModifyDN("ou=Org 1.1,ou=Org 1,dc=example,dc=com",
                                "ou=Org 2.1", true,
                                "ou=Org 2,dc=example,dc=com");
      assertSuccess(modifyDNOperation);
//      assertEquals(InvocationCounterPlugin.getSubordinateModifyDNCount(), 2);

      assertFalse(DirectoryServer.entryExists(
           dn("uid=first.test,ou=Org 1.1,ou=Org 1,dc=example,dc=com")));
      assertTrue(DirectoryServer.entryExists(
           dn("uid=first.test,ou=Org 2.1,ou=Org 2,dc=example,dc=com")));
    }
    finally
    {
      // Other tests in this class rely on a predefined structure, so we need to
      // make sure to put it back to the way it should be.
      setUp();
      InvocationCounterPlugin.resetAllCounters();
    }
  }

  @Test
  public void testCancelBeforeStartup() throws Exception
  {
    InvocationCounterPlugin.resetAllCounters();

    ModifyDNOperation modifyDNOperation = newModifyDNOperation(
        "uid=user.invalid,ou=People,dc=example,dc=com", "uid=user.test0", true, "dc=example,dc=com");

    CancelRequest cancelRequest = new CancelRequest(false,
                                                    LocalizableMessage.raw("testCancelBeforeStartup"));
    modifyDNOperation.abort(cancelRequest);
    modifyDNOperation.run();
    assertEquals(modifyDNOperation.getResultCode(), CANCELLED);
  }

  /**
   * Tests whether an invalid rdn is allowed during an modrdn operation.
   * This test uses a valid attribute type with an empty value.
   *
   * @throws Exception
   */
  @Test(expectedExceptions=InvalidNameException.class)
  public void testInvalidModRDN() throws Exception
  {
    Hashtable<String,String> env = new Hashtable<>();
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
    String url = "ldap://localhost:" + TestCaseUtils.getServerLdapPort()
            +"/dc=example,dc=com";
    env.put(Context.PROVIDER_URL,url);
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, "cn=directory manager");
    env.put(Context.SECURITY_CREDENTIALS, "password");

    env.put("java.naming.ldap.deleteRDN", "true");  // default is 'true'
    /* Create the initial context */
    DirContext ctx = new InitialDirContext(env);
    try
    {
      ctx.rename("uid=user.0,ou=People,dc=example,dc=com",
                   "uid=,ou=People,dc=example,dc=com");
    }
    finally
    {
      /* Close the context when it's done */
      ctx.close();
    }
  }
}
