/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ratis.server.impl;

import org.apache.ratis.BaseTest;
import org.apache.ratis.RaftTestUtil;
import org.apache.ratis.RaftTestUtil.SimpleMessage;
import org.apache.ratis.client.RaftClient;
import org.apache.ratis.client.RaftClientRpc;
import org.apache.ratis.proto.RaftProtos.LogEntryProto;
import org.apache.ratis.protocol.RaftClientReply;
import org.apache.ratis.protocol.RaftClientRequest;
import org.apache.ratis.protocol.RaftGroup;
import org.apache.ratis.protocol.RaftGroupId;
import org.apache.ratis.protocol.RaftPeer;
import org.apache.ratis.protocol.RaftPeerId;
import org.apache.ratis.protocol.SetConfigurationRequest;
import org.apache.ratis.protocol.exceptions.SetConfigurationException;
import org.apache.ratis.protocol.exceptions.LeaderNotReadyException;
import org.apache.ratis.protocol.exceptions.ReconfigurationInProgressException;
import org.apache.ratis.protocol.exceptions.ReconfigurationTimeoutException;
import org.apache.ratis.server.RaftConfiguration;
import org.apache.ratis.server.RaftServer;
import org.apache.ratis.server.RaftServerConfigKeys;
import org.apache.ratis.server.raftlog.LogProtoUtils;
import org.apache.ratis.server.raftlog.RaftLog;
import org.apache.ratis.server.raftlog.RaftLogBase;
import org.apache.ratis.server.storage.RaftStorageTestUtils;
import org.apache.ratis.test.tag.Flaky;
import org.apache.ratis.util.JavaUtils;
import org.apache.ratis.util.LifeCycle;
import org.apache.ratis.util.Slf4jUtils;
import org.apache.ratis.util.TimeDuration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.slf4j.event.Level;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.apache.ratis.server.impl.RaftServerTestUtil.waitAndCheckNewConf;
import static org.junit.jupiter.api.Assertions.assertThrows;

public abstract class RaftReconfigurationBaseTest<CLUSTER extends MiniRaftCluster>
    extends BaseTest
    implements MiniRaftCluster.Factory.Get<CLUSTER> {
  static {
    Slf4jUtils.setLogLevel(RaftServer.Division.LOG, Level.DEBUG);
  }

  private static final DelayLocalExecutionInjection LOG_SYNC_DELAY = RaftServerTestUtil.getLogSyncDelay();
  private static final DelayLocalExecutionInjection LEADER_PLACE_HOLDER_DELAY =
      new DelayLocalExecutionInjection(LeaderStateImpl.APPEND_PLACEHOLDER);

  static final int STAGING_CATCHUP_GAP = 10;

  {
    RaftServerConfigKeys.setStagingCatchupGap(getProperties(), STAGING_CATCHUP_GAP);
  }

  private void checkPriority(CLUSTER cluster, RaftGroupId groupId, List<RaftPeer> peersWithPriority)
      throws InterruptedException {
    RaftTestUtil.waitForLeader(cluster, groupId);

    for (int i = 0; i < peersWithPriority.size(); i ++) {
      RaftPeerId peerId = peersWithPriority.get(i).getId();
      final RaftServer.Division server = cluster.getDivision(peerId, groupId);
      final RaftConfiguration conf = server.getRaftConf();

      for (int j = 0; j < peersWithPriority.size(); j ++) {
        int priorityInConf = conf.getPeer(peersWithPriority.get(j).getId()).getPriority();
        Assertions.assertEquals(priorityInConf, peersWithPriority.get(j).getPriority());
      }
    }
  }

  @Test
  public void testRestorePriority() throws Exception {
    runWithNewCluster(3, cluster -> {
      // Add groups
      List<RaftPeer> peers = cluster.getPeers();

      List<RaftPeer> peersWithPriority = new ArrayList<>();
      for (int i = 0; i < peers.size(); i++) {
        RaftPeer peer = peers.get(i);
        peersWithPriority.add(
            RaftPeer.newBuilder(peer).setPriority(i).build());
      }

      final RaftGroup newGroup = RaftGroup.valueOf(RaftGroupId.randomId(), peersWithPriority);
      LOG.info("add new group: " + newGroup);
      try (final RaftClient client = cluster.createClient(newGroup)) {
        for (RaftPeer p : newGroup.getPeers()) {
          client.getGroupManagementApi(p.getId()).add(newGroup);
        }
      }

      RaftGroupId groupId = newGroup.getGroupId();

      checkPriority(cluster, groupId, peersWithPriority);

      cluster.restart(false);

      checkPriority(cluster, groupId, peersWithPriority);
    });
  }

  /**
   * add 2 new peers (3 peers -> 5 peers), no leader change
   */
  @Test
  public void testAddPeers() throws Exception {
    runWithNewCluster(3, cluster -> {
      RaftTestUtil.waitForLeader(cluster);

      // add new peers
      final List<RaftPeer> allPeers = cluster.addNewPeers(2, true).getPeersInNewConf();

      // trigger setConfiguration
      cluster.setConfiguration(allPeers);

      // wait for the new configuration to take effect
      waitAndCheckNewConf(cluster, allPeers, 0, null);
    });
  }

  /**
   * Test leader election when changing cluster from single mode to HA mode.
   */
  @Test
  public void testLeaderElectionWhenChangeFromSingleToHA() throws Exception {
    runWithNewCluster(1, cluster -> {
      RaftTestUtil.waitForLeader(cluster);

      RaftGroupId groupId = cluster.getGroup().getGroupId();
      RaftPeer curPeer = cluster.getGroup().getPeers().iterator().next();
      RaftPeer newPeer = cluster.addNewPeers(1, true).getAddedPeers().get(0);

      RaftServerProxy leaderServer = cluster.getServer(curPeer.getId());

      // Update leader conf to transitional single mode.
      RaftConfigurationImpl oldNewConf = RaftConfigurationImpl.newBuilder()
          .setOldConf(new PeerConfiguration(Collections.singletonList(curPeer)))
          .setConf(new PeerConfiguration(Arrays.asList(curPeer, newPeer)))
          .setLogEntryIndex(Long.MAX_VALUE / 2)
          .build();
      Assertions.assertTrue(oldNewConf.isSingleMode(curPeer.getId()));
      RaftServerTestUtil.setRaftConf(leaderServer, groupId, oldNewConf);
      try(RaftClient client = cluster.createClient()) {
        client.admin().transferLeadership(null, leaderServer.getId(), 1000);
      }

      final RaftServer.Division newLeader = RaftTestUtil.waitForLeader(cluster);
      Assertions.assertEquals(leaderServer.getId(), newLeader.getId());
      Assertions.assertEquals(oldNewConf, newLeader.getRaftConf());
    });
  }

  @Test
  public void testChangeMajority() throws Exception {
    runWithNewCluster(1, cluster -> {
      RaftTestUtil.waitForLeader(cluster);
      final RaftPeerId leaderId = cluster.getLeader().getId();

      try (final RaftClient client = cluster.createClient(leaderId)) {
        final PeerChanges c1 = cluster.addNewPeers(2, true);

        SetConfigurationRequest.Arguments arguments = SetConfigurationRequest.Arguments.newBuilder()
            .setServersInCurrentConf(cluster.getPeers())
            .setServersInNewConf(c1.getPeersInNewConf())
            .setMode(SetConfigurationRequest.Mode.COMPARE_AND_SET)
            .build();
        assertThrows(SetConfigurationException.class, () -> client.admin().setConfiguration(arguments),
            "Expect change majority error.");
      }
    });
  }

  /**
   * remove 2 peers (5 peers -> 3 peers), no leader change
   */
  @Test
  public void testRemovePeers() throws Exception {
    runWithNewCluster(5, cluster -> {
      RaftTestUtil.waitForLeader(cluster);

      // remove peers, leader still included in the new conf
      final List<RaftPeer> allPeers = cluster.removePeers(2, false, Collections.emptyList())
          .getPeersInNewConf();

      // trigger setConfiguration
      cluster.setConfiguration(allPeers);

      // wait for the new configuration to take effect
      waitAndCheckNewConf(cluster, allPeers, 2, 2, null);
    });
  }

  /**
   * 5 peers -> 5 peers, remove 2 old, add 2 new, no leader change
   */
  @Test
  public void testAddRemovePeers() throws Exception {
    runWithNewCluster(5, cluster -> runTestAddRemovePeers(false, cluster));
  }

  @Test
  public void testLeaderStepDown() throws Exception {
    runWithNewCluster(5, cluster -> runTestAddRemovePeers(true, cluster));
  }

  private void runTestAddRemovePeers(boolean leaderStepdown, CLUSTER cluster) throws Exception {
      RaftTestUtil.waitForLeader(cluster);

      PeerChanges change = cluster.addNewPeers(2, true);
      final List<RaftPeer> allPeers = cluster.removePeers(2, leaderStepdown, change.getAddedPeers())
          .getPeersInNewConf();

      // trigger setConfiguration
      cluster.setConfiguration(allPeers);

      // wait for the new configuration to take effect
      waitAndCheckNewConf(cluster, allPeers, 2, null);
  }

  @Test
  public void testSetConfigurationInAddMode() throws Exception {
    runWithNewCluster(2, this::runTestSetConfigurationInAddMode);
  }

  private void runTestSetConfigurationInAddMode(CLUSTER cluster) throws Exception {
    final RaftServer.Division leader = RaftTestUtil.waitForLeader(cluster);

    PeerChanges change = cluster.addNewPeers(1, true);
    List<RaftPeer> peers = change.getAddedPeers();

    try (final RaftClient client = cluster.createClient(leader.getId())) {
      for (int i = 0; i < 10; i++) {
        RaftClientReply reply = client.io().send(new SimpleMessage("m" + i));
        Assertions.assertTrue(reply.isSuccess());
      }
      RaftClientReply reply = client.admin().setConfiguration(
          SetConfigurationRequest.Arguments.newBuilder()
          .setServersInNewConf(peers)
          .setMode(SetConfigurationRequest.Mode.ADD).build());
      Assertions.assertTrue(reply.isSuccess());
      waitAndCheckNewConf(cluster, change.getPeersInNewConf(), 0, null);
    }
    cluster.close();
  }

  @Test
  public void testSetConfigurationInCasMode() throws Exception {
    runWithNewCluster(2, this::runTestSetConfigurationInCasMode);
  }

  private void runTestSetConfigurationInCasMode(CLUSTER cluster) throws Exception {
    final RaftServer.Division leader = RaftTestUtil.waitForLeader(cluster);
    List<RaftPeer> oldPeers = cluster.getPeers();

    PeerChanges change = cluster.addNewPeers(1, true);
    List<RaftPeer> peers = change.getPeersInNewConf();

    try (final RaftClient client = cluster.createClient(leader.getId())) {
      for (int i = 0; i < 10; i++) {
        RaftClientReply reply = client.io().send(new SimpleMessage("m" + i));
        Assertions.assertTrue(reply.isSuccess());
      }

      testFailureCase("Can't set configuration in CAS mode ",
          () -> client.admin().setConfiguration(SetConfigurationRequest.Arguments.newBuilder()
              .setServersInNewConf(peers)
              .setServersInCurrentConf(peers)
              .setMode(SetConfigurationRequest.Mode.COMPARE_AND_SET)
              .build()), SetConfigurationException.class);

      Collections.shuffle(oldPeers);
      RaftClientReply reply = client.admin().setConfiguration(
          SetConfigurationRequest.Arguments.newBuilder()
              .setServersInNewConf(peers)
              .setServersInCurrentConf(oldPeers)
              .setMode(SetConfigurationRequest.Mode.COMPARE_AND_SET)
              .build());
      Assertions.assertTrue(reply.isSuccess());
      waitAndCheckNewConf(cluster, change.getPeersInNewConf(), 0, null);
    }
    cluster.close();
  }



  @Test
  @Timeout(value = 30)
  public void testReconfTwice() throws Exception {
    runWithNewCluster(3, this::runTestReconfTwice);
  }

  void runTestReconfTwice(CLUSTER cluster) throws Exception {
      final RaftPeerId leaderId = RaftTestUtil.waitForLeader(cluster).getId();
      try (final RaftClient client = cluster.createClient(leaderId)) {

        // submit some msgs before reconf
        for (int i = 0; i < STAGING_CATCHUP_GAP * 2; i++) {
          RaftClientReply reply = client.io().send(new SimpleMessage("m" + i));
          Assertions.assertTrue(reply.isSuccess());
        }

        final AtomicBoolean reconf1 = new AtomicBoolean(false);
        final AtomicBoolean reconf2 = new AtomicBoolean(false);
        final AtomicReference<List<RaftPeer>> finalPeers = new AtomicReference<>(null);
        CountDownLatch latch = new CountDownLatch(1);
        Thread clientThread = new Thread(() -> {
          try {
            PeerChanges c1 = cluster.addNewPeers(2, true);
            LOG.info("Start changing the configuration: {}", c1.getPeersInNewConf());

            RaftClientReply reply = client.admin().setConfiguration(c1.getPeersInNewConf());
            reconf1.set(reply.isSuccess());

            PeerChanges c2 = cluster.removePeers(2, true, c1.getAddedPeers());
            finalPeers.set(c2.getPeersInNewConf());

            LOG.info("Start changing the configuration again: {}", c2.getPeersInNewConf());
            reply = client.admin().setConfiguration(c2.getPeersInNewConf());
            reconf2.set(reply.isSuccess());

            latch.countDown();
          } catch (Exception ignored) {
            LOG.warn("{} is ignored", JavaUtils.getClassSimpleName(ignored.getClass()), ignored);
          }
        });
        clientThread.start();

        latch.await();
        Assertions.assertTrue(reconf1.get());
        Assertions.assertTrue(reconf2.get());
        waitAndCheckNewConf(cluster, finalPeers.get(), 2, null);
        final RaftPeerId leader2 = RaftTestUtil.waitForLeader(cluster).getId();

        // check configuration manager's internal state
        // each reconf will generate two configurations: (old, new) and (new)
        cluster.getServerAliveStream().forEach(server -> {
          final ConfigurationManager confManager = RaftServerTestUtil.getConfigurationManager(server);
          // each reconf will generate two configurations: (old, new) and (new)
          // each leader change generates one configuration.
          // expectedConf = 1 (init) + 2*2 (two conf changes) + #leader
          final int expectedConf = leader2.equals(leaderId) ? 6 : 7;
          Assertions.assertEquals(expectedConf, confManager.numOfConf(), server.getId() + ": " + confManager);
        });
      }
  }

  @Test
  public void testReconfTimeout() throws Exception {
    // originally 3 peers
    runWithNewCluster(3, this::runTestReconfTimeout);
  }

  void runTestReconfTimeout(CLUSTER cluster) throws Exception {
    final RaftPeerId leaderId = RaftTestUtil.waitForLeader(cluster).getId();

    try (final RaftClient client = cluster.createClient(leaderId)) {
      PeerChanges c1 = cluster.addNewPeers(2, false);

      LOG.info("Start changing the configuration: {}", c1.getPeersInNewConf());
      Assertions.assertFalse(((RaftConfigurationImpl)cluster.getLeader().getRaftConf()).isTransitional());

      final RaftClientRpc sender = client.getClientRpc();
      final SetConfigurationRequest request = cluster.newSetConfigurationRequest(
          client.getId(), leaderId, c1.getPeersInNewConf());
      try {
        RaftClientReply reply = sender.sendRequest(request);
        Assertions.fail("did not get expected exception " + reply.toString());
      } catch (IOException e) {
        Assertions.assertTrue(
           e instanceof ReconfigurationTimeoutException, "Got exception " + e);
      }

      // the two new peers have not started yet, the bootstrapping must timeout
      LOG.info(cluster.printServers());

      // resend the same request, make sure the server has correctly reset its
      // state so that we still get timeout instead of in-progress exception
      try {
        sender.sendRequest(request);
        Assertions.fail("did not get expected exception");
      } catch (IOException e) {
        Assertions.assertTrue(
           e instanceof ReconfigurationTimeoutException, "Got exception " + e);
      }

      // start the two new peers
      LOG.info("Start new peers");
      for (RaftPeer np : c1.getAddedPeers()) {
        cluster.restartServer(np.getId(), false);
      }
      Assertions.assertTrue(client.admin().setConfiguration(c1.getPeersInNewConf()).isSuccess());
    }
  }

  @Test
  public void testBootstrapReconfWithSingleNodeAddOne() throws Exception {
    // originally 1 peer, add 1 more
    runWithNewCluster(1, cluster -> runTestBootstrapReconf(1, true, cluster));
  }

  @Test
  public void testBootstrapReconfWithSingleNodeAddTwo() throws Exception {
    // originally 1 peer, add 2 more
    runWithNewCluster(1, cluster -> {
      RaftTestUtil.waitForLeader(cluster);
      final RaftPeerId leaderId = cluster.getLeader().getId();

      try (final RaftClient client = cluster.createClient(leaderId)) {
        final PeerChanges c1 = cluster.addNewPeers(2, true);

        assertThrows(SetConfigurationException.class, () -> client.admin().setConfiguration(c1.getPeersInNewConf()),
            "Expect change majority error.");
      }
    });
  }

  @Test
  public void testBootstrapReconf() throws Exception {
    // originally 3 peers, add 2 more
    runWithNewCluster(3, cluster -> runTestBootstrapReconf(2, true, cluster));
  }

  void runTestBootstrapReconf(int numNewPeer, boolean startNewPeer, CLUSTER cluster) throws Exception {
      LOG.info("Originally {} peer(s), add {} more, startNewPeer={}",
          cluster.getNumServers(), numNewPeer, startNewPeer);
      RaftTestUtil.waitForLeader(cluster);
      final RaftPeerId leaderId = cluster.getLeader().getId();
      try (final RaftClient client = cluster.createClient(leaderId)) {

        // submit some msgs before reconf
        for (int i = 0; i < STAGING_CATCHUP_GAP * 2; i++) {
          RaftClientReply reply = client.io().send(new SimpleMessage("m" + i));
          Assertions.assertTrue(reply.isSuccess());
        }

        final PeerChanges c1 = cluster.addNewPeers(numNewPeer, startNewPeer);
        LOG.info("Start changing the configuration: {}", c1.getPeersInNewConf());
        final AtomicReference<Boolean> success = new AtomicReference<>();
        Thread clientThread = new Thread(() -> {
          try {
            RaftClientReply reply = client.admin().setConfiguration(c1.getPeersInNewConf());
            success.set(reply.isSuccess());
          } catch (IOException ioe) {
            LOG.error("FAILED", ioe);
          }
        });
        clientThread.start();

        if (startNewPeer) {
          // Make sure that set configuration is run inside the thread
          RaftTestUtil.waitFor(() -> clientThread.isAlive(), 300, 5000);
          ONE_SECOND.sleep();
          LOG.info("start new peer(s): {}", c1.getAddedPeers());
          for(RaftPeer p : c1.getAddedPeers()) {
            cluster.restartServer(p.getId(), false);
          }
        }

        RaftTestUtil.waitFor(() -> success.get() != null && success.get(), 300, 15000);
        LOG.info(cluster.printServers());

        RaftTestUtil.waitFor(() -> cluster.getLeader() != null, 300, 5000);
        final RaftLog leaderLog = cluster.getLeader().getRaftLog();
        for (RaftPeer newPeer : c1.getAddedPeers()) {
          final RaftServer.Division d = cluster.getDivision(newPeer.getId());
          RaftTestUtil.waitFor(() -> leaderLog.getEntries(0, Long.MAX_VALUE).length
                  == d.getRaftLog().getEntries(0, Long.MAX_VALUE).length, 300, 15000);
          Assertions.assertArrayEquals(leaderLog.getEntries(0, Long.MAX_VALUE),
              d.getRaftLog().getEntries(0, Long.MAX_VALUE));
        }
      }
  }

  /**
   * kill the leader before reconfiguration finishes. Make sure the client keeps
   * retrying.
   */
  @Test
  @Flaky("RATIS-2251")
  public void testKillLeaderDuringReconf() throws Exception {
    // originally 3 peers
    runWithNewCluster(3, this::runTestKillLeaderDuringReconf);
  }

  void runTestKillLeaderDuringReconf(CLUSTER cluster) throws Exception {
    final AtomicBoolean clientRunning = new AtomicBoolean(true);
    Thread clientThread = null;
    try {
      final RaftPeerId leaderId = RaftTestUtil.waitForLeader(cluster).getId();

      PeerChanges c1 = cluster.addNewPeers(1, false);
      PeerChanges c2 = cluster.removePeers(1, false, c1.getAddedPeers());

      LOG.info("Start setConf: {}", c2.getPeersInNewConf());
      LOG.info(cluster.printServers());

      final CompletableFuture<Void> setConf = new CompletableFuture<>();
      clientThread = new Thread(() -> {
        try(final RaftClient client = cluster.createClient(leaderId)) {
          for(int i = 0; clientRunning.get() && !setConf.isDone(); i++) {
            final RaftClientReply reply = client.admin().setConfiguration(c2.getPeersInNewConf());
            if (reply.isSuccess()) {
              setConf.complete(null);
              return;
            }
            LOG.info("setConf attempt #{} failed, {}", i, cluster.printServers());
          }
        } catch(Exception e) {
          LOG.error("Failed to setConf", e);
          setConf.completeExceptionally(e);
        }
      });
      clientThread.start();

      // the leader cannot generate the (old, new) conf, and it will keep
      // bootstrapping the 1 new peer since it has not started yet.
      Assertions.assertFalse(((RaftConfigurationImpl)cluster.getLeader().getRaftConf()).isTransitional());

      // (0) the first conf entry, (1) the 1st setConf entry, (2) a metadata entry
      // (3) new current conf entry  (4) a metadata entry
      {
        final RaftLog leaderLog = cluster.getLeader().getRaftLog();
        for(LogEntryProto e : RaftTestUtil.getLogEntryProtos(leaderLog)) {
          LOG.info("{}", LogProtoUtils.toLogEntryString(e));
        }
        final long commitIndex = leaderLog.getLastCommittedIndex();
        Assertions.assertTrue(commitIndex <= 2, "commitIndex = " + commitIndex + " > 2");
      }

      final RaftPeerId killed = RaftTestUtil.waitAndKillLeader(cluster);
      Assertions.assertEquals(leaderId, killed);
      final RaftPeerId newLeaderId = RaftTestUtil.waitForLeader(cluster).getId();
      LOG.info("newLeaderId: {}", newLeaderId);
      TimeDuration.valueOf(1500, TimeUnit.MILLISECONDS).sleep();

      LOG.info("start new peers: {}", c1.getAddedPeers());
      for (RaftPeer np : c1.getAddedPeers()) {
        cluster.restartServer(np.getId(), false);
      }

      try {
        setConf.get(10, TimeUnit.SECONDS);
      } catch(TimeoutException ignored) {
      }

      RaftServerProxy newServer = cluster.getServer(c1.getAddedPeers().get(0).getId());
      if (newServer.getLifeCycleState() == LifeCycle.State.CLOSED) {
        LOG.info("New peer {} is shutdown. Skip the check", c1.getAddedPeers().get(0).getId());
      } else {
        // the client fails with the first leader, and then retry the same setConfiguration request
        waitAndCheckNewConf(cluster, c2.getPeersInNewConf(), 1, Collections.singletonList(leaderId));
        setConf.get(2, TimeUnit.SECONDS);
      }
    } finally {
      if (clientThread != null) {
        clientRunning.set(false);
        clientThread.interrupt();
      }
    }
  }

  /**
   * When a request's new configuration is the same with the current one, make
   * sure we return success immediately and no log entry is recorded.
   */
  @Test
  public void testNoChangeRequest() throws Exception {
    // originally 3 peers
    runWithNewCluster(3, this::runTestNoChangeRequest);
  }

  void runTestNoChangeRequest(CLUSTER cluster) throws Exception {
    final RaftServer.Division leader = RaftTestUtil.waitForLeader(cluster);
    try(final RaftClient client = cluster.createClient(leader.getId())) {
      client.io().send(new SimpleMessage("m"));

      final RaftLog leaderLog = leader.getRaftLog();
      final long committedIndex = leaderLog.getLastCommittedIndex();
      final RaftConfiguration confBefore = cluster.getLeader().getRaftConf();

      // no real configuration change in the request
      final RaftClientReply reply = client.admin().setConfiguration(cluster.getPeers().toArray(RaftPeer.emptyArray()));
      Assertions.assertTrue(reply.isSuccess());
      final long newCommittedIndex = leaderLog.getLastCommittedIndex();
      for(long i = committedIndex + 1; i <= newCommittedIndex; i++) {
        final LogEntryProto e = leaderLog.get(i);
        Assertions.assertTrue(e.hasMetadataEntry());
      }
      Assertions.assertSame(confBefore, cluster.getLeader().getRaftConf());
    }
  }

  /**
   * Make sure a setConfiguration request is rejected if a configuration change
   * is still in progress (i.e., has not been committed yet).
   */
  @Test
  public void testOverlappedSetConfRequests() throws Exception {
    // Make sure leadership check won't affect the test.
    // logSyncDelay.setDelayMs() blocks RaftServerImpl::appendEntriesAsync(),
    // which will disable heartbeats.
    final TimeDuration oldTimeoutMin = RaftServerConfigKeys.Rpc.timeoutMin(getProperties());
    RaftServerConfigKeys.Rpc.setTimeoutMin(getProperties(), TimeDuration.valueOf(2, TimeUnit.SECONDS));
    final TimeDuration oldTimeoutMax = RaftServerConfigKeys.Rpc.timeoutMax(getProperties());
    RaftServerConfigKeys.Rpc.setTimeoutMax(getProperties(), TimeDuration.valueOf(4, TimeUnit.SECONDS));

    // originally 3 peers
    runWithNewCluster(3, this::runTestOverlappedSetConfRequests);

    // reset for the other tests
    RaftServerConfigKeys.Rpc.setTimeoutMin(getProperties(), oldTimeoutMin);
    RaftServerConfigKeys.Rpc.setTimeoutMax(getProperties(), oldTimeoutMax);
  }

  void runTestOverlappedSetConfRequests(CLUSTER cluster) throws Exception {
    try {
      RaftTestUtil.waitForLeader(cluster);

      final RaftPeerId leaderId = cluster.getLeader().getId();

      final List<RaftPeer> newPeers = cluster.addNewPeers(2, true).getPeersInNewConf();

      // delay every peer's logSync so that the setConf request is delayed
      cluster.getPeers()
          .forEach(peer -> LOG_SYNC_DELAY.setDelayMs(peer.getId().toString(), 1000));

      final CountDownLatch latch = new CountDownLatch(1);
      final List<RaftPeer> peersInRequest2 = cluster.getPeers();
      AtomicBoolean caughtException = new AtomicBoolean(false);
      new Thread(() -> {
        try(final RaftClient client2 = cluster.createClient(leaderId)) {
          latch.await();
          LOG.info("client2 starts to change conf");
          final RaftClientRpc sender2 = client2.getClientRpc();
          sender2.sendRequest(cluster.newSetConfigurationRequest(
              client2.getId(), leaderId, peersInRequest2));
        } catch (ReconfigurationInProgressException e) {
          caughtException.set(true);
        } catch (Exception e) {
          LOG.warn("Got unexpected exception when client2 changes conf", e);
        }
      }).start();

      AtomicBoolean confChanged = new AtomicBoolean(false);
      new Thread(() -> {
        try(final RaftClient client1 = cluster.createClient(leaderId)) {
          LOG.info("client1 starts to change conf");
          confChanged.set(client1.admin().setConfiguration(newPeers).isSuccess());
        } catch (IOException e) {
          LOG.warn("Got unexpected exception when client1 changes conf", e);
        }
      }).start();
      Thread.sleep(100);
      latch.countDown();

      for (int i = 0; i < 10 && !confChanged.get(); i++) {
        Thread.sleep(1000);
      }
      Assertions.assertTrue(confChanged.get());
      Assertions.assertTrue(caughtException.get());
    } finally {
      LOG_SYNC_DELAY.clear();
    }
  }

  /**
   * Test a scenario where the follower truncates its log entries which causes
   * configuration change.
   */
  @Test
  public void testRevertConfigurationChange() throws Exception {
    runWithNewCluster(5, this::runTestRevertConfigurationChange);
  }

  void runTestRevertConfigurationChange(CLUSTER cluster) throws Exception {
    RaftLogBase log2 = null;
    try {
      RaftTestUtil.waitForLeader(cluster);

      final RaftServer.Division leader = cluster.getLeader();
      final RaftPeerId leaderId = leader.getId();

      final RaftLog log = leader.getRaftLog();
      log2 = (RaftLogBase) log;
      Thread.sleep(1000);

      // we block the incoming msg for the leader and block its requests to
      // followers, so that we force the leader change and the old leader will
      // not know
      LOG.info("start blocking the leader");
      BlockRequestHandlingInjection.getInstance().blockReplier(leaderId.toString());
      cluster.setBlockRequestsFrom(leaderId.toString(), true);

      PeerChanges change = cluster.removePeers(1, false, Collections.emptyList());

      AtomicBoolean gotNotLeader = new AtomicBoolean(false);
      final Thread clientThread = new Thread(() -> {
        try(final RaftClient client = cluster.createClient(leaderId)) {
          LOG.info("client starts to change conf");
          final RaftClientRpc sender = client.getClientRpc();
          RaftClientReply reply = sender.sendRequest(cluster.newSetConfigurationRequest(
              client.getId(), leaderId, change.getPeersInNewConf()));
          if (reply.getNotLeaderException() != null) {
            gotNotLeader.set(true);
          }
        } catch (IOException e) {
          LOG.warn("Got unexpected exception when client1 changes conf", e);
        }
      });
      clientThread.start();

      // find ConfigurationEntry
      final TimeDuration sleepTime = TimeDuration.valueOf(500, TimeUnit.MILLISECONDS);
      final long confIndex = JavaUtils.attemptRepeatedly(() -> {
        final long last = log.getLastEntryTermIndex().getIndex();
        for (long i = last; i >= 1; i--) {
          if (log.get(i).hasConfigurationEntry()) {
            return i;
          }
        }
        throw new Exception("ConfigurationEntry not found: last=" + last);
      }, 10, sleepTime, "confIndex", LOG);

      // wait till the old leader persist the new conf
      JavaUtils.attemptRepeatedly(() -> {
        Assertions.assertTrue(log.getFlushIndex() >= confIndex);
        return null;
      }, 10, sleepTime, "FLUSH", LOG);
      final long committed = log.getLastCommittedIndex();
      Assertions.assertTrue(committed < confIndex);

      // unblock the old leader
      BlockRequestHandlingInjection.getInstance().unblockReplier(leaderId.toString());
      cluster.setBlockRequestsFrom(leaderId.toString(), false);

      // the client should get NotLeaderException
      clientThread.join(5000);
      Assertions.assertTrue(gotNotLeader.get());

      // the old leader should have truncated the setConf from the log
      JavaUtils.attemptRepeatedly(() -> {
        Assertions.assertTrue(log.getLastCommittedIndex() >= confIndex);
        return null;
      }, 10, ONE_SECOND, "COMMIT", LOG);
      Assertions.assertTrue(log.get(confIndex).hasConfigurationEntry());
      log2 = null;
    } finally {
      RaftStorageTestUtils.printLog(log2, s -> LOG.info(s));
    }
  }

  /**
   * Delay the commit of the leader placeholder log entry and see if the client
   * can correctly receive and handle the LeaderNotReadyException.
   */
  @Test
  public void testLeaderNotReadyException() throws Exception {
    LOG.info("Start testLeaderNotReadyException");
    final MiniRaftCluster cluster = newCluster(1).initServers();
    try {
      // delay 1s for each logSync call
      cluster.getServers().forEach(
          peer -> LEADER_PLACE_HOLDER_DELAY.setDelayMs(peer.getId().toString(), 2000));
      cluster.start();

      AtomicBoolean caughtNotReady = new AtomicBoolean(false);
      AtomicBoolean success = new AtomicBoolean(false);
      final RaftPeerId leaderId = cluster.getPeers().iterator().next().getId();
      new Thread(() -> {
        try (final RaftClient client = cluster.createClient(leaderId)) {
          final RaftClientRpc sender = client.getClientRpc();
          final RaftClientRequest request = cluster.newRaftClientRequest(
                  client.getId(), leaderId, new SimpleMessage("test"));
          while (!success.get()) {
            try {
              final RaftClientReply reply = sender.sendRequest(request);
              success.set(reply.isSuccess());
              if (reply.getException() != null && reply.getException() instanceof LeaderNotReadyException) {
                caughtNotReady.set(true);
              }
            } catch (IOException e) {
              LOG.info("Hit other IOException", e);
            }
            if (!success.get()) {
              try {
                Thread.sleep(200);
              } catch (InterruptedException ignored) {
                Thread.currentThread().interrupt();
              }
            }
          }
        } catch (Exception ignored) {
          LOG.warn("{} is ignored", JavaUtils.getClassSimpleName(ignored.getClass()), ignored);
        }
      }).start();

      RaftTestUtil.waitForLeader(cluster);
      for (int i = 0; !success.get() && i < 5; i++) {
        Thread.sleep(1000);
      }
      Assertions.assertTrue(success.get());
      Assertions.assertTrue(caughtNotReady.get());
    } finally {
      LEADER_PLACE_HOLDER_DELAY.clear();
      cluster.shutdown();
    }
  }
}
