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

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

import static org.junit.Assert.*;

import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.*;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Region;
import org.apache.geode.cache30.*;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.ThreadUtils;
import org.apache.geode.test.dunit.VM;

/**
 * 
 * This class tests PRID generation in multiple partiton regions on 4 VMs
 */
@Category(DistributedTest.class)
public class PartitionedRegionPRIDDUnitTest extends PartitionedRegionDUnitTestCase {

  /** Maximum number of regions * */
  public final static int MAX_REGIONS = 1;

  /** redundancy used for the creation of the partition region */
  // int redundancy = 0;

  /** local maxmemory used for the creation of the partition region */
  int localMaxMemory = 200;

  /** to store references of 4 vms */
  VM vm[] = new VM[4];

  public PartitionedRegionPRIDDUnitTest() {
    super();
  }

  /**
   * This test performs following operations 1. creates 25 partition regions on 3 nodes. 2. creates
   * more 25 partition regions on 4 nodes 3. tests PRID generation
   * 
   */
  @Test
  public void testPRIDGenerationInMultiplePartitionRegion() throws Exception {
    Host host = Host.getHost(0);
    /** creating 4 VMs */
    createVMs(host);

    /** Prefix will be used for naming the partititon Region */
    String prPrefix = "testBucketCreationInMultiPlePartitionRegion";

    /** these indices represents range of partition regions present in each VM */
    int startIndexForRegion = 0;
    int endIndexForRegion = MAX_REGIONS;

    int startIndexForNode;
    int endIndexForNode;
    List vmList = new ArrayList();
    startIndexForNode = 0;
    endIndexForNode = 3;
    addNodeToList(vmList, startIndexForNode, endIndexForNode);
    localMaxMemory = 200;
    final int redundancy = 0;
    // Create 1/2 * MAX_REGIONS regions in VM 0,1,2 with scope D_ACK.
    createPartitionRegion(vmList, startIndexForRegion, endIndexForRegion, localMaxMemory,
        redundancy, prPrefix);
    LogWriterUtils.getLogWriter().info(
        "testPRIDGenerationInMultiplePartitionRegion() - Partition regions on 3 nodes successfully created");

    startIndexForRegion = MAX_REGIONS;
    endIndexForRegion = 2 * MAX_REGIONS;
    // creating partition regions on 4 with scope DISTRIBUTED_ACK
    startIndexForNode = 0;
    endIndexForNode = 4;
    addNodeToList(vmList, startIndexForNode, endIndexForNode);
    localMaxMemory = 200;
    final int pr2_redundancy = 1;
    // Create MAX_REGION regions on VM 0,1,2,3.
    // VM 3 contains regions from id MAX_REGIONS to 2*MAX_REGIONS only.
    createPartitionRegion(vmList, startIndexForRegion, endIndexForRegion, localMaxMemory,
        pr2_redundancy, prPrefix);
    LogWriterUtils.getLogWriter().info(
        "testPRIDGenerationInMultiplePartitionRegion() - Partition regions on 4 nodes successfully created");
    // validating PRID generation for multiple partition regions
    int AsyncInvocationArrSize = 4;
    AsyncInvocation[] async = new AsyncInvocation[AsyncInvocationArrSize];
    async[0] = vm[0].invokeAsync(validatePRIDCreation(0, endIndexForRegion, prPrefix));
    async[1] = vm[1].invokeAsync(validatePRIDCreation(0, endIndexForRegion, prPrefix));
    async[2] = vm[2].invokeAsync(validatePRIDCreation(0, endIndexForRegion, prPrefix));
    async[3] = vm[3].invokeAsync(validatePRIDCreation(MAX_REGIONS, endIndexForRegion, prPrefix));

    /** main thread is waiting for the other threads to complete */
    for (int count = 0; count < AsyncInvocationArrSize; count++) {
      ThreadUtils.join(async[count], 30 * 1000);
    }

    for (int count = 0; count < AsyncInvocationArrSize; count++) {
      if (async[count].exceptionOccurred()) {
        Assert.fail("VM " + count + " encountered this exception during async invocation",
            async[count].getException());
      }
    }
  }


  /**
   * This function perfoms following checks on PRID creation 1. PRID generated should be between 0
   * to number of partition regions in distributed system 2. PRID should be unique for the partition
   * regions
   * 
   * @param startIndexForRegion
   * @param endIndexForRegion
   * @return
   */
  private CacheSerializableRunnable validatePRIDCreation(final int startIndexForRegion,
      final int endIndexForRegion, final String prPrefix) {
    CacheSerializableRunnable validatePRID = new CacheSerializableRunnable("validatePRIDCreation") {
      String innerPrPrefix = prPrefix;

      public void run2() {
        int noPartitionRegions = endIndexForRegion - startIndexForRegion;
        Cache cache = getCache();
        // getting root region
        Region root =
            cache.getRegion(Region.SEPARATOR + PartitionedRegionHelper.PR_ROOT_REGION_NAME);
        // root region should niot be null
        assertNotNull("Root region can not be null", root);
        // getting allParititionedRegions
        // Region allPartitionedRegions = root
        // .getSubregion(PartitionedRegionHelper.PARTITIONED_REGION_CONFIG_NAME);
        // allPartitionedRegion should not be null
        // assertNotNull("allPartitionedRegion can not be null",
        // allPartitionedRegions);
        // scope of all partition region should be DISTRIBUTED_ACK
        List prIdList = new ArrayList();
        for (int i = startIndexForRegion; i < endIndexForRegion; i++) {
          final String rName = Region.SEPARATOR + innerPrPrefix + i;
          PartitionedRegion pr = (PartitionedRegion) cache.getRegion(rName);
          assertNotNull("This Partitioned Region " + rName + " cannot be null", pr);
          PartitionRegionConfig prConfig =
              (PartitionRegionConfig) root.get(pr.getRegionIdentifier());
          assertNotNull("PRConfig for Partitioned Region " + rName + " can not be null", prConfig);
          prIdList.add(Integer.toString(prConfig.getPRId()));

          // this partition region should present in prIdToPr
          /*
           * if (PartitionedRegion.prIdToPR.containsKey(Integer.toString(prConfig .getPRId())) ==
           * false) fail("this partition region is not present in the prIdToPR map " +
           * pr.getName());
           */
        }

        // checking uniqueness of prId in allPartitionRegion
        SortedSet prIdSet = new TreeSet(prIdList);
        if (prIdSet.size() != prIdList.size())
          fail("Duplicate PRID are generated");

        // prId generated should be between 0 to number of partition regions-1
        Iterator prIdSetItr = prIdSet.iterator();
        while (prIdSetItr.hasNext()) {
          int val = Integer.parseInt((String) prIdSetItr.next());
          if (val > noPartitionRegions - 1 & val < 0) {
            fail("PRID limit is out of range");
          }
        }
        // no of PRID generated in allPartitionRegion should be equal to number of partition region
        if (prIdSet.size() != noPartitionRegions)
          fail("Different PRID generated equal to " + prIdSet.size());

        // no of PRID generated in prIdToPR should be equal to number of partition region
        if (PartitionedRegion.prIdToPR.size() != noPartitionRegions)
          fail("number of entries in the prIdToPR is " + PartitionedRegion.prIdToPR.size());

        // checking uniqueness of prId in prIdToPR
        SortedSet prIdPRSet = new TreeSet(PartitionedRegion.prIdToPR.keySet());
        if (prIdPRSet.size() != PartitionedRegion.prIdToPR.size())
          fail("Duplicate PRID are generated in prIdToPR");

        LogWriterUtils.getLogWriter().info("Size of allPartition region : " + prIdSet.size());
        LogWriterUtils.getLogWriter().info("Size of prIdToPR region     : " + prIdPRSet.size());
        LogWriterUtils.getLogWriter().info("PRID generated successfully");
      }
    };
    return validatePRID;
  }

  /**
   * This function createas multiple partition regions on nodes specified in the vmList
   */
  private void createPartitionRegion(List vmList, int startIndexForRegion, int endIndexForRegion,
      int localMaxMemory, int redundancy, String prPrefix) throws Exception {
    int AsyncInvocationArrSize = 4;
    AsyncInvocation[] async = new AsyncInvocation[AsyncInvocationArrSize];
    int numNodes = 0;
    Iterator nodeIterator = vmList.iterator();
    while (nodeIterator.hasNext()) {
      VM vm = (VM) nodeIterator.next();
      async[numNodes] = vm.invokeAsync(createMultiplePartitionRegion(prPrefix, startIndexForRegion,
          endIndexForRegion, redundancy, localMaxMemory));
      numNodes++;
    }
    for (int i = 0; i < numNodes; i++) {
      ThreadUtils.join(async[i], 30 * 1000);
    }

    for (int i = 0; i < numNodes; i++) {
      if (async[i].exceptionOccurred()) {
        Assert.fail("VM " + i + " encountered this exception during async invocation",
            async[i].getException());
      }
    }
  }

  /**
   * This function adds nodes to node list in the range specified by startIndexForNode and
   * endIndexForNode
   */
  private void addNodeToList(List vmList, int startIndexForNode, int endIndexForNode) {
    vmList.clear();
    for (int i = startIndexForNode; i < endIndexForNode; i++) {
      vmList.add(vm[i]);
    }
  }

  /** creates 4 VMS on the given host */
  private void createVMs(Host host) {
    for (int i = 0; i < 4; i++) {
      vm[i] = host.getVM(i);
    }
  }
}
