/*
 * 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.geode.internal.cache.wan.parallel;

import static org.apache.geode.internal.Assert.fail;
import static org.apache.geode.test.awaitility.GeodeAwaitility.await;
import static org.apache.geode.test.dunit.IgnoredException.addIgnoredException;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.internal.PoolImpl;
import org.apache.geode.internal.cache.execute.data.CustId;
import org.apache.geode.internal.cache.execute.data.Customer;
import org.apache.geode.internal.cache.execute.data.Order;
import org.apache.geode.internal.cache.execute.data.OrderId;
import org.apache.geode.internal.cache.execute.data.Shipment;
import org.apache.geode.internal.cache.execute.data.ShipmentId;
import org.apache.geode.internal.cache.wan.WANTestBase;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.junit.categories.WanTest;

@Category({WanTest.class})
public class ParallelWANStatsDistributedTest extends WANTestBase {

  private static final int NUM_PUTS = 100;
  private static final long serialVersionUID = 1L;

  private String testName;

  public ParallelWANStatsDistributedTest() {
    super();
  }

  @Override
  protected final void postSetUpWANTestBase() {
    testName = getTestMethodName();
  }

  @Test
  public void testConnectionStatsAreCreated() {
    // 1. Create locators
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    // 2. Create cache & receiver in vm2
    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    // 3. Create cache & sender in vm4
    createSenderInVm(lnPort, vm4);

    // 4. Create Region in vm4 (sender)
    createSenderPRInVM(1, vm4);

    // 5. Create region in vm2 (receiver)
    createReceiverPR(vm2, 1);

    // 6. Start sender in vm4
    startSenderInVMs("ln", vm4);

    vm4.invoke(() -> WANTestBase.checkConnectionStats("ln"));
  }

  @Test
  public void testQueueSizeInSecondaryBucketRegionQueuesWithMemberRestart() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    createSendersWithConflation(lnPort);

    createSenderPRs(1);

    startPausedSenders();

    createReceiverPR(vm2, 1);
    putKeyValues();

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));

      // queue size
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(NUM_PUTS);
      // eventsReceived
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(
          NUM_PUTS * 2);
      // events queued
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(
          NUM_PUTS * 2);
      // events distributed
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(0);
      // secondary queue size
      assertThat(v4List.get(10) + v5List.get(10) + v6List.get(10) + v7List.get(10)).isEqualTo(
          NUM_PUTS);

      System.out.println("Current secondary queue sizes:" + v4List.get(10) + ":" + v5List.get(10)
          + ":" + v6List.get(10) + ":" + v7List.get(10));
    });

    // stop vm7 to trigger rebalance and move some primary buckets
    vm7.invoke(WANTestBase::closeCache);
    await().untilAsserted(() -> {
      int v4secondarySize = vm4.invoke(() -> WANTestBase.getSecondaryQueueSizeInStats("ln"));
      int v5secondarySize = vm5.invoke(() -> WANTestBase.getSecondaryQueueSizeInStats("ln"));
      int v6secondarySize = vm6.invoke(() -> WANTestBase.getSecondaryQueueSizeInStats("ln"));
      assertThat(v4secondarySize + v5secondarySize + v6secondarySize).isEqualTo(NUM_PUTS);
      System.out
          .println("New secondary queue sizes:" + v4secondarySize + ":" + v5secondarySize + ":"
              + v6secondarySize);
    });

    vm7.invoke(() -> WANTestBase.createCache(lnPort));
    vm7.invoke(() -> WANTestBase.createSender("ln", 2, true, 100, 10, true, false, null, true));
    vm7.invoke(() -> WANTestBase.createPartitionedRegion(testName, "ln", 1, 10, isOffHeap()));
    startSenderInVMs("ln", vm7);
    vm7.invoke(() -> pauseSender("ln"));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      // secondary queue size
      assertThat(v4List.get(10) + v5List.get(10) + v6List.get(10) + v7List.get(10)).isEqualTo(
          NUM_PUTS);
      System.out.println("After restart vm7, secondary queue sizes:" + v4List.get(10) + ":"
          + v5List.get(10) + ":" + v6List.get(10) + ":" + v7List.get(10));
    });

    vm4.invoke(() -> WANTestBase.resumeSender("ln"));
    vm5.invoke(() -> WANTestBase.resumeSender("ln"));
    vm6.invoke(() -> WANTestBase.resumeSender("ln"));
    vm7.invoke(() -> WANTestBase.resumeSender("ln"));

    vm2.invoke(() -> WANTestBase.validateRegionSize(testName, NUM_PUTS));
    vm2.invoke(() -> WANTestBase.checkGatewayReceiverStats(0, NUM_PUTS, NUM_PUTS));

    vm4.invoke(() -> WANTestBase.checkQueueSize("ln", 0));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(NUM_PUTS);
      // secondary queue size:
      assertThat(v4List.get(10) + v5List.get(10) + v6List.get(10) + v7List.get(10)).isEqualTo(0);
    });
  }

  // TODO: add a test without redundancy for primary switch
  @Test
  public void testQueueSizeInSecondaryWithPrimarySwitch() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    createSendersWithConflation(lnPort);

    createSenderPRs(1);

    startPausedSenders();

    createReceiverPR(vm2, 1);

    putKeyValues();

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(NUM_PUTS);
      // events received:
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(
          NUM_PUTS * 2);
      // events queued:
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(
          NUM_PUTS * 2);
      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(0);
      // secondary queue size:
      assertThat(v4List.get(10) + v5List.get(10) + v6List.get(10) + v7List.get(10)).isEqualTo(
          NUM_PUTS);
    });

    vm4.invoke(() -> WANTestBase.resumeSender("ln"));
    vm5.invoke(() -> WANTestBase.resumeSender("ln"));
    vm6.invoke(() -> WANTestBase.resumeSender("ln"));
    vm7.invoke(() -> WANTestBase.resumeSender("ln"));
    vm2.invoke(() -> WANTestBase.validateRegionSize(testName, NUM_PUTS));
    vm2.invoke(() -> WANTestBase.checkGatewayReceiverStats(0, NUM_PUTS, NUM_PUTS));

    vm4.invoke(() -> WANTestBase.checkQueueSize("ln", 0));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(NUM_PUTS);
      // secondary queue size:
      assertThat(v4List.get(10) + v5List.get(10) + v6List.get(10) + v7List.get(10)).isEqualTo(0);
    });
  }

  @Test
  public void testPartitionedRegionParallelPropagation_BeforeDispatch() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2, vm3);
    createReceiverInVMs(vm2, vm3);

    createSendersWithConflation(lnPort);

    createSenderPRs(0);

    startPausedSenders();

    createReceiverPR(vm2, 1);
    createReceiverPR(vm3, 1);

    putKeyValues();

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", NUM_PUTS));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(NUM_PUTS);
      // events received:
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(NUM_PUTS);
      // events queued:
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(NUM_PUTS);
      // events distributed
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(0);
      // batches distributed:
      assertThat(v4List.get(4) + v5List.get(4) + v6List.get(4) + v7List.get(4)).isEqualTo(0);
      // batches redistributed
      assertThat(v4List.get(5) + v5List.get(5) + v6List.get(5) + v7List.get(5)).isEqualTo(0);
    });
  }

  @Test
  public void testPartitionedRegionParallelPropagation_AfterDispatch_NoRedundancy() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    createSenders(lnPort);

    createReceiverPR(vm2, 0);

    createSenderPRs(0);

    startSenderInVMs("ln", vm4, vm5, vm6, vm7);

    vm4.invoke(() -> WANTestBase.doPuts(testName, NUM_PUTS));

    vm2.invoke(() -> WANTestBase.validateRegionSize(testName, NUM_PUTS));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(0);
      // eventsReceived:
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(NUM_PUTS);
      // events queued:
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(NUM_PUTS);
      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(NUM_PUTS);
      // batches distributed:
      assertThat(v4List.get(4) + v5List.get(4) + v6List.get(4) + v7List.get(4))
          .isGreaterThanOrEqualTo(10);
      // batches redistributed:
      assertThat(v4List.get(5) + v5List.get(5) + v6List.get(5) + v7List.get(5)).isEqualTo(0);
    });

    vm2.invoke(() -> WANTestBase.checkGatewayReceiverStats(10, NUM_PUTS, NUM_PUTS));
  }

  @Test
  public void testPRParallelPropagationWithoutGroupTransactionEventsSendsBatchesWithIncompleteTransactions() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    createSenders(lnPort, false);

    createReceiverCustomerOrderShipmentPR(vm2);

    createSenderCustomerOrderShipmentPRs(vm4);
    createSenderCustomerOrderShipmentPRs(vm5);
    createSenderCustomerOrderShipmentPRs(vm6);
    createSenderCustomerOrderShipmentPRs(vm7);

    startSenderInVMs("ln", vm4, vm5, vm6, vm7);

    final Map<Object, Object> custKeyValue = new HashMap<>();
    int intCustId = 1;
    CustId custId = new CustId(intCustId);
    custKeyValue.put(custId, new Customer());
    vm4.invoke(() -> WANTestBase.putGivenKeyValue(customerRegionName, custKeyValue));

    int transactions = 3;
    final Map<Object, Object> keyValues = new HashMap<>();
    for (int i = 0; i < transactions; i++) {
      OrderId orderId = new OrderId(i, custId);
      ShipmentId shipmentId1 = new ShipmentId(i, orderId);
      ShipmentId shipmentId2 = new ShipmentId(i + 1, orderId);
      ShipmentId shipmentId3 = new ShipmentId(i + 2, orderId);
      keyValues.put(orderId, new Order());
      keyValues.put(shipmentId1, new Shipment());
      keyValues.put(shipmentId2, new Shipment());
      keyValues.put(shipmentId3, new Shipment());
    }
    int eventsPerTransaction = 4;
    vm4.invoke(() -> WANTestBase.doOrderAndShipmentPutsInsideTransactions(keyValues,
        eventsPerTransaction));

    int entries = (transactions * eventsPerTransaction) + 1;

    vm4.invoke(() -> WANTestBase.validateRegionSize(customerRegionName, 1));
    vm4.invoke(() -> WANTestBase.validateRegionSize(orderRegionName, transactions));
    vm4.invoke(() -> WANTestBase.validateRegionSize(shipmentRegionName, transactions * 3));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(0);
      // eventsReceived:
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(entries);
      // events queued:
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(entries);
      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(entries);
      // batches distributed:
      assertThat(v4List.get(4) + v5List.get(4) + v6List.get(4) + v7List.get(4)).isEqualTo(2);
      // batches redistributed:
      assertThat(v4List.get(5) + v5List.get(5) + v6List.get(5) + v7List.get(5)).isEqualTo(0);
    });
  }

  @Test
  public void testPRParallelPropagationWithGroupTransactionEventsWithoutBatchRedistributionSendsBatchesWithCompleteTransactions_SeveralClients() {
    testPRParallelPropagationWithGroupTransactionEventsSendsBatchesWithCompleteTransactions_SeveralClients(
        false);
  }

  @Test
  public void testPRParallelPropagationWithGroupTransactionEventsWithBatchRedistributionSendsBatchesWithCompleteTransactions_SeveralClients() {
    testPRParallelPropagationWithGroupTransactionEventsSendsBatchesWithCompleteTransactions_SeveralClients(
        true);
  }

  public void testPRParallelPropagationWithGroupTransactionEventsSendsBatchesWithCompleteTransactions_SeveralClients(
      boolean isBatchesRedistributed) {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);

    if (!isBatchesRedistributed) {
      createReceiverInVMs(vm2);
    }

    createSenders(lnPort, true);

    createReceiverCustomerOrderShipmentPR(vm2);

    createSenderCustomerOrderShipmentPRs(vm4);
    createSenderCustomerOrderShipmentPRs(vm5);
    createSenderCustomerOrderShipmentPRs(vm6);
    createSenderCustomerOrderShipmentPRs(vm7);

    startSenderInVMs("ln", vm4, vm5, vm6, vm7);

    int clients = 4;
    int transactions = 300;
    // batchSize is 10. Each transaction will contain 1 order + 3 shipments = 4 events.
    // As a result, all batches will contain extra events to complete the
    // transactions it will deliver.
    int shipmentsPerTransaction = 3;

    final List<Map<Object, Object>> customerData = new ArrayList<>(clients);
    for (int intCustId = 0; intCustId < clients; intCustId++) {
      final Map<Object, Object> custKeyValue = new HashMap<>();
      CustId custId = new CustId(intCustId);
      custKeyValue.put(custId, new Customer());
      customerData.add(new HashMap<>());
      vm4.invoke(() -> WANTestBase.putGivenKeyValue(customerRegionName, custKeyValue));

      for (int i = 0; i < transactions; i++) {
        OrderId orderId = new OrderId(i, custId);
        customerData.get(intCustId).put(orderId, new Order());
        for (int j = 0; j < shipmentsPerTransaction; j++) {
          customerData.get(intCustId).put(new ShipmentId(i + j, orderId), new Shipment());
        }
      }
    }

    List<AsyncInvocation<Void>> asyncInvocations = new ArrayList<>(clients);

    int eventsPerTransaction = shipmentsPerTransaction + 1;
    for (int i = 0; i < clients; i++) {
      final int intCustId = i;
      AsyncInvocation<Void> asyncInvocation =
          vm4.invokeAsync(() -> WANTestBase.doOrderAndShipmentPutsInsideTransactions(
              customerData.get(intCustId),
              eventsPerTransaction));
      asyncInvocations.add(asyncInvocation);
    }

    try {
      for (AsyncInvocation<Void> asyncInvocation : asyncInvocations) {
        asyncInvocation.await();
      }
    } catch (InterruptedException e) {
      fail("Interrupted");
    }

    vm4.invoke(() -> WANTestBase.validateRegionSize(customerRegionName, clients));
    vm4.invoke(() -> WANTestBase.validateRegionSize(orderRegionName, transactions * clients));
    vm4.invoke(() -> WANTestBase.validateRegionSize(shipmentRegionName,
        transactions * shipmentsPerTransaction * clients));

    if (isBatchesRedistributed) {
      // wait for batches to be redistributed and then start the receiver
      vm4.invoke(() -> await()
          .until(() -> WANTestBase.getSenderStats("ln", -1).get(5) > 0));
      createReceiverInVMs(vm2);
    }

    // Check that all entries have been written in the receiver
    vm2.invoke(
        () -> WANTestBase.validateRegionSize(customerRegionName, clients));
    vm2.invoke(
        () -> WANTestBase.validateRegionSize(orderRegionName, transactions * clients));
    vm2.invoke(
        () -> WANTestBase.validateRegionSize(shipmentRegionName,
            shipmentsPerTransaction * transactions * clients));

    checkQueuesAreEmptyAndOnlyCompleteTransactionsAreReplicated(isBatchesRedistributed);
  }

  private void checkQueuesAreEmptyAndOnlyCompleteTransactionsAreReplicated(
      boolean isBatchesRedistributed) {
    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(0);
      // batches redistributed:
      int batchesRedistributed = v4List.get(5) + v5List.get(5) + v6List.get(5) + v7List.get(5);
      if (isBatchesRedistributed) {
        assertThat(batchesRedistributed).isGreaterThan(0);
      } else {
        assertThat(batchesRedistributed).isEqualTo(0);
      }
      // batches with incomplete transactions
      assertThat(v4List.get(13) + v5List.get(13) + v6List.get(13) + v7List.get(7)).isEqualTo(0);
    });

    vm4.invoke(() -> WANTestBase.validateParallelSenderQueueAllBucketsDrained("ln"));
    vm5.invoke(() -> WANTestBase.validateParallelSenderQueueAllBucketsDrained("ln"));
    vm6.invoke(() -> WANTestBase.validateParallelSenderQueueAllBucketsDrained("ln"));
    vm7.invoke(() -> WANTestBase.validateParallelSenderQueueAllBucketsDrained("ln"));
  }

  @Test
  public void testPRParallelPropagationWithGroupTransactionEventsSendsBatchesWithCompleteTransactions() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    createSenders(lnPort, true);

    createReceiverCustomerOrderShipmentPR(vm2);

    createSenderCustomerOrderShipmentPRs(vm4);
    createSenderCustomerOrderShipmentPRs(vm5);
    createSenderCustomerOrderShipmentPRs(vm6);
    createSenderCustomerOrderShipmentPRs(vm7);

    startSenderInVMs("ln", vm4, vm5, vm6, vm7);

    final Map<Object, Object> custKeyValue = new HashMap<>();
    int intCustId = 1;
    CustId custId = new CustId(intCustId);
    custKeyValue.put(custId, new Customer());
    vm4.invoke(() -> WANTestBase.putGivenKeyValue(customerRegionName, custKeyValue));

    int transactions = 3;
    final Map<Object, Object> keyValues = new HashMap<>();
    for (int i = 0; i < transactions; i++) {
      OrderId orderId = new OrderId(i, custId);
      ShipmentId shipmentId1 = new ShipmentId(i, orderId);
      ShipmentId shipmentId2 = new ShipmentId(i + 1, orderId);
      ShipmentId shipmentId3 = new ShipmentId(i + 2, orderId);
      keyValues.put(orderId, new Order());
      keyValues.put(shipmentId1, new Shipment());
      keyValues.put(shipmentId2, new Shipment());
      keyValues.put(shipmentId3, new Shipment());
    }

    // 3 transactions of 4 events each are sent so that the batch would
    // initially contain the first 2 transactions complete and the first
    // 2 events of the last transaction (10 entries).
    // As --group-transaction-events is configured in the senders, the remaining
    // 2 events of the last transaction are added to the batch which makes
    // that only one batch of 12 events is sent.
    int eventsPerTransaction = 4;
    vm4.invoke(() -> WANTestBase.doOrderAndShipmentPutsInsideTransactions(keyValues,
        eventsPerTransaction));

    int entries = (transactions * eventsPerTransaction) + 1;

    vm4.invoke(() -> WANTestBase.validateRegionSize(customerRegionName, 1));
    vm4.invoke(() -> WANTestBase.validateRegionSize(orderRegionName, transactions));
    vm4.invoke(() -> WANTestBase.validateRegionSize(shipmentRegionName, transactions * 3));

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v5List = vm5.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v6List = vm6.invoke(() -> WANTestBase.getSenderStats("ln", 0));
      List<Integer> v7List = vm7.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat(v4List.get(0) + v5List.get(0) + v6List.get(0) + v7List.get(0)).isEqualTo(0);
      // eventsReceived:
      assertThat(v4List.get(1) + v5List.get(1) + v6List.get(1) + v7List.get(1)).isEqualTo(entries);
      // events queued:
      assertThat(v4List.get(2) + v5List.get(2) + v6List.get(2) + v7List.get(2)).isEqualTo(entries);
      // events distributed:
      assertThat(v4List.get(3) + v5List.get(3) + v6List.get(3) + v7List.get(3)).isEqualTo(entries);
      // batches distributed:
      assertThat(v4List.get(4) + v5List.get(4) + v6List.get(4) + v7List.get(4)).isEqualTo(1);
      // batches redistributed:
      assertThat(v4List.get(5) + v5List.get(5) + v6List.get(5) + v7List.get(5)).isEqualTo(0);
      // events not queued conflated:
      assertThat(v4List.get(7) + v5List.get(7) + v6List.get(7) + v7List.get(7)).isEqualTo(0);
      // batches with incomplete transactions
      assertThat((int) v4List.get(13)).isEqualTo(0);
    });

  }

  @Test
  public void testPRParallelPropagationWithGroupTransactionEventsWithIncompleteTransactions() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);
    createReceiverInVMs(vm2);

    int dispThreads = 2;
    createSenderInVm(lnPort, vm4, dispThreads);

    createReceiverPR(vm2, 0);

    createSenderPRInVM(0, vm4);

    startSenderInVMs("ln", vm4);

    // Adding events in transactions
    // Transactions will contain objects assigned to different buckets but given that there is only
    // one server, there will be no TransactionDataNotCollocatedException.
    // With this and by using more than one dispatcher thread, we will provoke that
    // it will be impossible for the batches to have complete transactions as some
    // events for a transaction will be handled by one dispatcher thread and some other events by
    // another thread.
    final Map<Object, Object> keyValue = new HashMap<>();
    int entries = 30;
    for (int i = 0; i < entries; i++) {
      keyValue.put(i, i);
    }

    int entriesPerTransaction = 3;
    vm4.invoke(
        () -> WANTestBase.doPutsInsideTransactions(testName, keyValue, entriesPerTransaction));

    vm4.invoke(() -> WANTestBase.validateRegionSize(testName, entries));

    // The number of batches will be 4 because each
    // dispatcher thread (there are 2) will send half the number of entries,
    // each on 2 batches.
    final int batches = 4;

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat((int) v4List.get(0)).isEqualTo(0);
      // eventsReceived:
      assertThat((int) v4List.get(1)).isEqualTo(entries);
      // events queued:
      assertThat((int) v4List.get(2)).isEqualTo(entries);
      // events distributed:
      assertThat((int) v4List.get(3)).isEqualTo(entries);
      // batches distributed:
      assertThat((int) v4List.get(4)).isEqualTo(batches);
      // batches redistributed:
      assertThat((int) v4List.get(5)).isEqualTo(0);
      // events not queued conflated:
      assertThat((int) v4List.get(7)).isEqualTo(0);
      // batches with incomplete transactions
      assertThat((int) v4List.get(13)).isEqualTo(batches);
    });

    vm2.invoke(() -> WANTestBase.checkGatewayReceiverStats(batches, entries, entries));
  }


  @Test
  public void testPRParallelPropagationWithBatchRedistWithoutGroupTransactionEventsSendsBatchesWithIncompleteTransactions() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);

    createSenders(lnPort, false);

    createSenderCustomerOrderShipmentPRs(vm4);

    startSenderInVMs("ln", vm4);

    final Map<Object, Object> custKeyValue = new HashMap<>();
    int intCustId = 1;
    CustId custId = new CustId(intCustId);
    custKeyValue.put(custId, new Customer());
    vm4.invoke(() -> WANTestBase.putGivenKeyValue(customerRegionName, custKeyValue));

    int transactions = 6;
    final Map<Object, Object> keyValues = new HashMap<>();
    for (int i = 0; i < transactions; i++) {
      OrderId orderId = new OrderId(i, custId);
      ShipmentId shipmentId1 = new ShipmentId(i, orderId);
      ShipmentId shipmentId2 = new ShipmentId(i + 1, orderId);
      ShipmentId shipmentId3 = new ShipmentId(i + 2, orderId);
      keyValues.put(orderId, new Order());
      keyValues.put(shipmentId1, new Shipment());
      keyValues.put(shipmentId2, new Shipment());
      keyValues.put(shipmentId3, new Shipment());
    }
    int eventsPerTransaction = 4;
    vm4.invoke(() -> WANTestBase.doOrderAndShipmentPutsInsideTransactions(keyValues,
        eventsPerTransaction));

    int entries = (transactions * eventsPerTransaction) + 1;

    createReceiverCustomerOrderShipmentPR(vm2);

    vm4.invoke(() -> WANTestBase.validateRegionSize(customerRegionName, 1));
    vm4.invoke(() -> WANTestBase.validateRegionSize(orderRegionName, transactions));
    vm4.invoke(() -> WANTestBase.validateRegionSize(shipmentRegionName, transactions * 3));

    // wait for batches to be redistributed and then start the receiver
    vm4.invoke(() -> await()
        .until(() -> WANTestBase.getSenderStats("ln", -1).get(5) > 0));

    createReceiverInVMs(vm2);

    await().untilAsserted(() -> {
      List<Integer> v4List = vm4.invoke(() -> WANTestBase.getSenderStats("ln", 0));

      // queue size:
      assertThat((int) v4List.get(0)).isEqualTo(0);
      // events received:
      assertThat((int) v4List.get(1)).isEqualTo(entries);
      // events queued:
      assertThat((int) v4List.get(2)).isEqualTo(entries);
      // events distributed:
      assertThat((int) v4List.get(3)).isEqualTo(entries);
      // batches distributed:
      assertThat((int) v4List.get(4)).isEqualTo(3);
      // batches redistributed:
      assertThat(v4List.get(5)).as("Batch was not redistributed").isGreaterThan(0);
    });
  }

  @Test
  public void testPRParallelPropagationWithBatchRedistWithGroupTransactionEventsSendsBatchesWithCompleteTransactions() {
    Integer lnPort = vm0.invoke(() -> WANTestBase.createFirstLocatorWithDSId(1));
    Integer nyPort = vm1.invoke(() -> WANTestBase.createFirstRemoteLocator(2, lnPort));

    createCacheInVMs(nyPort, vm2);

    createSenders(lnPort, true);

    createReceiverCustomerOrderShipmentPR(vm2);

    createSenderCustomerOrderShipmentPRs(vm4);

    startSenderInVMs("ln", vm4);


    final Map<Object, Object> custKeyValue = new HashMap<>();
    int intCustId = 1;
    CustId custId = new CustId(intCustId);
    custKeyValue.put(custId, new Customer());
    vm4.invoke(() -> WANTestBase.putGivenKeyValue(customerRegionName, custKeyValue));

    int transactions = 6;
    final Map<Object, Object> keyValues = new HashMap<>();
    for (int i = 0; i < transactions; i++) {
      OrderId orderId = new OrderId(i, custId);
      ShipmentId shipmentId1 = new ShipmentId(i, orderId);
      ShipmentId shipmentId2 = new ShipmentId(i + 1, orderId);
      ShipmentId shipmentId3 = new ShipmentId(i + 2, orderId);
      keyValues.put(orderId, new Order());
      keyValues.put(shipmentId1, new Shipment());
      keyValues.put(shipmentId2, new Shipment());
      keyValues.put(shipmentId3, new Shipment());
    }

    // 6 transactions of 4 events each are sent so that the first batch
