/*
 * 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 2007-2010 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.server.replication;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;

import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.MemoryBackend;
import org.opends.server.core.DirectoryServer;
import org.opends.server.replication.common.CSNGenerator;
import org.opends.server.replication.plugin.DomainFakeCfg;
import org.opends.server.replication.plugin.LDAPReplicationDomain;
import org.opends.server.replication.plugin.MultimasterReplication;
import org.opends.server.replication.protocol.AddMsg;
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.ModifyDNMsg;
import org.opends.server.replication.protocol.ModifyMsg;
import org.opends.server.replication.server.ReplServerFakeConfiguration;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.replication.service.ReplicationBroker;
import org.opends.server.types.Attributes;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.core.DirectoryServer.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.testng.Assert.*;

/**
 * Test that the dependencies are computed correctly when replaying
 * sequences of operations that requires to follow a given order
 * such as : ADD an entry, ADD a children entry.
 */
@SuppressWarnings("javadoc")
public class DependencyTest extends ReplicationTestCase
{

  private final long CLEAN_DB_GENERATION_ID = 7883L;
  private DN TEST_ROOT_DN;


  @BeforeClass
  public void setup() throws Exception
  {
     TEST_ROOT_DN = DN.valueOf(TEST_ROOT_DN_STRING);
  }

  /**
   * Check that a sequence of dependents adds and mods is correctly ordered:
   * Using a deep dit :
   *    TEST_ROOT_DN_STRING
   *       |
   *    dc=dependency1
   *       |
   *    dc=dependency2
   *       |
   *    dc=dependency3
   *       |
   *
   *       |
   *    dc=dependencyN
   * This test sends a sequence of interleaved ADD operations and MODIFY
   * operations to build such a dit.
   *
   * Then test that the sequence of Delete necessary to remove
   * all those entries is also correctly ordered.
   */
  @Test(enabled=true, groups="slow")
  public void addModDelDependencyTest() throws Exception
  {
    ReplicationServer replServer = null;
    LDAPReplicationDomain domain = null;
    DN baseDN = TEST_ROOT_DN;
    int brokerId = 2;
    int serverId = 1;
    int replServerId = 81;
    int addSequenceLength = 30;


    cleanDB();

    try
    {
      /*
       * FIRST PART :
       * Check that a sequence of dependent ADD is correctly ordered.
       *
       * - Create replication server
       * - Send sequence of ADD messages to the replication server
       * - Configure replication server
       * - check that the last entry has been correctly added
       */
      Entry entry = TestCaseUtils.entryFromLdifString(
          "dn:" + TEST_ROOT_DN_STRING + "\n"
          + "objectClass: top\n"
          + "objectClass: organization\n"
          + "entryuuid: " + stringUID(1) + "\n");

      replServer = newReplicationServer(replServerId, addSequenceLength * 5 + 100, "dependencyTestAddModDelDependencyTestDb");

      ReplicationBroker broker = openReplicationSession(
          baseDN, brokerId, 1000, replServer.getReplicationPort(), 1000, CLEAN_DB_GENERATION_ID);

      Thread.sleep(2000);

      // send a sequence of add operation
      DN addDN = TEST_ROOT_DN;
      CSNGenerator gen = new CSNGenerator(brokerId, 0L);

      int sequence;
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        entry.removeAttribute(getAttributeTypeOrNull("entryuuid"));
        entry.addAttribute(Attributes.create("entryuuid", stringUID(sequence+1)),
                           new LinkedList<ByteString>());
        addDN = DN.valueOf("dc=dependency" + sequence + "," + addDN);
        broker.publish(addMsg(addDN, entry, sequence + 1, sequence, gen));
        broker.publish(modifyMsg(addDN, sequence + 1, generatemods("description", "test"), gen));
      }

      // configure and start replication of TEST_ROOT_DN_STRING on the server
      domain = startNewLDAPReplicationDomain(replServer, baseDN, serverId, 100000);

      // check that last entry in sequence got added.
      Entry lastEntry = getEntry(addDN, 30000, true);
      assertNotNull(lastEntry,
                    "The last entry of the ADD sequence was not added.");

      // Check that all the modify have been replayed
      // (all the entries should have a description).
      addDN = TEST_ROOT_DN;
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        addDN = DN.valueOf("dc=dependency" + sequence + "," + addDN);
        checkEntryHasAttributeValue(
            addDN, "description", "test", 10, "The modification was not replayed on entry " + addDN);
      }

      /*
       * SECOND PART
       *
       * Now check that the dependencies between delete are correctly
       * managed.
       *
       * disable the domain while we publish the delete message to
       * to replication server so that when we enable it it receives the
       * delete operation in bulk.
       */

      domain.disable();
      Thread.sleep(2000);  // necessary because disable does not wait
                           // for full termination of all threads. (issue 1571)

      DN deleteDN = addDN;
      while (sequence-->1)
      {
        broker.publish(delMsg(deleteDN, sequence + 1, gen));
        deleteDN = deleteDN.parent();
      }

      domain.enable();

      // check that entry just below the base entry was deleted.
      // (we can't delete the base entry because some other tests might
      // have added other children)
      DN node1 = DN.valueOf("dc=dependency1," + TEST_ROOT_DN_STRING);
      Entry baseEntry = getEntry(node1, 30000, false);
      assertNull(baseEntry,
                 "The last entry of the DEL sequence was not deleted.");
    }
    finally
    {
      remove(replServer);
      if (domain != null)
      {
        MultimasterReplication.deleteDomain(baseDN);
      }
    }
  }

  private AddMsg addMsg(DN addDN, Entry entry, int uniqueId, int parentId, CSNGenerator gen)
  {
    return new AddMsg(gen.newCSN(), addDN, stringUID(uniqueId), stringUID(parentId),
        entry.getObjectClassAttribute(), entry.getAttributes(), null);
  }

  private ModifyMsg modifyMsg(DN dn, int entryUUID, List<Modification> mods, CSNGenerator gen)
  {
    return new ModifyMsg(gen.newCSN(), dn, mods, stringUID(entryUUID));
  }

  private DeleteMsg delMsg(DN delDN, int entryUUID, CSNGenerator gen)
  {
    return new DeleteMsg(delDN, gen.newCSN(), stringUID(entryUUID));
  }

  private ModifyDNMsg modDNMsg(DN dn, String newRDN, int entryUUID, int newSuperiorEntryUUID, CSNGenerator gen)
  {
    return new ModifyDNMsg(dn, gen.newCSN(), stringUID(entryUUID), stringUID(newSuperiorEntryUUID), true, null, newRDN);
  }

  /**
   * Check the dependency between moddn and delete operation
   * when an entry is renamed to a new dn and then deleted.
   * Disabled: need investigations to fix random failures
   */
  @Test(enabled=false)
  public void moddnDelDependencyTest() throws Exception
  {
    ReplicationServer replServer = null;
    LDAPReplicationDomain domain = null;
    DN baseDN = TEST_ROOT_DN;
    int brokerId = 2;
    int serverId = 1;
    int replServerId = 82;

    cleanDB();

    try
    {
      // Create replication server, replication domain and broker.
      Entry entry = TestCaseUtils.entryFromLdifString(
          "dn:" + TEST_ROOT_DN_STRING + "\n"
          + "objectClass: top\n"
          + "objectClass: organization\n");

      CSNGenerator gen = new CSNGenerator(brokerId, 0L);
      int renamedEntryUuid = 100;

      replServer = newReplicationServer(replServerId, 200, "dependencyTestModdnDelDependencyTestDb");

      // configure and start replication of TEST_ROOT_DN_STRING on the server
      Thread.sleep(2000);
      domain = startNewLDAPReplicationDomain(replServer, baseDN, serverId, 100000);

      ReplicationBroker broker = openReplicationSession(
          baseDN, brokerId, 1000, replServer.getReplicationPort(), 1000, CLEAN_DB_GENERATION_ID);

      // add an entry to play with.
      entry.removeAttribute(getAttributeTypeOrNull("entryuuid"));
      entry.addAttribute(Attributes.create("entryuuid",
                         stringUID(renamedEntryUuid)),
                         new LinkedList<ByteString>());
      DN addDN = DN.valueOf("dc=moddndel" + "," + TEST_ROOT_DN_STRING);
      broker.publish(addMsg(addDN, entry, renamedEntryUuid, 1, gen));

      // check that the entry was correctly added
      checkEntryHasAttributeValue(addDN, "entryuuid", stringUID(renamedEntryUuid), 30, "The initial entry add failed");

      // disable the domain to make sure that the messages are all sent in a row.
      domain.disable();

      // rename and delete the entry.
      broker.publish(modDNMsg(addDN, "dc=new_name", renamedEntryUuid, 1, gen));
      DN delDN = DN.valueOf("dc=new_name" + "," + TEST_ROOT_DN_STRING);
      broker.publish(delMsg(delDN, renamedEntryUuid, gen));

      // enable back the domain to trigger message replay.
      domain.enable();

      // check that entry does not exist anymore.
      checkEntryHasNoSuchAttributeValue(DN.valueOf("dc=new_name" + "," + TEST_ROOT_DN_STRING), "entryuuid",
          stringUID(renamedEntryUuid), 30, "The delete dependencies was not correctly enforced");
    }
    finally
    {
      remove(replServer);
      if (domain != null)
      {
        MultimasterReplication.deleteDomain(baseDN);
      }
    }
  }

  /**
   * Clean the database and replace with a single entry.
   *
   * @throws FileNotFoundException
   * @throws IOException
   * @throws Exception
   */
  private void cleanDB() throws FileNotFoundException, IOException, Exception
  {
    // Clear backend
    TestCaseUtils.initializeTestBackend(false);

    // Create top entry with uuid
    Entry topEntry = TestCaseUtils.entryFromLdifString(
        "dn:" + TEST_ROOT_DN_STRING + "\n"
         + "objectClass: top\n"
         + "objectClass: organization\n"
         + "o: test\n"
         + "entryuuid: " + stringUID(1) + "\n");

    MemoryBackend memoryBackend = (MemoryBackend) DirectoryServer.getBackend(TEST_BACKEND_ID);
    memoryBackend.addEntry(topEntry, null);
  }


  /**
   * Check that after a sequence of add/del/add done on the same DN
   * the second entry is in the database.
   * The unique id of the entry is used to check that the correct entry
   * has been added.
   * To increase the risks of failures a loop of add/del/add is done.
   */
  @Test(enabled=true, groups="slow")
  public void addDelAddDependencyTest() throws Exception
  {
    ReplicationServer replServer = null;
    LDAPReplicationDomain domain = null;
    DN baseDN = TEST_ROOT_DN;
    int brokerId = 2;
    int serverId = 1;
    int replServerId = 83;
    int addSequenceLength = 30;

    cleanDB();

    try
    {
      Entry entry = TestCaseUtils.entryFromLdifString(
          "dn:" + TEST_ROOT_DN_STRING + "\n"
          + "objectClass: top\n"
          + "objectClass: organization\n");

      replServer = newReplicationServer(replServerId, 5 * addSequenceLength + 100, "dependencyTestAddDelAddDependencyTestDb");

      ReplicationBroker broker = openReplicationSession(
          baseDN, brokerId, 100, replServer.getReplicationPort(), 1000, CLEAN_DB_GENERATION_ID);

      // send a sequence of add/del/add operations
      CSNGenerator gen = new CSNGenerator(brokerId, 0L);

      int sequence;
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        // add the entry a first time
        entry.removeAttribute(getAttributeTypeOrNull("entryuuid"));
        entry.addAttribute(Attributes.create("entryuuid", stringUID(sequence+1)),
                           new LinkedList<ByteString>());
        DN addDN = DN.valueOf("dc=dependency" + sequence + "," + TEST_ROOT_DN_STRING);
        broker.publish(addMsg(addDN, entry, sequence + 1, 1, gen));
        broker.publish(delMsg(addDN, sequence + 1, gen));

        // add again the entry with a new entryuuid.
        entry.removeAttribute(getAttributeTypeOrNull("entryuuid"));
        entry.addAttribute(Attributes.create("entryuuid", stringUID(sequence+1025)),
                           new LinkedList<ByteString>());
        broker.publish(addMsg(addDN, entry, sequence + 1025, 1, gen));
      }

      domain = startNewLDAPReplicationDomain(replServer, baseDN, serverId, -1);

      // check that all entries have been deleted and added
      // again by checking that they do have the correct entryuuid
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        String addDn = "dc=dependency" + sequence + "," + TEST_ROOT_DN_STRING;
        checkEntryHasAttributeValue(DN.valueOf(addDn), "entryuuid", stringUID(sequence + 1025), 30,
            "The second add was not replayed on entry " + addDn);
      }

      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        DN deleteDN = DN.valueOf("dc=dependency" + sequence + "," + TEST_ROOT_DN_STRING);
        broker.publish(delMsg(deleteDN, sequence + 1025, gen));
      }

      // check that the database was cleaned successfully
      DN node1 = DN.valueOf("dc=dependency1," + TEST_ROOT_DN_STRING);
      Entry baseEntry = getEntry(node1, 30000, false);
      assertNull(baseEntry,
        "The entry were not removed succesfully after test completion.");
    }
    finally
    {
      remove(replServer);
      if (domain != null)
      {
        MultimasterReplication.deleteDomain(baseDN);
      }
    }
  }

  private ReplicationServer newReplicationServer(int replServerId, int windowSize, String dirName) throws Exception
  {
    int replServerPort = TestCaseUtils.findFreePort();
    return new ReplicationServer(new ReplServerFakeConfiguration(
        replServerPort, dirName, 0, replServerId, 0, windowSize, null));
  }

  private LDAPReplicationDomain startNewLDAPReplicationDomain(ReplicationServer replServer, DN baseDN, int serverId,
      int heartBeatInterval) throws ConfigException
  {
    SortedSet<String> replServers = newTreeSet("localhost:" + replServer.getReplicationPort());
    DomainFakeCfg domainConf = new DomainFakeCfg(baseDN, serverId, replServers);
    if (heartBeatInterval > 0)
    {
      domainConf.setHeartbeatInterval(heartBeatInterval);
    }
    LDAPReplicationDomain domain = MultimasterReplication.createNewDomain(domainConf);
    domain.start();
    return domain;
  }

  /**
   * Check that the dependency of moddn operation are working by
   * issuing a set of Add operation followed by a modrdn of the added entry.
   */
  @Test(enabled=true, groups="slow")
  public void addModdnDependencyTest() throws Exception
  {
    ReplicationServer replServer = null;
    LDAPReplicationDomain domain = null;
    DN baseDN = TEST_ROOT_DN;
    int brokerId = 2;
    int serverId = 1;
    int replServerId = 84;
    int addSequenceLength = 30;

    cleanDB();

    try
    {
      Entry entry = TestCaseUtils.entryFromLdifString(
          "dn:" + TEST_ROOT_DN_STRING + "\n"
          + "objectClass: top\n"
          + "objectClass: organization\n");

      replServer = newReplicationServer(replServerId, 5 * addSequenceLength + 100, "dependencyTestAddModdnDependencyTestDb");

      ReplicationBroker broker = openReplicationSession(
          baseDN, brokerId, 100, replServer.getReplicationPort(), 1000, CLEAN_DB_GENERATION_ID);


      DN addDN = TEST_ROOT_DN;
      CSNGenerator gen = new CSNGenerator(brokerId, 0L);

      // send a sequence of add/modrdn operations
      int sequence;
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        // add the entry
        entry.removeAttribute(getAttributeTypeOrNull("entryuuid"));
        entry.addAttribute(Attributes.create("entryuuid", stringUID(sequence+1)),
                           new LinkedList<ByteString>());
        addDN = DN.valueOf("dc=dependency" + sequence + "," + TEST_ROOT_DN_STRING);
        broker.publish(addMsg(addDN, entry, sequence + 1, 1, gen));

        // rename the entry
        broker.publish(modDNMsg(addDN, "dc=new_dep" + sequence, sequence + 1, 1, gen));
      }

      // configure and start replication of TEST_ROOT_DN_STRING on the server
      domain = startNewLDAPReplicationDomain(replServer, baseDN, serverId, -1);

      // check that all entries have been renamed
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        addDN = DN.valueOf("dc=new_dep" + sequence + "," + TEST_ROOT_DN_STRING);
        Entry baseEntry = getEntry(addDN, 30000, true);
        assertNotNull(baseEntry,
          "The rename was not applied correctly on :" + addDN);
      }

      // delete the entries to clean the database.
      for (sequence = 1; sequence<=addSequenceLength; sequence ++)
      {
        addDN = DN.valueOf("dc=new_dep" + sequence + "," + TEST_ROOT_DN_STRING);
        broker.publish(delMsg(addDN, sequence + 1, gen));
      }
    }
    finally
    {
      remove(replServer);
      if (domain != null)
      {
        MultimasterReplication.deleteDomain(baseDN);
      }
    }
  }

  /**
   * Builds and return a uuid from an integer.
   * This methods assume that unique integers are used and does not make any
   * unicity checks. It is only responsible for generating a uid with a
   * correct syntax.
   */
  private String stringUID(int i)
  {
    return String.format("11111111-1111-1111-1111-%012x", i);
  }
}
