/*
 * 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 static org.apache.geode.distributed.ConfigurationProperties.SERIALIZABLE_OBJECT_FILTER;
import static org.apache.geode.internal.cache.PartitionedRegionGetSomeKeys.getSomeKeys;
import static org.apache.geode.test.dunit.Host.getHost;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.Set;

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

import org.apache.geode.DataSerializable;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache30.CacheSerializableRunnable;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.CacheTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

/**
 * Confirm that the utils used for testing work as advertised
 *
 * @since GemFire 5.0
 */
@Category(DistributedTest.class)
public class PartitionedRegionTestUtilsDUnitTest extends CacheTestCase {

  private static final int TOTAL_NUM_BUCKETS = 5;
  private static final int MAX_KEYS = 50;

  private String regionName;

  private VM vm0;
  private VM vm1;
  private VM vm2;
  private VM vm3;

  @Before
  public void setUp() {
    vm0 = getHost(0).getVM(0);
    vm1 = getHost(0).getVM(1);
    vm2 = getHost(0).getVM(2);
    vm3 = getHost(0).getVM(3);

    regionName = getUniqueName();
  }

  @Override
  public Properties getDistributedSystemProperties() {
    Properties config = new Properties();
    config.put(SERIALIZABLE_OBJECT_FILTER, TestPRKey.class.getName());
    return config;
  }

  /**
   * Test the {@link PartitionedRegionGetSomeKeys#getSomeKeys(PartitionedRegion, Random)} method,
   * making sure it returns keys when there are keys and {@link java.util.Collections#EMPTY_SET}
   * when there are none.
   */
  @Test
  public void testGetKeys() throws Exception {
    vm0.invoke(() -> createPartitionedRegion());
    vm1.invoke(() -> createPartitionedRegion());
    vm2.invoke(() -> createPartitionedRegion());

    vm0.invoke(() -> {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      Random random = new Random(123);
      // Assert that its empty
      for (int i = 0; i < 5; i++) {
        Set someKeys = getSomeKeys(partitionedRegion, random);
        assertNotNull(someKeys);
        assertTrue(someKeys.isEmpty());
      }

      for (int i = 0; i < MAX_KEYS; i++) {
        partitionedRegion.put("testKey" + i, i);
      }

      // Assert not empty and has value in an acceptable range
      for (int i = 0; i < 5; i++) {
        Set someKeys = getSomeKeys(partitionedRegion, random);
        assertNotNull(someKeys);
        assertFalse(someKeys.isEmpty());
        for (Object key : someKeys) {
          Integer val = (Integer) partitionedRegion.get(key);
          assertNotNull(val);
          assertTrue(val >= 0);
          assertTrue(val < MAX_KEYS);
        }
      }
    });
  }

  @Test
  public void testGetNodes() throws Exception {
    VM validatorVM = vm2;

    validatorVM.invoke(() -> createPRAndTestGetAllNodes());

    validatorVM.invoke(() -> {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      Set<InternalDistributedMember> allNodes = partitionedRegion.getAllNodes();
      assertThat(allNodes).isNotNull().hasSize(1);
    });

    vm0.invoke(() -> createPRAndTestGetAllNodes());

    validatorVM.invoke(() -> {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      Set<InternalDistributedMember> allNodes = partitionedRegion.getAllNodes();
      assertThat(allNodes).isNotNull().hasSize(2);
    });

    vm1.invoke(() -> createPRAndTestGetAllNodes());

    validatorVM.invoke(() -> {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      Set<InternalDistributedMember> allNodes = partitionedRegion.getAllNodes();
      assertThat(allNodes).isNotNull().hasSize(3);
    });
  }

  /**
   * Test the test utilities that allow investigation of a PartitionedRegion's local cache.
   */
  @Test
  public void testLocalCacheOps() throws Exception {
    vm0.invoke(() -> {
      PartitionAttributesFactory paf = new PartitionAttributesFactory();
      paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);
      paf.setLocalMaxMemory(8);
      RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);
      regionFactory.setPartitionAttributes(paf.create());
      regionFactory.create(regionName);
    });

    vm2.invoke(() -> {
      PartitionAttributesFactory paf = new PartitionAttributesFactory();
      paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);
      paf.setLocalMaxMemory(0);
      RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);
      regionFactory.setPartitionAttributes(paf.create());
      PartitionedRegion partitionedRegion = (PartitionedRegion) regionFactory.create(regionName);

      String key3 = "lcKey3";
      String val3 = "lcVal3";
      String key4 = "lcKey4";
      String val4 = "lcVal4";

      // Test localCacheContainsKey
      assertThat(partitionedRegion.localCacheContainsKey(key3)).isFalse();
      assertThat(partitionedRegion.localCacheContainsKey(key4)).isFalse();
      partitionedRegion.put(key3, val3);
      assertThat(partitionedRegion.localCacheContainsKey(key3)).isFalse();
      assertThat(partitionedRegion.localCacheContainsKey(key4)).isFalse();
      assertThat(partitionedRegion.get(key3)).isEqualTo(val3);
      assertThat(partitionedRegion.localCacheContainsKey(key3)).isFalse();
      assertThat(partitionedRegion.localCacheContainsKey(key4)).isFalse();

      // test localCacheKeySet
      Set localCacheKeySet = partitionedRegion.localCacheKeySet();
      assertThat(localCacheKeySet.contains(key3)).isFalse();
      assertThat(localCacheKeySet.contains(key4)).isFalse();

      // test localCacheGet
      assertThat(partitionedRegion.localCacheGet(key3)).isNull();
      assertThat(partitionedRegion.localCacheGet(key4)).isNull();
      partitionedRegion.put(key4, val4);
      assertThat(partitionedRegion.localCacheGet(key4)).isNull();
      assertThat(partitionedRegion.get(key4)).isEqualTo(val4);
      assertThat(partitionedRegion.localCacheGet(key4)).isNull();
    });
  }

  /**
   * Test the test method PartitionedRegion.getAllNodes Verify that it returns nodes after a value
   * has been placed into the PartitionedRegion.
   *
   * @see PartitionedRegion#getAllNodes()
   */
  @Test
  public void testGetBucketKeys() throws Exception {
    vm2.invoke(() -> createPartitionedRegion());
    vm3.invoke(() -> createPartitionedRegion());

    // Create an accessor
    PartitionAttributesFactory paf = new PartitionAttributesFactory();
    paf.setLocalMaxMemory(0);
    paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);

    RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);
    regionFactory.setPartitionAttributes(paf.create());

    PartitionedRegion partitionedRegion = (PartitionedRegion) regionFactory.create(regionName);

    int numberOfBuckets = partitionedRegion.getTotalNumberOfBuckets();

    for (int whichBucket = numberOfBuckets - 1; whichBucket >= 0; whichBucket--) {
      Set bucketKeys = partitionedRegion.getBucketKeys(whichBucket);
      assertThat(bucketKeys).isEmpty();
    }

    // Create bucket number of keys, assuming a mod per key hashCode
    // There should be one key per bucket
    partitionedRegion.put(new TestPRKey(0, 1), 0);
    partitionedRegion.put(new TestPRKey(0, 2), 1);
    partitionedRegion.put(new TestPRKey(0, 3), 2);

    Set<TestPRKey> bucketKeys = partitionedRegion.getBucketKeys(0);

    assertThat(bucketKeys).hasSize(3);
    assertThat(bucketKeys.iterator().next().hashCode()).isEqualTo(0);
    assertThat(bucketKeys.iterator().next().hashCode()).isEqualTo(0);
    assertThat(bucketKeys.iterator().next().hashCode()).isEqualTo(0);

    // Skip bucket zero since we have three keys there, but fill out all the rest with keys
    for (int whichBucket = numberOfBuckets - 1; whichBucket > 0; whichBucket--) {
      TestPRKey key = new TestPRKey(whichBucket, 0);
      partitionedRegion.put(key, whichBucket);
    }

    // Assert that the proper number of keys are placed in each bucket
    for (int whichBucket = 1; whichBucket < numberOfBuckets; whichBucket++) {
      bucketKeys = partitionedRegion.getBucketKeys(whichBucket);
      assertThat(bucketKeys).hasSize(1);
      TestPRKey key = bucketKeys.iterator().next();
      assertThat(key.hashCode()).isEqualTo(whichBucket);
    }
  }

  /**
   * Test the test method {@link PartitionedRegion#getBucketOwnersForValidation(int)} Verify that
   * the information it discovers is the same as the local advisor.
   */
  @Test
  public void testGetBucketOwners() throws Exception {
    String regionName1 = getUniqueName() + "-r0";
    String regionName2 = getUniqueName() + "-r1";
    String regionName3 = getUniqueName() + "-r2";
    String[] regions = {regionName1, regionName2, regionName3};

    int numberOfBuckets = 3;

    VM datastore1VM = vm2;
    VM datastore2VM = vm3;
    VM datastore3VM = vm0;
    VM accessorVM = vm1;

    datastore1VM
        .invoke(() -> createPartitionedRegionsWithIncreasingRedundancy(numberOfBuckets, regions));
    datastore2VM
        .invoke(() -> createPartitionedRegionsWithIncreasingRedundancy(numberOfBuckets, regions));
    datastore3VM
        .invoke(() -> createPartitionedRegionsWithIncreasingRedundancy(numberOfBuckets, regions));

    accessorVM.invoke(() -> {
      PartitionAttributesFactory paf = new PartitionAttributesFactory();
      paf.setLocalMaxMemory(0);
      paf.setTotalNumBuckets(numberOfBuckets);

      RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);

      for (int redundancy = 0; redundancy < regions.length; redundancy++) {
        paf.setRedundantCopies(redundancy);
        regionFactory.setPartitionAttributes(paf.create());
        Region region = regionFactory.create(regions[redundancy]);
        assertThat(region.size()).isEqualTo(0);
      }
    });

    datastore1VM.invoke(() -> validateNoBucketOwners(regions, numberOfBuckets));
    datastore2VM.invoke(() -> validateNoBucketOwners(regions, numberOfBuckets));
    datastore3VM.invoke(() -> validateNoBucketOwners(regions, numberOfBuckets));
    accessorVM.invoke(() -> validateNoBucketOwners(regions, numberOfBuckets));

    accessorVM.invoke(() -> {
      for (String regionName : regions) {
        PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
        assertThat(partitionedRegion.getTotalNumberOfBuckets()).isEqualTo(3);
        // Create one bucket
        partitionedRegion.put(0, "zero");
        assertThat(partitionedRegion.getRegionAdvisor().getCreatedBucketsCount()).isEqualTo(1);
      }
    });

    accessorVM.invoke(() -> validateOneBucketPrimary(regions));
    datastore1VM.invoke(() -> validateOneBucketPrimary(regions));
    datastore2VM.invoke(() -> validateOneBucketPrimary(regions));
    datastore3VM.invoke(() -> validateOneBucketPrimary(regions));
  }

  private void validateOneBucketPrimary(String[] regions) {
    for (String regionName : regions) {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      try {
        for (int bucketId : partitionedRegion.getRegionAdvisor().getBucketSet()) {
          assertThat(partitionedRegion.getRegionAdvisor().getBucketOwners(bucketId))
              .hasSize(partitionedRegion.getRedundantCopies() + 1);

          List primaries = partitionedRegion.getBucketOwnersForValidation(bucketId);
          assertThat(primaries).hasSize(partitionedRegion.getRedundantCopies() + 1);

          int primaryCount = 0;
          for (Object primaryInfo : primaries) {
            Object[] memberAndBoolean = (Object[]) primaryInfo;
            assertThat(memberAndBoolean).hasSize(3); // memberId, isPrimary and hostToken(new)
            assertThat(memberAndBoolean[0]).isInstanceOf(DistributedMember.class);
            assertThat(memberAndBoolean[1].getClass()).isSameAs(Boolean.class);
            Boolean isPrimary = (Boolean) memberAndBoolean[1];
            if (isPrimary) {
              primaryCount++;
            }
          }
          assertThat(primaryCount).isEqualTo(1);
        }
      } catch (ForceReattemptException noGood) {
        Assert.fail("Unexpected force retry", noGood);
      }
    }
  }

  private void validateNoBucketOwners(String[] regions, int numberOfBuckets)
      throws ForceReattemptException {
    for (String regionName : regions) {
      PartitionedRegion partitionedRegion = (PartitionedRegion) getCache().getRegion(regionName);
      assertThat(partitionedRegion.getTotalNumberOfBuckets()).isEqualTo(numberOfBuckets);

      for (int whichBucket = 0; whichBucket < partitionedRegion
          .getTotalNumberOfBuckets(); whichBucket++) {
        assertThat(partitionedRegion.getRegionAdvisor().getBucketOwners(whichBucket)).isEmpty();
        assertThat(partitionedRegion.getBucketOwnersForValidation(whichBucket)).isEmpty();
      }
    }
  }

  private void createPartitionedRegionsWithIncreasingRedundancy(int numberOfBuckets,
      String[] regions) {
    PartitionAttributesFactory paf = new PartitionAttributesFactory();
    paf.setTotalNumBuckets(numberOfBuckets);

    RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);

    for (int redundancy = 0; redundancy < regions.length; redundancy++) {
      paf.setRedundantCopies(redundancy);
      regionFactory.setPartitionAttributes(paf.create());
      Region region = regionFactory.create(regions[redundancy]);
      assertThat(region.size()).isEqualTo(0);
    }
  }

  private void createPartitionedRegion() {
    PartitionAttributesFactory paf = new PartitionAttributesFactory();
    paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);

    RegionFactory regionFactory = getCache().createRegionFactory(RegionShortcut.PARTITION);
    regionFactory.setPartitionAttributes(paf.create());

    regionFactory.create(regionName);
  }

  private void createPRAndTestGetAllNodes() {
    PartitionAttributesFactory paf = new PartitionAttributesFactory();
    paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);

    RegionFactory<Object, Integer> regionFactory =
        getCache().createRegionFactory(RegionShortcut.PARTITION);
    regionFactory.setPartitionAttributes(paf.create());

    Region<Object, Integer> region = regionFactory.create(regionName);

    // For each invocation, create a key that has a sequential hashCode.
    // Putting this key into the PR should force a new bucket allocation on
    // each new VM (assuming a mod on the hashCode), forcing the number of VMs to increase
    // when we call getAllNodes each time this method is called.
    Integer counter = region.get("Counter");
    Integer keyHash;
    if (counter == null) {
      counter = 0;
    } else {
      counter = counter + 1;
    }
    keyHash = counter;
    region.put("Counter", counter);
    region.put(new TestGetNodesKey(keyHash), counter);

    Set<InternalDistributedMember> allNodes = ((PartitionedRegion) region).getAllNodes();
    assertThat(allNodes).isNotNull().isNotEmpty();
  }

  /**
   * Test the test method PartitionedRegion.getAllNodes Verify that it returns nodes after a value
   * has been placed into the PartitionedRegion.
   *
   * @see PartitionedRegion#getAllNodes()
   */
  private static class TestGetNodesKey implements DataSerializable {

    private int hashCode;

    TestGetNodesKey(int hashCode) {
      this.hashCode = hashCode;
    }

    public TestGetNodesKey() {
      // nothing
    }

    public int hashCode() {
      return hashCode;
    }

    @Override
    public void toData(DataOutput out) throws IOException {
      out.writeInt(hashCode);
    }

    @Override
    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      hashCode = in.readInt();
    }
  }

  private static class TestPRKey implements Serializable {

    private final int hashCode;
    private final int differentiator;

    TestPRKey(int hashCode, int differentiator) {
      this.hashCode = hashCode;
      this.differentiator = differentiator;
    }

    public int hashCode() {
      return hashCode;
    }

    public boolean equals(Object obj) {
      return obj instanceof TestPRKey && ((TestPRKey) obj).differentiator == differentiator;
    }

    public String toString() {
      return getClass().getSimpleName() + " " + hashCode + " diff " + differentiator;
    }
  }
}
