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

import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.testng.Assert.*;

import java.util.Map;
import java.util.SortedSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.task.Task;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.ReplServerFakeConfiguration;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.replication.service.ReplicationDomain.ImportExportContext;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Test the Generic Replication Service.
 */
@SuppressWarnings("javadoc")
public class ReplicationDomainTest extends ReplicationTestCase
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private static final Task NO_INIT_TASK = null;

  @DataProvider(name = "publishAndReceiveData")
  public Object[][] createpublishAndReceiveData()
  {
    return new Object[][] {
       {1, 2, 3, 4},
       {1, 2, 1, 2},
       {1, 2, 45891, 45672},
       {45610, 45720, 1, 2},
       {45610, 45720, 45891, 45672}
    };
  }

  /**
   * Test that a ReplicationDomain is able to publish and receive UpdateMsg.
   * Also test the ReplicationDomain.resetReplicationLog() method.
   */
  @Test(dataProvider = "publishAndReceiveData", enabled=true)
  public void publishAndReceive(
      int replServerID1, int replServerID2,
      int domain1ServerId, int domain2ServerId)
      throws Exception
  {
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer1 = null;
    ReplicationServer replServer2 = null;
    FakeReplicationDomain domain1 = null;
    FakeReplicationDomain domain2 = null;

    try
    {
      int[] ports = TestCaseUtils.findFreePorts(2);
      int replServerPort1 = ports[0];
      int replServerPort2 = ports[1];

      replServer1 = createReplicationServer(replServerID1, replServerPort1,
          "ReplicationDomainTestDb1", 100, "localhost:" + replServerPort2);
      replServer2 = createReplicationServer(replServerID2, replServerPort2,
          "ReplicationDomainTestDb2", 100, "localhost:" + replServerPort1);

      SortedSet<String> servers = newTreeSet("localhost:" + replServerPort1);
      BlockingQueue<UpdateMsg> rcvQueue1 = new LinkedBlockingQueue<>();
      domain1 = new FakeReplicationDomain(
          testService, domain1ServerId, servers, 100, 1000, rcvQueue1);

      SortedSet<String> servers2 = newTreeSet("localhost:" + replServerPort2);
      BlockingQueue<UpdateMsg> rcvQueue2 = new LinkedBlockingQueue<>();
      domain2 = new FakeReplicationDomain(
          testService, domain2ServerId, servers2, 100, 1000, rcvQueue2);

      Thread.sleep(500);

      /*
       * Publish a message from domain1,
       * Check that domain2 receives it shortly after.
       */
      byte[] test = {1, 2, 3 ,4, 0, 1, 2, 3, 4, 5};
      publish(domain1, test);

      UpdateMsg rcvdMsg = rcvQueue2.poll(20, TimeUnit.SECONDS);
      assertNotNull(rcvdMsg);
      assertEquals(test, rcvdMsg.getPayload());

      for (RSInfo replServerInfo : domain1.getRsInfos())
      {
        // The generation Id of the remote should be 1
        assertEquals(replServerInfo.getGenerationId(), 1,
            "Unexpected value of generationId in RSInfo for RS=" + replServerInfo);
      }

      for (DSInfo serverInfo : domain1.getReplicaInfos().values())
      {
        assertEquals(serverInfo.getStatus(), ServerStatus.NORMAL_STATUS);
      }

      domain1.setGenerationID(2);
      domain1.resetReplicationLog();
      Thread.sleep(500);

      for (RSInfo replServerInfo : domain1.getRsInfos())
      {
        // The generation Id of the remote should now be 2
        assertEquals(replServerInfo.getGenerationId(), 2,
            "Unexpected value of generationId in RSInfo for RS=" + replServerInfo);
      }

      int sleepTime = 50;
      while (true)
      {
        try
        {
          assertExpectedServerStatuses(domain1.getReplicaInfos(),
              domain1ServerId, domain2ServerId);
          assertExpectedServerStatuses(domain2.getReplicaInfos(),
              domain1ServerId, domain2ServerId);

          Map<Integer, ServerState> states1 = domain1.getReplicaStates();
          ServerState state2 = states1.get(domain2ServerId);
          assertNotNull(state2, "getReplicaStates is not showing DS2");

          Map<Integer, ServerState> states2 = domain2.getReplicaStates();
          ServerState state1 = states2.get(domain1ServerId);
          assertNotNull(state1, "getReplicaStates is not showing DS1");

          // if we reach this point all tests are OK
          break;
        }
        catch (AssertionError e)
        {
          if (sleepTime >= 30000)
          {
            throw e;
          }
          Thread.sleep(sleepTime);
          sleepTime *= 2;
        }
      }
    }
    finally
    {
      disable(domain1, domain2);
      remove(replServer1, replServer2);
    }
  }

  /**
   * Publish information to the Replication Service (not assured mode).
   *
   * @param msg  The byte array containing the information that should
   *             be sent to the remote entities.
   */
  void publish(FakeReplicationDomain domain, byte[] msg)
  {
    UpdateMsg updateMsg;
    synchronized (this)
    {
      updateMsg = new UpdateMsg(domain.getGenerator().newCSN(), msg);
      // If assured replication is configured,
      // this will prepare blocking mechanism.
      // If assured replication is disabled, this returns immediately
      domain.prepareWaitForAckIfAssuredEnabled(updateMsg);
      domain.publish(updateMsg);
    }

    try
    {
      // If assured replication is enabled,
      // this will wait for the matching ack or time out.
      // If assured replication is disabled, this returns immediately
      domain.waitForAckIfAssuredEnabled(updateMsg);
    }
    catch (TimeoutException ex)
    {
      // This exception may only be raised if assured replication is enabled
      logger.info(NOTE_DS_ACK_TIMEOUT, domain.getBaseDN(), domain.getAssuredTimeout(), updateMsg);
    }
  }

  private void assertExpectedServerStatuses(Map<Integer, DSInfo> dsInfos,
      int domain1ServerId, int domain2ServerId)
  {
    for (DSInfo serverInfo : dsInfos.values())
    {
      if (serverInfo.getDsId() == domain2ServerId)
      {
        assertEquals(serverInfo.getStatus(), ServerStatus.BAD_GEN_ID_STATUS);
      }
      else
      {
        assertEquals(serverInfo.getDsId(), domain1ServerId);
        assertEquals(serverInfo.getStatus(), ServerStatus.NORMAL_STATUS);
      }
    }
  }

  /**
   * Publish performance test.
   * The test loops calling the publish methods of the ReplicationDomain.
   * It should not be enabled by default as it will use a lot of time.
   * Its call is only to investigate performance issues with the replication.
   */
  @Test(enabled=false)
  public void publishPerf() throws Exception
  {
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer1 = null;
    int replServerID1 = 10;
    FakeReplicationDomain domain1 = null;
    int domain1ServerId = 1;

    try
    {
      int replServerPort = TestCaseUtils.findFreePort();

      replServer1 = createReplicationServer(replServerID1, replServerPort,
          "ReplicationDomainTestDb", 100000, "localhost:" + replServerPort);

      SortedSet<String> servers = newTreeSet("localhost:" + replServerPort);
      BlockingQueue<UpdateMsg> rcvQueue1 = new LinkedBlockingQueue<>();
      domain1 = new FakeReplicationDomain(
          testService, domain1ServerId, servers, 1000, 100000, rcvQueue1);


      /*
       * Publish a message from domain1,
       * Check that domain2 receives it shortly after.
       */
      byte[] test = {1, 2, 3 ,4, 0, 1, 2, 3, 4, 5};

      long timeNow = System.nanoTime();
      timeNow = publishRepeatedly(domain1, test, timeNow);
      timeNow = publishRepeatedly(domain1, test, timeNow);
      timeNow = publishRepeatedly(domain1, test, timeNow);
      timeNow = publishRepeatedly(domain1, test, timeNow);
    }
    finally
    {
      disable(domain1);
      remove(replServer1);
    }
  }

  private long publishRepeatedly(FakeReplicationDomain domain1, byte[] test, long timeNow)
  {
    long timeStart = timeNow;
    for (int i = 0; i < 100000; i++)
    {
      publish(domain1, test);
    }
    timeNow = System.nanoTime();
    System.out.println(timeNow - timeStart);
    return timeNow;
  }

  private ReplicationServer createReplicationServer(int serverId,
      int replicationPort, String dirName, int windowSize,
      String... replServers) throws Exception
  {
    return createReplicationServer(serverId, replicationPort, dirName, windowSize, newTreeSet(replServers));
  }

  private ReplicationServer createReplicationServer(int serverId,
      int replicationPort, String dirName, int windowSize,
      SortedSet<String> replServers) throws Exception
  {
    return new ReplicationServer(
        new ReplServerFakeConfiguration(replicationPort, dirName, 0, serverId, 0, windowSize, replServers));
  }

  private void disable(ReplicationDomain... domains)
  {
    for (ReplicationDomain domain : domains)
    {
      if (domain != null)
      {
        domain.disableService();
      }
    }
  }

  @DataProvider(name = "exportAndImportData")
  public Object[][] createExportAndimportData()
  {
    return new Object[][] {
       {1, 2},
       {45610, 45720}
    };
  }

  /**
   * Test that a ReplicationDomain is able to export and import its database
   * When there is only one replication server.
   */
  @Test(dataProvider = "exportAndImportData", enabled=true)
  public void exportAndImport(int serverId1, int serverId2) throws Exception
  {
    final int ENTRYCOUNT=5000;
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer = null;
    int replServerID = 11;
    FakeReplicationDomain domain1 = null;
    FakeReplicationDomain domain2 = null;

    try
    {
      int replServerPort = TestCaseUtils.findFreePort();

      replServer = createReplicationServer(replServerID, replServerPort,
          "exportAndImportData", 100);
      SortedSet<String> servers = newTreeSet("localhost:" + replServerPort);

      String exportedData = buildExportedData(ENTRYCOUNT);
      domain1 = new FakeReplicationDomain(
          testService, serverId1, servers, 0, exportedData, null, ENTRYCOUNT);

      StringBuilder importedData = new StringBuilder();
      domain2 = new FakeReplicationDomain(
          testService, serverId2, servers, 0, null, importedData, 0);

      /*
       * Trigger a total update from domain1 to domain2.
       * Check that the exported data is correctly received on domain2.
       */
      assertTrue(initializeFromRemote(domain2));
      waitEndExport(exportedData, importedData);
      assertExportSucessful(domain1, domain2, exportedData, importedData);
    }
    finally
    {
      disable(domain1, domain2);
      remove(replServer);
    }
  }

  private boolean initializeFromRemote(ReplicationDomain domain) throws DirectoryException
  {
    for (DSInfo remoteDS : domain.getReplicaInfos().values())
    {
      if (remoteDS.getDsId() != domain.getServerId())
      {
        domain.initializeFromRemote(remoteDS.getDsId(), NO_INIT_TASK);
        return true;
      }
    }
    return false;
  }

  /**
   * Test that a ReplicationDomain is able to export and import its database
   * across 2 replication servers.
   */
  @Test(enabled=true)
  public void exportAndImportAcross2ReplServers() throws Exception
  {
    final int ENTRYCOUNT=5000;
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer2 = null;
    ReplicationServer replServer1 = null;
    int replServerID = 11;
    int replServerID2 = 12;
    FakeReplicationDomain domain1 = null;
    FakeReplicationDomain domain2 = null;

    try
    {
      int[] ports = TestCaseUtils.findFreePorts(2);
      int replServerPort1 = ports[0];
      int replServerPort2 = ports[1];

      replServer1 = createReplicationServer(replServerID, replServerPort1,
          "exportAndImportservice1", 100);
      replServer2 = createReplicationServer(replServerID2, replServerPort2,
          "exportAndImportservice2", 100, "localhost:" + replServerPort1);

      SortedSet<String> servers1 = newTreeSet("localhost:" + replServerPort1);
      SortedSet<String> servers2 = newTreeSet("localhost:" + replServerPort2);

      String exportedData = buildExportedData(ENTRYCOUNT);
      domain1 = new FakeReplicationDomain(
          testService, 1, servers1, 0, exportedData, null, ENTRYCOUNT);

      StringBuilder importedData = new StringBuilder();
      domain2 = new FakeReplicationDomain(
          testService, 2, servers2, 0, null, importedData, 0);

      domain2.initializeFromRemote(1, NO_INIT_TASK);

      waitEndExport(exportedData, importedData);
      assertExportSucessful(domain1, domain2, exportedData, importedData);
    }
    finally
    {
      disable(domain1, domain2);
      remove(replServer1, replServer2);
    }
  }

  private String buildExportedData(final int ENTRYCOUNT)
  {
    final StringBuilder sb = new StringBuilder();
    for (int i = 0; i < ENTRYCOUNT; i++)
    {
      sb.append("key : value").append(i).append("\n\n");
    }
    return sb.toString();
  }

  private void waitEndExport(String exportedData, StringBuilder importedData) throws Exception
  {
    int count = 0;
    while (importedData.length() < exportedData.length() && count < 500)
    {
      count ++;
      Thread.sleep(100);
    }
  }

  private void assertExportSucessful(ReplicationDomain domain1,
      ReplicationDomain domain2, String exportedData, StringBuilder importedData)
  {
    assertEquals(getLeftEntryCount(domain2), 0, "Wrong LeftEntryCount for export");
    assertEquals(getLeftEntryCount(domain1), 0, "Wrong LeftEntryCount for import");
    assertEquals(importedData.length(), exportedData.length());
    assertEquals(importedData.toString(), exportedData);
  }

  private long getLeftEntryCount(ReplicationDomain domain)
  {
    final ImportExportContext ieContext = domain.getImportExportContext();
    if (ieContext != null)
    {
      return ieContext.getLeftEntryCount();
    }
    return 0; // import/export is finished
  }

  /**
   * Sender side of the Total Update Perf test.
   * The goal of this test is to measure the performance
   * of the total update code.
   * It is not intended to be run as part of the daily unit test but
   * should only be used manually by developer in need of testing the
   * performance improvement or non-regression of the total update code.
   * Use this test in combination with the receiverInitialize() :
   *   - enable the test
   *   - start the senderInitialize first using
   *     ./build.sh \
   *        -Dorg.opends.test.suppressOutput=false \
   *        -Dtest.methods=org.opends.server.replication.service.ReplicationDomainTest.senderInitialize test
   *   - start the receiverInitialize second.
   *   - you may want to change  HOST1 and HOST2 to use 2 different hosts
   *     if you don't want to do a loopback test.
   *   - don't forget to disable again the tests after running them
   */
  final String HOST1 = "localhost:";
  final String HOST2 = "localhost:";
  final int SENDERPORT = 10102;
  final int RECEIVERPORT = 10101;

  @Test(enabled=false)
  public void senderInitialize() throws Exception
  {
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer = null;
    int replServerID = 12;
    FakeStressReplicationDomain domain1 = null;

    try
    {
      SortedSet<String> servers =
          newTreeSet(HOST1 + SENDERPORT, HOST2 + RECEIVERPORT);

      replServer = createReplicationServer(replServerID, SENDERPORT,
          "ReplicationDomainTestDb", 100, servers);

      BlockingQueue<UpdateMsg> rcvQueue1 = new LinkedBlockingQueue<>();
      domain1 = new FakeStressReplicationDomain(
          testService, 2, servers, 1000, rcvQueue1);

      System.out.println("waiting");
      Thread.sleep(1000000000);
    }
    finally
    {
      disable(domain1);
      remove(replServer);
    }
  }

  /**
   * See comments in senderInitialize() above.
   */
  @Test(enabled=false)
  public void receiverInitialize() throws Exception
  {
    DN testService = DN.valueOf("o=test");
    ReplicationServer replServer = null;
    int replServerID = 11;
    FakeStressReplicationDomain domain1 = null;

    try
    {
      SortedSet<String> servers =
          newTreeSet(HOST1 + SENDERPORT, HOST2 + RECEIVERPORT);

      replServer = createReplicationServer(replServerID, RECEIVERPORT,
          "ReplicationDomainTestDb", 100, servers);

      BlockingQueue<UpdateMsg> rcvQueue1 = new LinkedBlockingQueue<>();
      domain1 = new FakeStressReplicationDomain(
          testService, 1, servers, 100000, rcvQueue1);
      /*
       * Trigger a total update from domain1 to domain2.
       * Check that the exported data is correctly received on domain2.
       */
      while (!initializeFromRemote(domain1))
      {
        System.out.println("trying...");
        Thread.sleep(1000);
      }
      System.out.println("waiting");
      Thread.sleep(10000000);
    }
    finally
    {
      disable(domain1);
      remove(replServer);
    }
  }

}
