/*
 * 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.plugin;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.*;

import org.assertj.core.api.Assertions;
import org.assertj.core.data.MapEntry;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.*;
import org.opends.server.replication.protocol.*;
import org.opends.server.types.*;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.assertj.core.data.MapEntry.*;
import static org.opends.server.TestCaseUtils.*;
import static org.opends.server.protocols.internal.Requests.*;
import static org.testng.Assert.*;

/**
 * Test the client part (plugin) of the assured feature in both safe data and
 * safe read modes. We use a fake RS and control the behaviour of the client
 * DS (timeout, wait for acks, error handling...)
 * Also check for monitoring values for assured replication
 */
@SuppressWarnings("javadoc")
public class AssuredReplicationPluginTest extends ReplicationTestCase
{
  public class MonitorAssertions
  {
    private Map<String, Long> attributeValues = new HashMap<>();

    public MonitorAssertions(DN baseDN) throws Exception
    {
      List<String> attributes = Arrays.asList(
          "assured-sr-sent-updates",               "assured-sr-acknowledged-updates",
          "assured-sr-not-acknowledged-updates",   "assured-sr-timeout-updates",
          "assured-sr-wrong-status-updates",       "assured-sr-replay-error-updates",
          "assured-sr-received-updates",           "assured-sr-received-updates-acked",
          "assured-sr-received-updates-not-acked", "assured-sd-sent-updates",
          "assured-sd-acknowledged-updates",       "assured-sd-timeout-updates");
      for (String attribute : attributes)
      {
        attributeValues.put(attribute, getMonitorAttrValue(baseDN, attribute));
      }
    }

    public MonitorAssertions assertValue(String attribute, long expected)
    {
      assertEquals(attributeValues.remove(attribute), (Long) expected);
      return this;
    }

    public void assertRemainingValuesAreZero()
    {
      Set<String> keySet;
      while (!(keySet = attributeValues.keySet()).isEmpty())
      {
        assertValue(keySet.iterator().next(), 0L);
      }
    }
  }

  /** The port of the replicationServer. */
  private int replServerPort;
  private final int RS_SERVER_ID = 90;
  /** Sleep time of the RS, before sending an ack. */
  private static final long NO_TIMEOUT_RS_SLEEP_TIME = 2000;
  private final String testName = getClass().getSimpleName();

  /**
   * Create two distinct base dns, one for safe data replication, the other one
   * for safe read replication.
   */
  private final String SAFE_DATA_DN = "ou=safe-data," + TEST_ROOT_DN_STRING;
  private final String SAFE_READ_DN = "ou=safe-read," + TEST_ROOT_DN_STRING;
  private final String NOT_ASSURED_DN = "ou=not-assured," + TEST_ROOT_DN_STRING;
  private Entry safeDataDomainCfgEntry;
  private Entry safeReadDomainCfgEntry;
  private Entry notAssuredDomainCfgEntry;

  /** The fake replication server. */
  private FakeReplicationServer replicationServer;

  /** Definitions for the scenario the RS supports. */
  private static final int NOT_ASSURED_SCENARIO = 1;
  private static final int TIMEOUT_SCENARIO = 2;
  private static final int NO_TIMEOUT_SCENARIO = 3;
  private static final int SAFE_READ_MANY_ERRORS = 4;
  private static final int SAFE_DATA_MANY_ERRORS = 5;
  private static final int NO_READ = 6;

  /** The tracer object for the debug logger. */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  private void debugInfo(String s)
  {
    logger.error(LocalizableMessage.raw(s));
    if (logger.isTraceEnabled())
    {
      logger.trace("** TEST **" + s);
    }
  }

  /** Before starting the tests configure some stuff. */
  @BeforeClass
  @Override
  public void setUp() throws Exception
  {
    super.setUp();

    replServerPort = TestCaseUtils.findFreePort();

    // Create base dns for each tested modes
    String topEntry = "dn: " + SAFE_DATA_DN + "\n" + "objectClass: top\n" +
      "objectClass: organizationalUnit\n";
    addEntry(TestCaseUtils.entryFromLdifString(topEntry));
    topEntry = "dn: " + SAFE_READ_DN + "\n" + "objectClass: top\n" +
      "objectClass: organizationalUnit\n";
    addEntry(TestCaseUtils.entryFromLdifString(topEntry));
    topEntry = "dn: " + NOT_ASSURED_DN + "\n" + "objectClass: top\n" +
      "objectClass: organizationalUnit\n";
    addEntry(TestCaseUtils.entryFromLdifString(topEntry));
  }

  /** Add an entry in the database. */
  private void addEntry(Entry entry) throws Exception
  {
    debugInfo("AddEntry " + entry.getName());
    AddOperation addOp = connection.processAdd(entry);
    waitOpResult(addOp, ResultCode.SUCCESS);
    assertNotNull(getEntry(entry.getName(), 1000, true));
  }

  /**
   * Creates a domain using the passed assured settings.
   * Returns the matching config entry added to the config backend.
   */
  private Entry createAssuredDomain(AssuredMode assuredMode, int safeDataLevel,
    long assuredTimeout) throws Exception
  {
    String baseDn = null;
    switch (assuredMode)
    {
      case SAFE_READ_MODE:
        baseDn = SAFE_READ_DN;
        break;
      case SAFE_DATA_MODE:
        baseDn = SAFE_DATA_DN;
        break;
      default:
        fail("Unexpected assured level.");
    }
    // Create an assured config entry ldif, matching passed assured settings
    String prefixLdif = "dn: cn=" + testName + ", cn=domains, " +
      SYNCHRO_PLUGIN_DN + "\n" + "objectClass: top\n" +
      "objectClass: ds-cfg-replication-domain\n" + "cn: " + testName + "\n" +
      "ds-cfg-base-dn: " + baseDn + "\n" +
      "ds-cfg-replication-server: localhost:" + replServerPort + "\n" +
      "ds-cfg-server-id: 1\n" + "ds-cfg-receive-status: true\n" +
      // heartbeat = 10 min so no need to emulate heartbeat in fake RS: session
      // not closed by client
      "ds-cfg-heartbeat-interval: 600000ms\n" +
      // heartbeat = 10 min so no need to emulate heartbeat in fake RS: session
      // not closed by client
     "ds-cfg-changetime-heartbeat-interval: 0ms\n";

    String configEntryLdif = null;
    switch (assuredMode)
    {
      case SAFE_READ_MODE:
        configEntryLdif = prefixLdif + "ds-cfg-assured-type: safe-read\n" +
          "ds-cfg-assured-timeout: " + assuredTimeout + "ms\n";
        break;
      case SAFE_DATA_MODE:
        configEntryLdif = prefixLdif + "ds-cfg-assured-type: safe-data\n" +
          "ds-cfg-assured-sd-level: " + safeDataLevel + "\n" +
          "ds-cfg-assured-timeout: " + assuredTimeout + "ms\n";
        break;
      default:
        fail("Unexpected assured level.");
    }
    Entry domainCfgEntry = TestCaseUtils.entryFromLdifString(configEntryLdif);

    // Add the config entry to create the replicated domain
    DirectoryServer.getConfigHandler().addEntry(domainCfgEntry, null);
    assertNotNull(DirectoryServer.getConfigEntry(domainCfgEntry.getName()),
      "Unable to add the domain config entry: " + configEntryLdif);

    return domainCfgEntry;
  }

  /**
   * Creates a domain without assured mode
   * Returns the matching config entry added to the config backend.
   */
  private Entry createNotAssuredDomain() throws Exception
  {
    // Create a not assured config entry ldif
    String configEntryLdif = "dn: cn=" + testName + ", cn=domains, " +
      SYNCHRO_PLUGIN_DN + "\n" + "objectClass: top\n" +
      "objectClass: ds-cfg-replication-domain\n" + "cn: " + testName + "\n" +
      "ds-cfg-base-dn: " + NOT_ASSURED_DN + "\n" +
      "ds-cfg-replication-server: localhost:" + replServerPort + "\n" +
      "ds-cfg-server-id: 1\n" + "ds-cfg-receive-status: true\n" +
      // heartbeat = 10 min so no need to emulate heartbeat in fake RS: session
      // not closed by client
      "ds-cfg-heartbeat-interval: 600000ms\n" +
      "ds-cfg-changetime-heartbeat-interval: 0ms\n";

    Entry domainCfgEntry = TestCaseUtils.entryFromLdifString(configEntryLdif);

    // Add the config entry to create the replicated domain
    DirectoryServer.getConfigHandler().addEntry(domainCfgEntry, null);
    assertNotNull(DirectoryServer.getConfigEntry(domainCfgEntry.getName()),
      "Unable to add the domain config entry: " + configEntryLdif);

    return domainCfgEntry;
  }

  /**
   * The fake replication server used to emulate RS behaviour the way we want
   * for assured features test.
   * This fake replication server is able to receive a DS connection only.
   * According to the configured scenario, it will answer to updates with acks
   * as the scenario is requesting.
   */
  private class FakeReplicationServer extends Thread
  {
    private ServerSocket listenSocket;
    private boolean shutdown;
    private Session session;

    /** Parameters given at constructor time. */
    private final int port;
    private int serverId = -1;
    private boolean isAssured;
    private AssuredMode assuredMode = AssuredMode.SAFE_DATA_MODE;
    private byte safeDataLevel = 1;

    /** Predefined config parameters. */
    private final int degradedStatusThreshold = 5000;

    /** Parameters set with received server start message. */
    private DN baseDN;
    private long generationId = -1L;
    private ServerState serverState;
    private int windowSize = -1;
    private byte groupId = -1;
    private boolean sslEncryption;
    /** The scenario this RS is expecting. */
    private int scenario = -1;

    /** Parameters at handshake are ok. */
    private boolean handshakeOk;
    /**
     * Signal that the current scenario the RS must execute reached the point
     * where the main code can perform test assertion.
     */
    private boolean scenarioExecuted;

    private CSNGenerator gen;
    private String testcase;

    /**
     * Constructor for RS receiving updates in SR assured mode or not assured
     * The assured boolean means:
     * - true: SR mode
     * - false: not assured
     */
    public FakeReplicationServer(byte groupId, int port, int serverId, boolean assured,
        String testcase)
    {
      this.groupId = groupId;
      this.port = port;
      this.serverId = serverId;
      this.testcase = testcase;

      if (assured)
      {
        this.isAssured = true;
      }
      this.assuredMode = AssuredMode.SAFE_READ_MODE;
    }

    /** Constructor for RS receiving updates in SD assured mode. */
    public FakeReplicationServer(byte groupId, int port, int serverId, int safeDataLevel,
        String testcase)
    {
      this.groupId = groupId;
      this.port = port;
      this.serverId = serverId;

      this.isAssured = true;
      this.assuredMode = AssuredMode.SAFE_DATA_MODE;
      this.safeDataLevel = (byte) safeDataLevel;
      this.testcase = testcase;
    }

    public void setAssured(boolean assured)
    {
      this.isAssured = assured;
    }

    /** Starts the fake RS, expecting and testing the passed scenario. */
    public void start(int scenario)
    {
      gen = new CSNGenerator(3, 0L);

      // Store expected test case
      this.scenario = scenario;

      // Start listening
      start();
    }

    /** Wait for DS connections. */
    @Override
    public void run()
    {
      // Create server socket
      try
      {
        listenSocket = new ServerSocket();
        listenSocket.bind(new InetSocketAddress(port));
      } catch (IOException e)
      {
        fail("Fake replication server could not bind to port:" + port);
      }

      Socket newSocket = null;
      // Loop waiting for DS connections
      while (!shutdown)
      {
        try
        {
          newSocket = listenSocket.accept();
          newSocket.setTcpNoDelay(true);
          newSocket.setKeepAlive(true);
          // Create client session
          ReplSessionSecurity replSessionSecurity = new ReplSessionSecurity();
          int timeoutMS = MultimasterReplication.getConnectionTimeoutMS();
          session = replSessionSecurity.createServerSession(newSocket,
              timeoutMS);
          if (session == null) // Error, go back to accept
          {
            continue;
          }
          // Ok, now call connection handling code + special code for the
          // configured test
          handleClientConnection();
        } catch (Exception e)
        {
          // The socket has probably been closed as part of the
          // shutdown
        }
      }
    }

    /** Shutdown the Replication Server service and all its connections. */
    public void shutdown()
    {
      if (shutdown)
      {
        return;
      }

      shutdown = true;

      // Shutdown the listener thread and any current client handling code
      StaticUtils.close(listenSocket);
      StaticUtils.close(session);

      try
      {
        join();
      } catch (InterruptedException ie)
      {
      }
    }

    /**
     * Handle the handshake processing with the connecting DS
     * returns true if handshake was performed without errors.
     */
    private boolean performHandshake() throws Exception
    {
      {
        // Receive server start
        ServerStartMsg serverStartMsg = waitForSpecificMsg(session, ServerStartMsg.class);

        baseDN = serverStartMsg.getBaseDN();
        serverState = serverStartMsg.getServerState();
        generationId = serverStartMsg.getGenerationId();
        windowSize = serverStartMsg.getWindowSize();
        sslEncryption = serverStartMsg.getSSLEncryption();

        // Send replication server start
        String serverURL = "localhost:" + port;
        ReplServerStartMsg replServerStartMsg = new ReplServerStartMsg(serverId,
          serverURL, baseDN, windowSize, serverState, generationId, sslEncryption,
          groupId, degradedStatusThreshold);
        session.publish(replServerStartMsg);

        if (!sslEncryption)
        {
          session.stopEncryption();
        }

        ReplicationMsg msg = session.receive();
        if (msg instanceof StopMsg){
          // Disconnection of DS looking for best server
          return false;
        }

        StartSessionMsg startSessionMsg = (StartSessionMsg)msg;

        // Sanity checking for assured parameters
        assertEquals(startSessionMsg.isAssured(), isAssured);
        if (isAssured)
        {
          assertEquals(startSessionMsg.getAssuredMode(), assuredMode);
          assertEquals(startSessionMsg.getSafeDataLevel(), safeDataLevel);
        }
        assertEquals(startSessionMsg.getStatus(), ServerStatus.NORMAL_STATUS);
        Assertions.assertThat(startSessionMsg.getReferralsURLs()).isEmpty();

        debugInfo("Received start session assured parameters are ok.");

        // Send topo view
        RSInfo rsInfo = new RSInfo(serverId, "localhost:" + port, generationId, groupId, 1);
        session.publish(new TopologyMsg(new ArrayList<DSInfo>(), Arrays.asList(rsInfo)));
      }
      return true;
    }

    /**
     * Tells if the received handshake parameters regarding assured config were
     * ok and handshake phase is terminated.
     */
    public boolean isHandshakeOk()
    {
      return handshakeOk;
    }

    /**
     * Tells the main code that the fake RS executed enough of the expected
     * scenario and can perform test assertion.
     */
    public boolean isScenarioExecuted()
    {
      return scenarioExecuted;
    }

    /** Handle client connection then call code specific to configured test. */
    private void handleClientConnection() throws Exception
    {
      debugInfo("handleClientConnection " + testcase + " " + scenario);
      // Handle DS connection
      if (!performHandshake())
      {
        session.close();
        return;
      }
      // If we come here, assured parameters sent by DS are as expected and
      // handshake phase is terminated
      handshakeOk = true;

      // Now execute the requested scenario
      switch (scenario)
      {
        case NOT_ASSURED_SCENARIO:
          executeNotAssuredScenario();
          break;
        case TIMEOUT_SCENARIO:
          executeTimeoutScenario();
          break;
        case NO_TIMEOUT_SCENARIO:
          executeNoTimeoutScenario();
          break;
        case SAFE_READ_MANY_ERRORS:
          executeSafeReadManyErrorsScenario();
          break;
        case SAFE_DATA_MANY_ERRORS:
          executeSafeDataManyErrorsScenario();
          break;
        case NO_READ:
          // Nothing to execute, just let session opne. This scenario used to
          // send updates from the RS to the DS (reply test cases)
          while (!shutdown)
          {
            try
            {
              sleep(5000);
            } catch (InterruptedException ex)
            {
              // Going shutdown ?
              break;
            }
          }
          break;
        default:
          fail("Unknown scenario: " + scenario);
      }
      debugInfo("handleClientConnection " + testcase + " " + scenario + " done");
    }

    /**
     * Make the RS send an add message with the passed entry and return the ack
     * message it receives from the DS.
     */
    private AckMsg sendAssuredAddMsg(Entry entry, String parentUid) throws Exception
    {
      AddMsg addMsg = new AddMsg(
          gen.newCSN(), entry.getName(), UUID.randomUUID().toString(),
          parentUid, entry.getObjectClassAttribute(), entry.getAttributes(), null);

      // Send add message in assured mode
      addMsg.setAssured(isAssured);
      addMsg.setAssuredMode(assuredMode);
      addMsg.setSafeDataLevel(safeDataLevel);
      session.publish(addMsg);

      return waitForSpecificMsg(session, AckMsg.class);
    }

    /** Read the coming update and check parameters are not assured. */
    private void executeNotAssuredScenario() throws Exception
    {
      checkAssuredParametersOnReceivedUpdateMsg();

      scenarioExecuted = true;
    }

    /** Read the coming update and make the client time out by not sending back the ack. */
    private void executeTimeoutScenario() throws Exception
    {
      checkAssuredParametersOnReceivedUpdateMsg();

      scenarioExecuted = true;

      // We do not send back an ack and the client code is expected to be
      // blocked at least for the programmed timeout time.
    }

    /** Read the coming update, sleep some time then send back an ack. */
    private void executeNoTimeoutScenario() throws Exception
    {
      UpdateMsg updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // Sleep before sending back the ack
      sleep(NO_TIMEOUT_RS_SLEEP_TIME);

      // Send the ack without errors
      AckMsg ackMsg = new AckMsg(updateMsg.getCSN());
      session.publish(ackMsg);

      scenarioExecuted = true;
    }

    /**
     * Receives an {@link UpdateMsg} and checks that received update assured
     * parameters are as defined at RS start.
     */
    private UpdateMsg checkAssuredParametersOnReceivedUpdateMsg() throws Exception
    {
      final UpdateMsg updateMsg = (UpdateMsg) session.receive();

      assertEquals(updateMsg.isAssured(), isAssured,
          "msg=" + ((updateMsg instanceof AddMsg)?
              ((AddMsg)updateMsg).getDN():updateMsg.getCSN()));
      if (isAssured)
      {
        assertEquals(updateMsg.getAssuredMode(), assuredMode);
        assertEquals(updateMsg.getSafeDataLevel(), safeDataLevel);
      }
      debugInfo("Received update assured parameters are ok.");

      return updateMsg;
    }

    /** Read the coming safe read mode updates and send back acks with errors. */
    private void executeSafeReadManyErrorsScenario() throws Exception
    {
      // Read first update
      UpdateMsg updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // Sleep before sending back the ack
      sleep(NO_TIMEOUT_RS_SLEEP_TIME);

      // Send an ack with errors:
      // - replay error
      // - server 10 error, server 20 error
      List<Integer> serversInError = Arrays.asList(10, 20);
      session.publish(new AckMsg(updateMsg.getCSN(), false, false, true, serversInError));

      // Read second update
      updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // Sleep before sending back the ack
      sleep(NO_TIMEOUT_RS_SLEEP_TIME);

      // Send an ack with errors:
      // - timeout error
      // - wrong status error
      // - replay error
      // - server 10 error, server 20 error, server 30 error
      serversInError = Arrays.asList(10, 20, 30);
      session.publish(new AckMsg(updateMsg.getCSN(), true, true, true, serversInError));

      // Read third update
      updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // let timeout occur
      scenarioExecuted = true;
    }

    /** Read the coming safe data mode updates and send back acks with errors. */
    private void executeSafeDataManyErrorsScenario() throws Exception
    {
      // Read first update
      UpdateMsg updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // Sleep before sending back the ack
      sleep(NO_TIMEOUT_RS_SLEEP_TIME);

      // Send an ack with errors:
      // - timeout error
      // - server 10 error
      List<Integer> serversInError = Arrays.asList(10);
      session.publish(new AckMsg(updateMsg.getCSN(), true, false, false, serversInError));

      // Read second update
      updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // Sleep before sending back the ack
      sleep(NO_TIMEOUT_RS_SLEEP_TIME);

      // Send an ack with errors:
      // - timeout error
      // - server 10 error, server 20 error
      serversInError = Arrays.asList(10, 20);
      session.publish(new AckMsg(updateMsg.getCSN(), true, false, false, serversInError));

      // Read third update
      updateMsg = checkAssuredParametersOnReceivedUpdateMsg();

      // let timeout occur
      scenarioExecuted = true;
    }
  }

  /** Return various group id values. */
  @DataProvider(name = "rsGroupIdProvider")
  private Object[][] rsGroupIdProvider()
  {
    return new Object[][]
    {
    { (byte)1 },
    { (byte)2 }
    };
  }

  /**
   * Tests that a DS performing a modification in safe data mode waits for
   * the ack of the RS for the configured timeout time, then times out.
   * If the RS group id is not the same as the DS one, this must not time out
   * and return immediately.
   */
  @Test(dataProvider = "rsGroupIdProvider")
  public void testSafeDataModeTimeout(byte rsGroupId) throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeDataModeTimeout" + rsGroupId;
    try
    {
      // Create and start a RS expecting clients in safe data assured mode with
      // safe data level 2
      replicationServer = new FakeReplicationServer(rsGroupId, replServerPort, RS_SERVER_ID,
        1, testcase);
      if (rsGroupId != 1)
      {
        replicationServer.setAssured(false);
      }
      replicationServer.start(TIMEOUT_SCENARIO);

      long startTime;
      // Create a safe data assured domain
      if (rsGroupId == 1)
      {
        safeDataDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_DATA_MODE, 1, TIMEOUT);
        // Wait for connection of domain to RS
        waitForConnectionToRs(testcase, replicationServer);

        // Make an LDAP update (add an entry)
        startTime = System.currentTimeMillis(); // Time the update has been initiated
        String entry = "dn: ou=assured-sd-timeout-entry" + rsGroupId + "," + SAFE_DATA_DN + "\n" +
          "objectClass: top\n" +
          "objectClass: organizationalUnit\n";
        addEntry(TestCaseUtils.entryFromLdifString(entry));
      }
      else
      {
        safeDataDomainCfgEntry = createNotAssuredDomain();
        // Wait for connection of domain to RS
        waitForConnectionToRs(testcase, replicationServer);

        // Make an LDAP update (add an entry)
        startTime = System.currentTimeMillis(); // Time the update has been initiated
        String entry = "dn: ou=assured-sd-timeout-entry" + rsGroupId + "," + NOT_ASSURED_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
        addEntry(TestCaseUtils.entryFromLdifString(entry));
      }
      long endTime = System.currentTimeMillis();

      waitForScenarioExecutedOnRs(testcase, replicationServer);

      if (rsGroupId == 1)
      {
        // RS has same group id as DS
        assertBlockedLongerThanTimeout(startTime, endTime, TIMEOUT);
        assertTrue(replicationServer.isScenarioExecuted());

        // Check monitoring values
        Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
        DN baseDN = DN.valueOf(SAFE_DATA_DN);
        new MonitorAssertions(baseDN)
          .assertValue("assured-sd-sent-updates", 1)
          .assertValue("assured-sd-timeout-updates", 1)
          .assertRemainingValuesAreZero();
        assertServerErrorsSafeDataMode(baseDN, entry(RS_SERVER_ID, 1));
      }
      else
      {
        // RS has a different group id, addEntry should have returned quickly
        assertTrue(endTime - startTime < 3000);

        // No error should be seen in monitoring and update should have not been
        // sent in assured mode
        Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
        DN baseDN = DN.valueOf(NOT_ASSURED_DN);
        new MonitorAssertions(baseDN).assertRemainingValuesAreZero();
        assertNoServerErrors(baseDN);
      }
    } finally
    {
      endTest(testcase);
    }
  }

  private void assertNoServerErrors(DN baseDN) throws Exception
  {
    assertTrue(getErrorsByServers(baseDN, AssuredMode.SAFE_READ_MODE).isEmpty());
    assertTrue(getErrorsByServers(baseDN, AssuredMode.SAFE_DATA_MODE).isEmpty());
  }

  private void assertServerErrorsSafeDataMode(DN baseDN, MapEntry... entries) throws Exception
  {
    assertTrue(getErrorsByServers(baseDN, AssuredMode.SAFE_READ_MODE).isEmpty());

    Map<Integer, Integer> errorsByServer = getErrorsByServers(baseDN, AssuredMode.SAFE_DATA_MODE);
    Assertions.assertThat(errorsByServer).hasSize(entries.length);
    Assertions.assertThat(errorsByServer).contains(entries);
  }

  private void assertServerErrorsSafeReadMode(DN baseDN, MapEntry... entries) throws Exception
  {
    assertTrue(getErrorsByServers(baseDN, AssuredMode.SAFE_DATA_MODE).isEmpty());

    Map<Integer, Integer> errorsByServer = getErrorsByServers(baseDN, AssuredMode.SAFE_READ_MODE);
    Assertions.assertThat(errorsByServer).hasSize(entries.length);
    Assertions.assertThat(errorsByServer).contains(entries);
  }

  /**
   * Tests that a DS performing a modification in safe read mode waits for
   * the ack of the RS for the configured timeout time, then times out.
   * If the RS group id is not the same as the DS one, this must not time out
   * and return immediately.
   */
  @Test(dataProvider = "rsGroupIdProvider")
  public void testSafeReadModeTimeout(byte rsGroupId) throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeReadModeTimeout" + rsGroupId;
    try
    {
      // Create and start a RS expecting clients in safe read assured mode
      replicationServer = new FakeReplicationServer(rsGroupId, replServerPort, RS_SERVER_ID,
        true, testcase);
      if (rsGroupId != 1)
      {
        replicationServer.setAssured(false);
      }
      replicationServer.start(TIMEOUT_SCENARIO);

      long startTime;

      // Create a safe data assured domain
      if (rsGroupId == 1)
      {
        // Create a safe read assured domain
        safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT);
        // Wait for connection of domain to RS
        waitForConnectionToRs(testcase, replicationServer);

        // Make an LDAP update (add an entry)
        startTime = System.currentTimeMillis(); // Time the update has been initiated
        String entry = "dn: ou=assured-sr-timeout-entry" + rsGroupId + "," + SAFE_READ_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
        addEntry(TestCaseUtils.entryFromLdifString(entry));
      }
      else
      {
        safeReadDomainCfgEntry = createNotAssuredDomain();
        // Wait for connection of domain to RS
        waitForConnectionToRs(testcase, replicationServer);

        // Make an LDAP update (add an entry)
        startTime = System.currentTimeMillis(); // Time the update has been initiated
        String entry = "dn: ou=assured-sr-timeout-entry" + rsGroupId + "," + NOT_ASSURED_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
        addEntry(TestCaseUtils.entryFromLdifString(entry));
      }
      long endTime = System.currentTimeMillis();

      waitForScenarioExecutedOnRs(testcase, replicationServer);

      if (rsGroupId == 1)
      {
        assertBlockedLongerThanTimeout(startTime, endTime, TIMEOUT);
        assertTrue(replicationServer.isScenarioExecuted());

        // Check monitoring values
        Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
        DN baseDN = DN.valueOf(SAFE_READ_DN);
        new MonitorAssertions(baseDN)
          .assertValue("assured-sr-sent-updates", 1)
          .assertValue("assured-sr-not-acknowledged-updates", 1)
          .assertValue("assured-sr-timeout-updates", 1)
          .assertRemainingValuesAreZero();
        assertServerErrorsSafeReadMode(baseDN, entry(RS_SERVER_ID, 1));
      }
      else
      {
        // RS has a different group id, addEntry should have returned quickly
        assertTrue(endTime - startTime < 3000);

        // No error should be seen in monitoring and update should have not been
        // sent in assured mode
        Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
        DN baseDN = DN.valueOf(NOT_ASSURED_DN);
        new MonitorAssertions(baseDN).assertRemainingValuesAreZero();
        assertNoServerErrors(baseDN);
      }
    } finally
    {
      endTest(testcase);
    }
  }

  /** Tests parameters sent in session handshake and updates, when not using assured replication. */
  @Test
  public void testNotAssuredSession() throws Exception
  {
    String testcase = "testNotAssuredSession";
    try
    {
      // Create and start a RS expecting not assured clients
      replicationServer = new FakeReplicationServer((byte)1, replServerPort, RS_SERVER_ID,
        false, testcase);
      replicationServer.start(NOT_ASSURED_SCENARIO);

      // Create a not assured domain
      notAssuredDomainCfgEntry = createNotAssuredDomain();
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make an LDAP update (add an entry)
      String entry = "dn: ou=not-assured-entry," + NOT_ASSURED_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      // Wait for entry received by RS
      waitForScenarioExecutedOnRs(testcase, replicationServer);

      // No more test to do here
    } finally
    {
      endTest(testcase);
    }
  }

  /**
   * Wait for connection to the fake replication server or times out with error
   * after some seconds.
   */
  private void waitForConnectionToRs(String testCase, FakeReplicationServer rs) throws Exception
  {
    int nsec = -1;
    do
    {
      nsec++;
      if (nsec == 10)
      {
        fail(testCase + ": timeout waiting for domain connection to fake RS after " + nsec + " seconds.");
      }
      Thread.sleep(1000);
    } while (!rs.isHandshakeOk());
  }

  /**
   * Wait for the scenario to be executed by the fake replication server or
   * times out with error after some seconds.
   */
  private void waitForScenarioExecutedOnRs(String testCase, FakeReplicationServer rs) throws Exception
  {
    int nsec = -1;
    do
    {
      nsec++;
      if (nsec == 10)
      {
        fail(testCase + ": timeout waiting for scenario to be exectued on fake RS after " + nsec + " seconds.");
      }
      Thread.sleep(1000);
    } while (!rs.isScenarioExecuted());
  }

  private void endTest(String testcase) throws Exception
  {
    debugInfo("Ending test " + testcase);
    if (replicationServer != null)
    {
      replicationServer.shutdown();
    }

    removeDomain(safeDataDomainCfgEntry, safeReadDomainCfgEntry, notAssuredDomainCfgEntry);
  }

  /**
   * Tests that a DS performing a modification in safe data mode receives the RS
   * ack and does not return before returning it.
   */
  @Test
  public void testSafeDataModeAck() throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeDataModeAck";
    try
    {
      // Create and start a RS expecting clients in safe data assured mode with
      // safe data level 2
      replicationServer = new FakeReplicationServer((byte)1, replServerPort, RS_SERVER_ID,
        2, testcase);
      replicationServer.start(NO_TIMEOUT_SCENARIO);

      // Create a safe data assured domain
      safeDataDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_DATA_MODE, 2, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make an LDAP update (add an entry)
      long startTime = System.currentTimeMillis(); // Time the update has been initiated
      String entry = "dn: ou=assured-sd-no-timeout-entry," + SAFE_DATA_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);
      assertTrue(replicationServer.isScenarioExecuted());

      // Check monitoring values
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      DN baseDN = DN.valueOf(SAFE_DATA_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sd-sent-updates", 1)
        .assertValue("assured-sd-acknowledged-updates", 1)
        .assertRemainingValuesAreZero();
      assertNoServerErrors(baseDN);
    } finally
    {
      endTest(testcase);
    }
  }

  /**
   * Tests that a DS performing a modification in safe read mode receives the RS
   * ack and does not return before returning it.
   */
  @Test
  public void testSafeReadModeAck() throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeReadModeAck";
    try
    {
      // Create and start a RS expecting clients in safe read assured mode
      replicationServer = new FakeReplicationServer((byte)1, replServerPort, RS_SERVER_ID,
        true, testcase);
      replicationServer.start(NO_TIMEOUT_SCENARIO);

      // Create a safe read assured domain
      safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make an LDAP update (add an entry)
      long startTime = System.currentTimeMillis(); // Time the update has been initiated
      String entry = "dn: ou=assured-sr-no-timeout-entry," + SAFE_READ_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);
      assertTrue(replicationServer.isScenarioExecuted());

      // Check monitoring values
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      DN baseDN = DN.valueOf(SAFE_READ_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sr-sent-updates", 1)
        .assertValue("assured-sr-acknowledged-updates", 1)
        .assertRemainingValuesAreZero();
      assertNoServerErrors(baseDN);
    } finally
    {
      endTest(testcase);
    }
  }

  /**
   * Tests that a DS receiving an update from a RS in safe read mode effectively
   * sends an ack back (with or without error).
   */
  @Test(dataProvider = "rsGroupIdProvider", groups = "slow")
  public void testSafeReadModeReply(byte rsGroupId) throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeReadModeReply";
    try
    {
      // Create and start a RS expecting clients in safe read assured mode
      replicationServer = new FakeReplicationServer(rsGroupId, replServerPort, RS_SERVER_ID,
        true, testcase);
      replicationServer.start(NO_READ);

      // Create a safe read assured domain
      safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      /* Send an update from the RS and get the ack */

      // Make the RS send an assured add message
      String entryStr = "dn: ou=assured-sr-reply-entry," + SAFE_READ_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      Entry entry = TestCaseUtils.entryFromLdifString(entryStr);
      String parentUid = getEntryUUID(DN.valueOf(SAFE_READ_DN));

      try {
        AckMsg ackMsg = replicationServer.sendAssuredAddMsg(entry, parentUid);

        if (rsGroupId == 2)
        {
          fail("Should only go here for RS with same group id as DS");
        }

        // Ack received, replay has occurred
        assertNotNull(DirectoryServer.getEntry(entry.getName()));

        // Check that DS replied an ack without errors anyway
        assertFalse(ackMsg.hasTimeout());
        assertFalse(ackMsg.hasReplayError());
        assertFalse(ackMsg.hasWrongStatus());
        assertEquals(ackMsg.getFailedServers().size(), 0);

        // Check for monitoring data
        DN baseDN = DN.valueOf(SAFE_READ_DN);
        new MonitorAssertions(baseDN)
          .assertValue("assured-sr-received-updates", 1)
          .assertValue("assured-sr-received-updates-acked", 1)
          .assertRemainingValuesAreZero();
        assertNoServerErrors(baseDN);
      }
      catch (SocketTimeoutException e)
      {
        // Expected
        if (rsGroupId == 1)
        {
          fail("Should only go here for RS with group id different from DS one");
        }

        return;
      }

      /* Send an update with error from the RS and get the ack with error */

      // Make the RS send a not possible assured add message

      // TODO: make the domain return an error: use a plugin ?
      // The resolution code does not generate any error so we need to find a
      // way to have the replay not working to test this...

      // Check that DS replied an ack with errors
//      assertFalse(ackMsg.hasTimeout());
//      assertTrue(ackMsg.hasReplayError());
//      assertFalse(ackMsg.hasWrongStatus());
//      List<Integer> failedServers = ackMsg.getFailedServers();
//      assertEquals(failedServers.size(), 1);
//      assertEquals((integer)failedServers.get(0), (integer)1);
    } finally
    {
      endTest(testcase);
    }
  }

  /**
   * Tests that a DS receiving an update from a RS in safe data mode does not
   * send back and ack (only safe read is taken into account in DS replay).
   */
  @Test(dataProvider = "rsGroupIdProvider", groups = "slow")
  public void testSafeDataModeReply(byte rsGroupId) throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeDataModeReply";
    try
    {
      // Create and start a RS expecting clients in safe data assured mode
      replicationServer = new FakeReplicationServer(rsGroupId, replServerPort, RS_SERVER_ID,
        4, testcase);
      replicationServer.start(NO_READ);

      // Create a safe data assured domain
      safeDataDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_DATA_MODE, 4, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make the RS send an assured add message: we expect a read timeout as
      // safe data should be ignored by DS
      String entryStr = "dn: ou=assured-sd-reply-entry," + SAFE_DATA_DN + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      Entry entry = TestCaseUtils.entryFromLdifString(entryStr);
      String parentUid = getEntryUUID(DN.valueOf(SAFE_DATA_DN));

      AckMsg ackMsg = replicationServer.sendAssuredAddMsg(entry, parentUid);
      fail("DS should not reply an ack in safe data mode, however, it replied: " + ackMsg);
    }
    catch (SocketTimeoutException expected)
    {
      return;
    }
    finally
    {
      endTest(testcase);
    }
  }

  /**
   * DS performs many successive modifications in safe data mode and receives RS
   * acks with various errors. Check for monitoring right errors
   */
  @Test(groups = "slow")
  public void testSafeDataManyErrors() throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeDataManyErrors";
    try
    {
      // Create and start a RS expecting clients in safe data assured mode with
      // safe data level 3
      replicationServer = new FakeReplicationServer((byte)1, replServerPort, RS_SERVER_ID,
        3, testcase);
      replicationServer.start(SAFE_DATA_MANY_ERRORS);

      // Create a safe data assured domain
      safeDataDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_DATA_MODE, 3, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make a first LDAP update (add an entry)
      long startTime = System.currentTimeMillis(); // Time the update has been initiated
      String entryDn = "ou=assured-sd-many-errors-entry," + SAFE_DATA_DN;
      String entry = "dn: " + entryDn + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);

      // Check monitoring values
      // The expected ack for the first update is:
      // - timeout error
      // - server 10 error
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      DN baseDN = DN.valueOf(SAFE_DATA_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sd-sent-updates", 1)
        .assertValue("assured-sd-timeout-updates", 1)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeDataMode(baseDN, entry(10, 1));

      // Make a second LDAP update (delete the entry)
      startTime = System.currentTimeMillis(); // Time the update has been initiated
      deleteEntry(entryDn);

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);

      // Check monitoring values
      // The expected ack for the second update is:
      // - timeout error
      // - server 10 error, server 20 error
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      baseDN = DN.valueOf(SAFE_DATA_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sd-sent-updates", 2)
        .assertValue("assured-sd-timeout-updates", 2)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeDataMode(baseDN, entry(10, 2), entry(20, 1));

      // Make a third LDAP update (re-add the entry)
      startTime = System.currentTimeMillis(); // Time the update has been initiated
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedLongerThanTimeout(startTime, System.currentTimeMillis(), TIMEOUT);
      assertTrue(replicationServer.isScenarioExecuted());

      // Check monitoring values
      // No ack should have comen back, so timeout incremented (flag and error for rs)
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      baseDN = DN.valueOf(SAFE_DATA_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sd-sent-updates", 3)
        .assertValue("assured-sd-timeout-updates", 3)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeDataMode(baseDN,
          entry(10, 2), entry(20, 1), entry(RS_SERVER_ID, 1));
    } finally
    {
      endTest(testcase);
    }
  }

  /**
   * In this scenario, the fake RS will not send back an ack so we expect the
   * add entry code (LDAP client code emulation) to be blocked for the timeout
   * value at least. If the time we have slept is lower, timeout handling code
   * is not working...
   */
  private void assertBlockedLongerThanTimeout(long startTime, long endTime, int TIMEOUT)
  {
    assertTrue(endTime - startTime >= TIMEOUT);
  }

  /**
   * In this scenario, the fake RS will send back an ack after
   * NO_TIMEOUT_RS_SLEEP_TIME seconds, so we expect the add/delete entry code
   * (LDAP client code emulation) to be blocked for more than
   * NO_TIMEOUT_RS_SLEEP_TIME seconds but no more than the timeout value.
   */
  private void assertBlockedForLessThanTimeout(long startTime, int TIMEOUT)
  {
    long endTime = System.currentTimeMillis();
    long callTime = endTime - startTime;
    assertTrue(NO_TIMEOUT_RS_SLEEP_TIME <= callTime && callTime <= TIMEOUT);
  }

  /**
   * DS performs many successive modifications in safe read mode and receives RS
   * acks with various errors. Check for monitoring right errors
   */
  @Test(groups = "slow")
  public void testSafeReadManyErrors() throws Exception
  {
    int TIMEOUT = 5000;
    String testcase = "testSafeReadManyErrors";
    try
    {
      // Create and start a RS expecting clients in safe read assured mode
      replicationServer = new FakeReplicationServer((byte)1, replServerPort, RS_SERVER_ID,
        true, testcase);
      replicationServer.start(SAFE_READ_MANY_ERRORS);

      // Create a safe read assured domain
      safeReadDomainCfgEntry = createAssuredDomain(AssuredMode.SAFE_READ_MODE, 0, TIMEOUT);
      // Wait for connection of domain to RS
      waitForConnectionToRs(testcase, replicationServer);

      // Make a first LDAP update (add an entry)
      long startTime = System.currentTimeMillis(); // Time the update has been initiated
      String entryDn = "ou=assured-sr-many-errors-entry," + SAFE_READ_DN;
      String entry = "dn: " + entryDn + "\n" +
        "objectClass: top\n" +
        "objectClass: organizationalUnit\n";
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);

      // Check monitoring values
      // The expected ack for the first update is:
      // - replay error
      // - server 10 error, server 20 error
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      DN baseDN = DN.valueOf(SAFE_READ_DN);
      new MonitorAssertions(baseDN)
        .assertValue("assured-sr-sent-updates", 1)
        .assertValue("assured-sr-not-acknowledged-updates", 1)
        .assertValue("assured-sr-replay-error-updates", 1)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeReadMode(baseDN, entry(10, 1), entry(20, 1));

      // Make a second LDAP update (delete the entry)
      startTime = System.currentTimeMillis(); // Time the update has been initiated
      deleteEntry(entryDn);

      assertBlockedForLessThanTimeout(startTime, TIMEOUT);

      // Check monitoring values
      // The expected ack for the second update is:
      // - timeout error
      // - wrong status error
      // - replay error
      // - server 10 error, server 20 error, server 30 error
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      new MonitorAssertions(baseDN)
        .assertValue("assured-sr-sent-updates", 2)
        .assertValue("assured-sr-not-acknowledged-updates", 2)
        .assertValue("assured-sr-timeout-updates", 1)
        .assertValue("assured-sr-wrong-status-updates", 1)
        .assertValue("assured-sr-replay-error-updates", 2)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeReadMode(baseDN,
          entry(10, 2), entry(20, 2), entry(30, 1));

      // Make a third LDAP update (re-add the entry)
      startTime = System.currentTimeMillis(); // Time the update has been initiated
      addEntry(TestCaseUtils.entryFromLdifString(entry));

      assertBlockedLongerThanTimeout(startTime, System.currentTimeMillis(), TIMEOUT);
      assertTrue(replicationServer.isScenarioExecuted());

      // Check monitoring values
      // No ack should have comen back, so timeout incremented (flag and error for rs)
      Thread.sleep(1000); // Sleep a while as counters are updated just after sending thread is unblocked
      new MonitorAssertions(baseDN)
        .assertValue("assured-sr-sent-updates", 3)
        .assertValue("assured-sr-not-acknowledged-updates", 3)
        .assertValue("assured-sr-timeout-updates", 2)
        .assertValue("assured-sr-wrong-status-updates", 1)
        .assertValue("assured-sr-replay-error-updates", 2)
        .assertRemainingValuesAreZero();
      assertServerErrorsSafeReadMode(baseDN,
          entry(10, 2), entry(20, 2), entry(30, 1), entry(RS_SERVER_ID, 1));
    } finally
    {
      endTest(testcase);
    }
  }

  /** Delete an entry from the database. */
  private void deleteEntry(String dn) throws Exception
  {
    DN realDN = DN.valueOf(dn);
    DeleteOperation delOp = connection.processDelete(realDN);
    waitOpResult(delOp, ResultCode.SUCCESS);
    assertNull(DirectoryServer.getEntry(realDN));
  }

  /**
   * Get the errors by servers from cn=monitor, according to the requested base dn
   * and the requested mode
   * This corresponds to the values for multi valued attributes:
   * - assured-sr-server-not-acknowledged-updates in SR mode
   * - assured-sd-server-timeout-updates in SD mode
   */
  private Map<Integer, Integer> getErrorsByServers(DN baseDN,
      AssuredMode assuredMode) throws Exception
  {
    // Find monitoring entry for requested base DN
    SearchFilter monitorFilter = SearchFilter.createFilterFromString(
        "(&(cn=Directory server*)(domain-name=" + baseDN + "))");
    DN dn = DN.valueOf("cn=replication,cn=monitor");

    InternalSearchOperation op;
    int count = 0;
    do
    {
      if (count++>0)
      {
        Thread.sleep(100);
      }
      final SearchRequest request = newSearchRequest(dn, SearchScope.WHOLE_SUBTREE, monitorFilter);
      op = connection.processSearch(request);
    }
    while (op.getSearchEntries().isEmpty() && count < 100);

    Assertions.assertThat(op.getSearchEntries()).isNotEmpty();

    SearchResultEntry entry = op.getSearchEntries().getFirst();
    assertNotNull(entry);

    /* Find the multi valued attribute matching the requested assured mode */
    String assuredAttr;
    switch(assuredMode)
    {
      case SAFE_READ_MODE:
        assuredAttr = "assured-sr-server-not-acknowledged-updates";
        break;
      case SAFE_DATA_MODE:
        assuredAttr = "assured-sd-server-timeout-updates";
        break;
      default:
        throw new Exception("Unknown assured type");
    }

    List<Attribute> attrs = entry.getAttribute(assuredAttr);
    if (attrs == null || attrs.isEmpty())
    {
      return Collections.emptyMap();
    }

    // Parse and store values
    Map<Integer,Integer> resultMap = new HashMap<>();
    for (ByteString val : attrs.get(0))
    {
      StringTokenizer strtok = new StringTokenizer(val.toString(), ":");

      String serverId = strtok.nextToken();
      if (serverId != null) {
        String nbErrors = strtok.nextToken();
        if (nbErrors != null) {
          resultMap.put(Integer.valueOf(serverId), Integer.valueOf(nbErrors));
        }
      }
    }
    return resultMap;
  }

  private void waitOpResult(Operation operation, ResultCode expectedResult) throws Exception
  {
    int ii=0;
    while (operation.getResultCode() == ResultCode.UNDEFINED ||
        operation.getResultCode() != expectedResult)
    {
      Thread.sleep(50);
      ii++;
      if (ii>10)
      {
        assertEquals(operation.getResultCode(), expectedResult,
            operation.getErrorMessage().toString());
      }
    }
  }
}
