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

import static org.opends.server.TestCaseUtils.*;
import static org.testng.Assert.*;

import java.io.IOException;
import java.util.SortedSet;
import java.util.TreeSet;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.TestCaseUtils;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.server.ReplServerFakeConfiguration;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.types.DN;
import org.opends.server.types.HostPort;
import org.testng.annotations.Test;

/**
 * Test if the replication domain is able to switch of replication server
 * if there is some replication server failure.
 */
@Test(sequential = true)
public class ReplicationServerFailoverTest extends ReplicationTestCase
{
  private static final int DS1_ID = 1;
  private static final int DS2_ID = 2;
  private static final int RS1_ID = 31;
  private static final int RS2_ID = 32;
  private int rs1Port = -1;
  private int rs2Port = -1;
  private LDAPReplicationDomain rd1;
  private LDAPReplicationDomain rd2;
  private ReplicationServer rs1;
  private ReplicationServer rs2;

  /** 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);
    }
  }

  private void initTest() throws IOException
  {
    rs1Port = rs2Port = -1;
    rd1 = rd2 = null;
    rs1 = rs2 = null;
    findFreePorts();
  }

  private void endTest() throws Exception
  {
    if (rd1 != null)
    {
      rd1.shutdown();
      rd1 = null;
    }

    if (rd2 != null)
    {
      rd2.shutdown();
      rd2 = null;
    }

    // Clear any reference to a domain in synchro plugin
    MultimasterReplication.deleteDomain(DN.valueOf(TEST_ROOT_DN_STRING));
    remove(rs1, rs2);
    rs1 = rs2 = null;
    rs1Port = rs2Port = -1;
  }

  /**
   * Test the failover feature when one RS fails:
   * 1 DS (DS1) and 2 RS (RS1 and RS2) in topology.
   * DS1 connected to one RS
   * Both RS are connected together (RS1<->RS2)
   * The RS connected to DS1 fails, DS1 should be connected
   * to the other RS
   *
   * @throws Exception If a problem occurred
   */
  @Test
  public void testFailOverSingle() throws Exception
  {
    String testCase = "testFailOverSingle";
    int rsPort = -1;
    debugInfo("Starting " + testCase);

    initTest();

    try
    {
      // Start RS1
      rs1 = createReplicationServer(RS1_ID, testCase);
      // Start RS2
      rs2 = createReplicationServer(RS2_ID, testCase);

      // Start DS1
      DN baseDn = DN.valueOf(TEST_ROOT_DN_STRING);
      rd1 = createReplicationDomain(baseDn, DS1_ID);

      // Wait a bit so that connections are performed
      Thread.sleep(2000);

      // DS1 connected to RS1 ?
      // Check which replication server is connected to this LDAP server
      rsPort = findReplServerConnected(rd1);

      if (rsPort == rs1Port)
      {
        // Simulate RS1 failure
        String msg = "Before " + RS1_ID + " failure";
        debugInfo(msg);
        rs1.remove();
        // Let time for failover to happen
        // DS1 connected to RS2 ?
        msg = "After " + RS1_ID + " failure";
        checkConnection(30, DS1_ID, RS2_ID, msg);
      }
      else if (rsPort == rs2Port)
      { // Simulate RS2 failure
        String msg = "Before " + RS2_ID + " failure";
        debugInfo(msg);
        rs2.remove();
        // DS1 connected to RS1 ?
        msg = "After " + RS2_ID + " failure";
        checkConnection(30, DS1_ID, RS1_ID, msg);
      }
      else {
        fail("DS1 is not connected to a RS");
      }

      debugInfo(testCase + " successfully ended.");
    } finally
    {
      endTest();
    }
  }

  /**
   * Test the failover feature when one RS fails:
   * 2 DS (DS1 and DS2) and 2 RS (RS1 and RS2) in topology.
   * Each DS connected to its own RS (DS1<->RS1, DS2<->RS2)
   * Both RS are connected together (RS1<->RS2)
   * RS1 fails, DS1 and DS2 should be both connected to RS2
   * RS1 comes back (no change)
   * RS2 fails, DS1 and DS2 should be both connected to RS1
   *
   * @throws Exception If a problem occurred
   */
  @Test(groups="slow")
  public void testFailOverMulti() throws Exception
  {
    String testCase = "testFailOverMulti";

    debugInfo("Starting " + testCase);

    initTest();

    try
    {
      // Start RS1
      rs1 = createReplicationServer(RS1_ID, testCase);
      // Start RS2
      rs2 = createReplicationServer(RS2_ID, testCase);

      // Start DS1
      DN baseDn = DN.valueOf(TEST_ROOT_DN_STRING);
      rd1 = createReplicationDomain(baseDn, DS1_ID);
      // Start DS2
      rd2 = createReplicationDomain(baseDn, DS2_ID);

      // Wait a bit so that connections are performed
      Thread.sleep(3000);

      // Simulate RS1 failure
      rs1.remove();

      // DS1 connected to RS2 ?
      String msg = "After " + RS1_ID + " failure";
      checkConnection(30, DS1_ID, RS2_ID, msg);
      // DS2 connected to RS2 ?
      checkConnection(30, DS2_ID, RS2_ID, msg);

      // Restart RS1
      rs1 = createReplicationServer(RS1_ID, testCase);

      // DS1 connected to RS2 ?
      msg = "Before " + RS2_ID + " failure";
      checkConnection(30, DS1_ID, RS2_ID, msg);
      // DS2 connected to RS2 ?
      checkConnection(30, DS2_ID, RS2_ID, msg);

      // Simulate RS2 failure
      rs2.remove();

      // DS1 connected to RS1 ?
      msg = "After " + RS2_ID + " failure";
      checkConnection(30, DS1_ID, RS1_ID, msg);
      // DS2 connected to RS1 ?
      checkConnection(30, DS2_ID, RS1_ID, msg);

      // Restart RS2
      rs2 = createReplicationServer(RS2_ID, testCase);

      // DS1 connected to RS1 ?
      msg = "After " + RS2_ID + " restart";
      checkConnection(30, DS1_ID, RS1_ID, msg);
      // DS2 connected to RS1 ?
      checkConnection(30, DS2_ID, RS1_ID, msg);

      debugInfo(testCase + " successfully ended.");
    } finally
    {
      endTest();
    }
  }

  /**
   * Check connection of the provided replication domain to the provided
   * replication server. Waits for connection to be ok up to secTimeout seconds
   * before failing.
   */
  private void checkConnection(int secTimeout, int dsId, int rsId, String msg)
      throws Exception
  {
    LDAPReplicationDomain rd = null;
    switch (dsId)
    {
      case DS1_ID:
        rd = rd1;
        break;
      case DS2_ID:
        rd = rd2;
        break;
      default:
        fail("Unknown replication domain server id.");
    }

    int rsPort = -1;
    switch (rsId)
    {
      case RS1_ID:
        rsPort = rs1Port;
        break;
      case RS2_ID:
        rsPort = rs2Port;
        break;
      default:
        fail("Unknown replication server id.");
    }

    int nSec = 0;

    // Go out of the loop only if connection is verified or if timeout occurs
    while (true)
    {
      // Test connection
      boolean connected = rd.isConnected();
      int rdPort = -1;
      boolean rightPort = false;
      if (connected)
      {
        String serverStr = rd.getReplicationServer();
        rdPort = HostPort.valueOf(serverStr).getPort();
        if (rdPort == rsPort)
        {
          rightPort = true;
        }
      }
      if (connected && rightPort)
      {
        // Connection verified
        debugInfo("checkConnection: connection from domain " + dsId + " to" +
          " replication server " + rsId + " obtained after "
          + nSec + " seconds.");
        return;
      }

      // Sleep 1 second
      Thread.sleep(1000);
      nSec++;

      if (nSec > secTimeout)
      {
        // Timeout reached, end with error
        fail("checkConnection: could not verify connection from domain " + dsId
          + " to replication server " + rsId + " after " + secTimeout + " seconds."
          + " Domain connected: " + connected + ", connection port: " + rdPort
          + " (should be: " + rsPort + "). [" + msg + "]");
      }
    }
  }

  /**
   * Find needed free TCP ports.
   */
  private void findFreePorts() throws IOException
  {
    int[] ports = TestCaseUtils.findFreePorts(2);
    rs1Port = ports[0];
    rs2Port = ports[1];
  }

  /**
   * Creates a new ReplicationServer.
   */
  private ReplicationServer createReplicationServer(int serverId, String suffix)
      throws ConfigException
  {
    SortedSet<String> replServers = new TreeSet<>();
    int port = -1;
    if (serverId == RS1_ID)
    {
      port = rs1Port;
      replServers.add("localhost:" + rs2Port);
    }
    else if (serverId == RS2_ID)
    {
      port = rs2Port;
      replServers.add("localhost:" + rs1Port);
    }
    else
    {
      fail("Unknown replication server id.");
    }

    String dir = "replicationServerFailoverTest" + serverId + suffix + "Db";
    return new ReplicationServer(new ReplServerFakeConfiguration(port, dir, 0, serverId, 0, 100, replServers));
  }

  /**
   * Creates a new ReplicationDomain.
   */
  private LDAPReplicationDomain createReplicationDomain(DN baseDn, int serverId)
      throws Exception
  {
    SortedSet<String> replServers = new TreeSet<>();

    // Create a domain with two replication servers
    replServers.add("localhost:" + rs1Port);
    replServers.add("localhost:" + rs2Port);

    DomainFakeCfg domainConf = new DomainFakeCfg(baseDn, serverId, replServers);
    // domainConf.setHeartbeatInterval(500);
    LDAPReplicationDomain replicationDomain =
        MultimasterReplication.createNewDomain(domainConf);
    replicationDomain.start();

    return replicationDomain;
  }

  private int findReplServerConnected(LDAPReplicationDomain rd)
  {
    // First check that the Replication domain is connected
    if (!rd.isConnected())
    {
      return -1;
    }

    String serverStr = rd.getReplicationServer();
    return HostPort.valueOf(serverStr).getPort();
  }
}
