/*
 * 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.junit.Assert.*;

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.Random;
import java.util.Set;

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.cache30.CacheSerializableRunnable;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.VM;
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 PartitionedRegionDUnitTestCase {

  final int totalNumBuckets = 5;

  /**
   * Test the {@link PartitionedRegion#getSomeKeys(java.util.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 {
    final String r = getUniqueName();
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);
    VM vm2 = host.getVM(2);

    CacheSerializableRunnable create = new CacheSerializableRunnable("CreatePartitionedRegion") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        attr.setPartitionAttributes(
            new PartitionAttributesFactory().setTotalNumBuckets(totalNumBuckets).create());
        Region p = cache.createRegion(r, attr.create());
        assertNotNull(p);
        assertTrue(!p.isDestroyed());
        assertNull(p.get("Key"));
      }
    };

    vm0.invoke(create);
    vm1.invoke(create);
    vm2.invoke(create);

    vm0.invoke(new CacheSerializableRunnable("GetSomeKeys") {
      public void run2() throws CacheException {
        PartitionedRegion pr = (PartitionedRegion) getCache().getRegion(r);
        Random rand = new Random(123);
        // Assert that its empty
        for (int i = 0; i < 5; i++) {
          LogWriterUtils.getLogWriter().info("Invocation " + i + " of getSomeKeys");
          try {
            Set s = null;
            s = pr.getSomeKeys(rand);
            assertNotNull(s);
            assertTrue(s.isEmpty());
          } catch (ClassNotFoundException cnfe) {
            Assert.fail("GetSomeKeys failed with ClassNotFoundException", cnfe);
          } catch (IOException ioe) {
            Assert.fail("GetSomeKeys failed with IOException", ioe);
          }
        }

        final int MAXKEYS = 50;
        for (int i = 0; i < MAXKEYS; i++) {
          pr.put("testKey" + i, new Integer(i));
        }

        // Assert not empty and has value in an accepable range
        for (int i = 0; i < 5; i++) {
          LogWriterUtils.getLogWriter().info("Invocation " + i + " of getSomeKeys");
          try {
            Set s = null;
            s = pr.getSomeKeys(rand);
            assertNotNull(s);
            assertFalse(s.isEmpty());
            Integer val;
            LogWriterUtils.getLogWriter().info("Invocation " + i + " got " + s.size() + " keys");
            for (Iterator it = s.iterator(); it.hasNext();) {
              Object key = it.next();
              LogWriterUtils.getLogWriter().info("Key: " + key);
              val = (Integer) pr.get(key);
              assertNotNull(val);
              assertTrue(val.intValue() >= 0);
              assertTrue(val.intValue() < MAXKEYS);
            }
          } catch (ClassNotFoundException cnfe) {
            Assert.fail("GetSomeKeys failed with ClassNotFoundException", cnfe);
          } catch (IOException ioe) {
            Assert.fail("GetSomeKeys failed with IOException", ioe);
          }
        }
      }
    });
  }

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

    public TestGetNodesKey(int hc) {
      this.hc = hc;
    }

    public TestGetNodesKey() {};

    public int hashCode() {
      return this.hc;
    }

    public void toData(DataOutput out) throws IOException {
      out.writeInt(this.hc);
    }

    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      this.hc = in.readInt();
    }
  }

  @Test
  public void testGetNodes() throws Exception {
    final String r = getUniqueName();
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);
    VM validator = host.getVM(2);

    CacheSerializableRunnable createAndTest =
        new CacheSerializableRunnable("CreatePRAndTestGetAllNodes") {
          public void run2() throws CacheException {
            Cache cache = getCache();
            AttributesFactory attr = new AttributesFactory();
            attr.setPartitionAttributes(
                new PartitionAttributesFactory().setTotalNumBuckets(totalNumBuckets).create());
            PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
            assertNotNull(p);
            assertTrue(!p.isDestroyed());

            // 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 i = (Integer) p.get("Counter");
            final Integer keyHash;
            if (i == null) {
              i = new Integer(0);
            } else {
              i = new Integer(i.intValue() + 1);
            }
            keyHash = i;
            p.put("Counter", i);
            p.put(new TestGetNodesKey(keyHash.intValue()), i);
            Set allN = p.getAllNodes();
            assertNotNull(allN);
            assertTrue(!allN.isEmpty());
          }
        };

    validator.invoke(createAndTest);
    validator.invoke(new CacheSerializableRunnable("AssertGetNodesCreation1") {
      public void run2() throws CacheException {
        PartitionedRegion p = (PartitionedRegion) getCache().getRegion(r);
        assertNotNull(p);
        assertTrue(!p.isDestroyed());
        Set allN = p.getAllNodes();
        assertNotNull(allN);
        assertEquals(1, allN.size());
      }
    });
    vm0.invoke(createAndTest);
    validator.invoke(new CacheSerializableRunnable("AssertGetNodesCreation2") {
      public void run2() throws CacheException {
        PartitionedRegion p = (PartitionedRegion) getCache().getRegion(r);
        assertNotNull(p);
        assertTrue(!p.isDestroyed());
        Set allN = p.getAllNodes();
        assertNotNull(allN);
        assertEquals(2, allN.size());
      }
    });

    vm1.invoke(createAndTest);
    validator.invoke(new CacheSerializableRunnable("AssertGetNodesCreation3") {
      public void run2() throws CacheException {
        PartitionedRegion p = (PartitionedRegion) getCache().getRegion(r);
        assertNotNull(p);
        assertTrue(!p.isDestroyed());
        Set allN = p.getAllNodes();
        assertNotNull(allN);
        assertEquals(3, allN.size());
      }
    });
  }

  /**
   * Test the test utilities that allow investigation of a PartitionedRegion's local cache.
   */
  @Test
  public void testLocalCacheOps() throws Exception {
    final String r = getUniqueName();
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm2 = host.getVM(2);

    vm0.invoke(new CacheSerializableRunnable("CreatePR") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        attr.setPartitionAttributes(new PartitionAttributesFactory()
            .setTotalNumBuckets(totalNumBuckets).setLocalMaxMemory(8).create());

        PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
        assertNotNull(p);
      }
    });

    // TODO enable this test when we have the LocalCache properly implemented -- mthomas 2/23/2006
    // vm1.invoke(new CacheSerializableRunnable("CreatePRWithLocalCacheAndTestOps") {
    // public void run2() throws CacheException
    // {
    // Cache cache = getCache();
    // AttributesFactory attr = new AttributesFactory();
    // attr.setScope(Scope.DISTRIBUTED_ACK);
    // Properties lp = new Properties();
    // lp.setProperty(PartitionAttributesFactory.LOCAL_MAX_MEMORY_PROPERTY, "0");
    // attr.setPartitionAttributes(new PartitionAttributesFactory()
    // .setLocalProperties(lp)
    // .createPartitionAttributes());
    //
    // PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
    // assertNotNull(p);
    //
    // final String key1 = "lcKey1"; final String val1 = "lcVal1";
    // final String key2 = "lcKey2"; final String val2 = "lcVal2";
    // // Test localCacheContainsKey
    // assertFalse(p.localCacheContainsKey(key1));
    // assertFalse(p.localCacheContainsKey(key2));
    // p.put(key1, val1);
    // assertFalse(p.localCacheContainsKey(key1));
    // assertFalse(p.localCacheContainsKey(key2));
    // assertIndexDetailsEquals(val1, p.get(key1));
    // assertTrue(p.localCacheContainsKey(key1));
    // assertFalse(p.localCacheContainsKey(key2));
    //
    // // test localCacheKeySet
    // Set lset = p.localCacheKeySet();
    // assertTrue(lset.contains(key1));
    // assertFalse(lset.contains(key2));
    //
    // // test localCacheGet
    // assertIndexDetailsEquals(val1, p.localCacheGet(key1));
    // assertNull(p.localCacheGet(key2));
    // p.put(key2, val2);
    // assertNull(p.localCacheGet(key2));
    // assertIndexDetailsEquals(val2, p.get(key2));
    // assertIndexDetailsEquals(val2, p.localCacheGet(key2));
    // }
    // });

    vm2.invoke(new CacheSerializableRunnable("CreatePRWithNoLocalCacheAndTestOps") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        attr.setPartitionAttributes(new PartitionAttributesFactory()
            .setTotalNumBuckets(totalNumBuckets).setLocalMaxMemory(0).create());

        PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
        assertNotNull(p);

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

        // Test localCacheContainsKey
        assertFalse(p.localCacheContainsKey(key3));
        assertFalse(p.localCacheContainsKey(key4));
        p.put(key3, val3);
        assertFalse(p.localCacheContainsKey(key3));
        assertFalse(p.localCacheContainsKey(key4));
        assertEquals(val3, p.get(key3));
        assertFalse(p.localCacheContainsKey(key3));
        assertFalse(p.localCacheContainsKey(key4));

        // test localCacheKeySet
        Set lset = p.localCacheKeySet();
        assertFalse(lset.contains(key3));
        assertFalse(lset.contains(key4));

        // test localCacheGet
        assertNull(val3, p.localCacheGet(key3));
        assertNull(p.localCacheGet(key4));
        p.put(key4, val4);
        assertNull(p.localCacheGet(key4));
        assertEquals(val4, p.get(key4));
        assertNull(p.localCacheGet(key4));
      }
    });
  }

  /**
   * 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 {
    final String r = getUniqueName();
    Host host = Host.getHost(0);
    VM vm2 = host.getVM(2);
    VM vm3 = host.getVM(3);

    CacheSerializableRunnable create = new CacheSerializableRunnable("CreatePR") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        attr.setPartitionAttributes(
            new PartitionAttributesFactory().setTotalNumBuckets(totalNumBuckets).create());
        PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
        assertNotNull(p);
      }
    };

    vm2.invoke(create);
    vm3.invoke(create);

    // Create an accessor
    Cache cache = getCache();
    AttributesFactory attr = new AttributesFactory();

    attr.setPartitionAttributes(new PartitionAttributesFactory().setTotalNumBuckets(totalNumBuckets)
        .setLocalMaxMemory(0).create());
    PartitionedRegion p = (PartitionedRegion) cache.createRegion(r, attr.create());
    assertNotNull(p);
    final int totalBucks = p.getTotalNumberOfBuckets();

    for (int i = totalBucks - 1; i >= 0; i--) {
      Set s = p.getBucketKeys(i);
      assertTrue(s.isEmpty());
    }

    class TestPRKey implements Serializable {
      int hashCode;
      int differentiator;

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

      public int hashCode() {
        return hashCode;
      }

      public boolean equals(Object obj) {
        if (!(obj instanceof TestPRKey)) {
          return false;
        }
        return ((TestPRKey) obj).differentiator == this.differentiator;
      }

      public String toString() {
        return "TestPRKey " + hashCode + " diff " + differentiator;
      }
    }

    TestPRKey key;
    Integer val;

    // Create bucket number of keys, assuming a modulous per key hashCode
    // There should be one key per bucket
    p.put(new TestPRKey(0, 1), new Integer(0));
    p.put(new TestPRKey(0, 2), new Integer(1));
    p.put(new TestPRKey(0, 3), new Integer(2));
    Set s = p.getBucketKeys(0);
    assertEquals(3, s.size());
    assertEquals(0, ((TestPRKey) s.iterator().next()).hashCode());
    assertEquals(0, ((TestPRKey) s.iterator().next()).hashCode());
    assertEquals(0, ((TestPRKey) s.iterator().next()).hashCode());

    // Skip bucket zero since we have three keys there, but fill out all the rest with keys
    for (int i = totalBucks - 1; i > 0; i--) {
      key = new TestPRKey(i, 0);
      val = new Integer(i);
      p.put(key, val);
      // Integer gottenVal = (Integer) p.get(key);
      // assertIndexDetailsEquals("Value for key: " + key + " val " + gottenVal + " wasn't expected
      // " + val, val, gottenVal);
    }

    // Assert that the proper number of keys are placed in each bucket
    for (int i = 1; i < totalBucks; i++) {
      s = p.getBucketKeys(i);
      assertEquals(s.size(), 1);
      key = (TestPRKey) s.iterator().next();
      assertEquals(i, key.hashCode());
      // assertIndexDetailsEquals(new Integer(i), p.get(key));
    }
  }

  /**
   * 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 {
    final String rName0 = getUniqueName() + "-r0";
    final String rName1 = getUniqueName() + "-r1";
    final String rName2 = getUniqueName() + "-r2";
    final String[] regions = {rName0, rName1, rName2};
    final int numBuckets = 3;
    final Host host = Host.getHost(0);
    final VM datastore1 = host.getVM(2);
    final VM datastore2 = host.getVM(3);
    final VM datastore3 = host.getVM(0);
    final VM accessor = host.getVM(1);

    final CacheSerializableRunnable create = new CacheSerializableRunnable("CreatePR") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        PartitionAttributesFactory paf =
            new PartitionAttributesFactory().setTotalNumBuckets(numBuckets);

        for (int redundancy = 0; redundancy < regions.length; redundancy++) {
          paf.setRedundantCopies(redundancy);
          attr.setPartitionAttributes(paf.create());
          PartitionedRegion p =
              (PartitionedRegion) cache.createRegion(regions[redundancy], attr.create());
          assertNotNull(p);
          assertEquals(0, p.size());
        }
      }
    };

    datastore1.invoke(create);
    datastore2.invoke(create);
    datastore3.invoke(create);

    accessor.invoke(new CacheSerializableRunnable("CreateAccessorPR") {
      public void run2() throws CacheException {
        Cache cache = getCache();
        AttributesFactory attr = new AttributesFactory();
        PartitionAttributesFactory paf =
            new PartitionAttributesFactory().setTotalNumBuckets(numBuckets).setLocalMaxMemory(0);

        for (int redundancy = 0; redundancy < regions.length; redundancy++) {
          paf.setRedundantCopies(redundancy);
          attr.setPartitionAttributes(paf.create());
          PartitionedRegion p =
              (PartitionedRegion) cache.createRegion(regions[redundancy], attr.create());
          assertNotNull(p);
          assertEquals(0, p.size());
        }
      }
    });

    final CacheSerializableRunnable noBucketOwners =
        new CacheSerializableRunnable("AssertNoBucketOwners") {
          public void run2() throws CacheException {
            String[] regions = {rName0, rName1, rName2};
            for (int rs = 0; rs < regions.length; rs++) {
              PartitionedRegion p = (PartitionedRegion) getCache().getRegion(regions[rs]);
              assertNotNull(p);
              assertTrue(!p.isDestroyed());
              assertEquals(numBuckets, p.getTotalNumberOfBuckets());
              try {
                for (int i = 0; i < p.getTotalNumberOfBuckets(); i++) {
                  assertEquals(0, p.getRegionAdvisor().getBucketOwners(i).size());
                  assertEquals(0, p.getBucketOwnersForValidation(i).size());
                }
              } catch (ForceReattemptException noGood) {
                Assert.fail("Unexpected force retry", noGood);
              }
            }
          }
        };
    datastore1.invoke(noBucketOwners);
    datastore2.invoke(noBucketOwners);
    datastore3.invoke(noBucketOwners);
    accessor.invoke(noBucketOwners);

    accessor.invoke(new CacheSerializableRunnable("CreateOneBucket") {
      public void run2() throws CacheException {
        for (int rs = 0; rs < regions.length; rs++) {
          PartitionedRegion p = (PartitionedRegion) getCache().getRegion(regions[rs]);
          assertNotNull(p);
          assertEquals(3, p.getTotalNumberOfBuckets());
          // Create one bucket
          p.put(new Integer(0), "zero");
          assertEquals(1, p.getRegionAdvisor().getCreatedBucketsCount());
        }
      }
    });

    final CacheSerializableRunnable oneBucketOwner =
        new CacheSerializableRunnable("AssertSingleBucketPrimary") {
          public void run2() throws CacheException {
            for (int rs = 0; rs < regions.length; rs++) {
              PartitionedRegion p = (PartitionedRegion) getCache().getRegion(regions[rs]);
              try {
                for (Iterator it = p.getRegionAdvisor().getBucketSet().iterator(); it.hasNext();) {
                  Integer bid = (Integer) it.next();
                  assertEquals(p.getRedundantCopies() + 1,
                      p.getRegionAdvisor().getBucketOwners(bid.intValue()).size());
                  List prims = p.getBucketOwnersForValidation(bid.intValue());
                  assertEquals(p.getRedundantCopies() + 1, prims.size());
                  int primCount = 0;
                  for (Iterator lit = prims.iterator(); lit.hasNext();) {
                    Object[] memAndBoolean = (Object[]) lit.next();
                    assertEquals(3, memAndBoolean.length); // memberId, isPrimary and hostToken(new)
                    assertTrue(memAndBoolean[0] instanceof DistributedMember);
                    assertEquals(Boolean.class, memAndBoolean[1].getClass());
                    Boolean isPrimary = (Boolean) memAndBoolean[1];
                    if (isPrimary.booleanValue()) {
                      primCount++;
                    }
                  }
                  assertEquals(1, primCount);
                }
              } catch (ForceReattemptException noGood) {
                Assert.fail("Unexpected force retry", noGood);
              }
            }
          }
        };
    accessor.invoke(oneBucketOwner);
    datastore1.invoke(oneBucketOwner);
    datastore2.invoke(oneBucketOwner);
    datastore3.invoke(oneBucketOwner);
  }
}
