/*
 * 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.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.geode.DataSerializable;
import org.apache.geode.DataSerializer;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.EvictionAction;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.InterestPolicy;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.SubscriptionAttributes;
import org.apache.geode.cache.control.ResourceManager;
import org.apache.geode.cache.partition.PartitionRegionHelper;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.cache.util.ObjectSizer;
import org.apache.geode.cache30.CacheTestCase;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;

/**
 * A test of the when we will use the object sizer to determine the actual size of objects wrapped
 * in CacheDeserializables.
 * 
 * 
 * 
 * TODO - I was intending to add tests that have an index and object sizer, but it appears we don't
 * support indexes on regions with overflow to disk.
 *
 */
@Category(DistributedTest.class)
public class SizingFlagDUnitTest extends JUnit4CacheTestCase {

  /**
   * @param name
   */
  public SizingFlagDUnitTest() {
    super();
  }

  @Test
  public void testRRMemLRU() {
    doRRMemLRUTest();
  }

  @Test
  public void testRRMemLRUDeltaAndFlag() {
    doRRMemLRUDeltaTest(true);
  }

  @Test
  public void testRRMemLRUDelta() {
    doRRMemLRUDeltaTest(false);
  }

  @Test
  public void testRRListener() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createRR(vm0);
    createRR(vm1);

    addListener(vm0);
    addListener(vm1);

    doListenerTestRR(vm0, vm1);
  }

  @Test
  public void testPRMemLRU() {
    doPRMemLRUTest();
  }

  @Test
  public void testPRMemLRUAndFlagDeltaPutOnPrimary() {
    doPRDeltaTestLRU(false, false, true, false);
  }

  @Test
  public void testPRMemLRUDeltaPutOnPrimary() {
    doPRDeltaTestLRU(false, false, true, false);
  }

  @Test
  public void testPRMemLRUAndFlagDeltaPutOnSecondary() {
    doPRDeltaTestLRU(false, false, false, true);
  }

  @Test
  public void testPRMemLRUDeltaPutOnSecondary() {
    doPRDeltaTestLRU(false, false, false, true);
  }

  @Test
  public void testPRNoLRUDelta() {
    doPRNoLRUDeltaTest(false);
  }

  @Test
  public void testPRNoLRUAndFlagDelta() {
    doPRNoLRUDeltaTest(true);
  }

  @Test
  public void testPRListener() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createPR(vm0, true);
    createPR(vm1, true);

    addListener(vm0);
    addListener(vm1);

    doListenerTestPR(vm0, vm1);
  }

  @Test
  public void testPRHeapLRU() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createPRHeapLRU(vm0);
    createPRHeapLRU(vm1);

    put(vm0, new TestKey("a"), new TestObject(100, 1000));

    assertValueType(vm0, new TestKey("a"), ValueType.CD_SERIALIZED);
    assertValueType(vm1, new TestKey("a"), ValueType.CD_SERIALIZED);

    assertEquals(1, getObjectSizerInvocations(vm0));
    long origSize0 = getSizeFromPRStats(vm0);
    assertTrue("Size was " + origSize0, 1000 > origSize0);
    assertEquals(1, getObjectSizerInvocations(vm1));
    long origSize1 = getSizeFromPRStats(vm1);
    assertTrue("Size was " + origSize1, 1000 > origSize1);

    get(vm0, new TestKey("a"), new TestObject(100, 1000));

    assertValueType(vm0, new TestKey("a"), ValueType.CD_DESERIALIZED);
    assertValueType(vm1, new TestKey("a"), ValueType.CD_SERIALIZED);
    // assertTrue(1000 < getSizeFromPRStats(vm0));
    assertEquals(3, getObjectSizerInvocations(vm0));
    // assertTrue(1000 > getSizeFromPRStats(vm1));
    assertEquals(1, getObjectSizerInvocations(vm1));

    // Test what happens when we reach the heap threshold??
  }

  @Test
  public void testRRHeapLRU() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createRRHeapLRU(vm0);
    createRRHeapLRU(vm1);

    put(vm0, "a", new TestObject(100, 1000));

    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_SERIALIZED);

    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(0, getObjectSizerInvocations(vm1));

    get(vm1, "a", new TestObject(100, 1000));

    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(1, getObjectSizerInvocations(vm1));

    // Test what happens when we reach the heap threshold??
  }

  @Test
  public void testPRHeapLRUDeltaWithFlagPutOnPrimary() {
    doPRDeltaTestLRU(false, true, true, false);
  }

  @Test
  public void testPRHeapLRUDeltaPutOnPrimary() {
    doPRDeltaTestLRU(false, true, true, false);
  }

  @Test
  public void testPRHeapLRUDeltaWithFlagPutOnSecondary() {
    doPRDeltaTestLRU(false, true, false, true);
  }

  @Test
  public void testPRHeapLRUDeltaPutOnSecondary() {
    doPRDeltaTestLRU(false, true, false, true);
  }

  // test to cover bug41916
  @Test
  public void testLargeDelta() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    setDeltaRecalculatesSize(vm0, false);
    setDeltaRecalculatesSize(vm1, false);

    createPR(vm0, false);
    createPR(vm1, false);
    // make a string bigger than socket-buffer-size which defaults to 32k
    int BIG_DELTA_SIZE = 32 * 1024 * 2;
    StringBuilder sb = new StringBuilder(BIG_DELTA_SIZE);
    for (int i = 0; i < BIG_DELTA_SIZE; i++) {
      sb.append('7');
    }
    TestDelta delta1 = new TestDelta(true, sb.toString());


    assignPRBuckets(vm0);
    boolean vm0isPrimary = prHostsBucketForKey(vm0, 0);
    if (!vm0isPrimary) {
      assertEquals(true, prHostsBucketForKey(vm1, 0));
    }
    VM primaryVm;
    VM secondaryVm;
    if (vm0isPrimary) {
      primaryVm = vm0;
      secondaryVm = vm1;
    } else {
      primaryVm = vm1;
      secondaryVm = vm0;
    }

    put(secondaryVm, 0, delta1);
  }

  void doPRDeltaTestLRU(boolean shouldSizeChange, boolean heapLRU, boolean putOnPrimary,
      boolean wasDelta) {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    setDeltaRecalculatesSize(vm0, shouldSizeChange);
    setDeltaRecalculatesSize(vm1, shouldSizeChange);

    if (heapLRU) {
      createPRHeapLRU(vm0);
      createPRHeapLRU(vm1);
    } else {// memLRU
      createPR(vm0, true);
      createPR(vm1, true);
    }
    assignPRBuckets(vm0);
    boolean vm0isPrimary = prHostsBucketForKey(vm0, 0);
    if (!vm0isPrimary) {
      assertEquals(true, prHostsBucketForKey(vm1, 0));
    }
    VM primaryVm;
    VM secondaryVm;
    if (vm0isPrimary) {
      primaryVm = vm0;
      secondaryVm = vm1;
    } else {
      primaryVm = vm1;
      secondaryVm = vm0;
    }

    TestDelta delta1 = new TestDelta(false, "12345");
    if (putOnPrimary) {
      put(primaryVm, 0, delta1);
    } else {
      put(secondaryVm, 0, delta1);
    }
    // if the put is done on the primary then it will be CD_DESERIALIZED on the primary
    // otherwise it will be CD_SERIALIZED on the primary.
    if (putOnPrimary) {
      assertValueType(primaryVm, 0, ValueType.CD_DESERIALIZED);
      assertEquals(1, getObjectSizerInvocations(primaryVm));
    } else {
      assertValueType(primaryVm, 0, ValueType.CD_SERIALIZED);
      assertEquals(0, getObjectSizerInvocations(primaryVm));
    }
    // It will always be CD_SERIALIZED on the secondary.
    assertValueType(secondaryVm, 0, ValueType.CD_SERIALIZED);
    assertEquals(0, getObjectSizerInvocations(secondaryVm));

    long origEvictionSize0 = getSizeFromEvictionStats(primaryVm);
    long origEvictionSize1 = getSizeFromEvictionStats(secondaryVm);
    long origPRSize0 = getSizeFromPRStats(primaryVm);
    long origPRSize1 = getSizeFromPRStats(secondaryVm);
    delta1.info = "1234567890";
    delta1.hasDelta = true;
    // Update the delta
    if (putOnPrimary) {
      put(primaryVm, 0, delta1);
    } else {
      put(secondaryVm, 0, delta1);
    }

    assertValueType(primaryVm, 0, ValueType.CD_DESERIALIZED);
    assertValueType(secondaryVm, 0, ValueType.CD_DESERIALIZED);

    if (shouldSizeChange) {
      assertEquals(2, getObjectSizerInvocations(primaryVm));
      // once when we deserialize the value in the cache
      // and once when we size the new value from applying the delta
      assertEquals(2, getObjectSizerInvocations(secondaryVm));
    } else if (wasDelta) {
      assertEquals(0, getObjectSizerInvocations(primaryVm));
      // 1 sizer invoke since the first value needs to be deserialized
      assertEquals(0, getObjectSizerInvocations(secondaryVm));
    } else {
      assertEquals(1, getObjectSizerInvocations(primaryVm));
      // 1 sizer invoke since the first value needs to be deserialized
      assertEquals(0, getObjectSizerInvocations(secondaryVm));
    }

    long finalEvictionSize0 = getSizeFromEvictionStats(primaryVm);
    long finalEvictionSize1 = getSizeFromEvictionStats(secondaryVm);
    long finalPRSize0 = getSizeFromPRStats(primaryVm);
    long finalPRSize1 = getSizeFromPRStats(secondaryVm);
    if (shouldSizeChange) {
      // I'm not sure what the change in size should be, because we went
      // from serialized to deserialized
      assertTrue(finalEvictionSize0 - origEvictionSize0 != 0);
      assertTrue(finalPRSize0 - origPRSize0 != 0);
      assertTrue(finalEvictionSize1 - origEvictionSize1 != 0);
      assertTrue(finalPRSize1 - origPRSize1 != 0);
    } else {
      assertEquals(0, finalEvictionSize1 - origEvictionSize1);
      assertEquals(0, finalPRSize0 - origPRSize0);
      assertEquals(0, finalPRSize1 - origPRSize1);
    }
  }

  private void addListener(VM vm) {
    vm.invoke(new SerializableRunnable("Add listener") {
      public void run() {
        Cache cache = getCache();
        Region region = cache.getRegion("region");
        try {
          region.getAttributesMutator().addCacheListener(new TestCacheListener());
        } catch (Exception e) {
          Assert.fail("couldn't create index", e);
        }
      }
    });
  }


  private void doListenerTestRR(VM vm0, VM vm1) {
    assertEquals(0, getObjectSizerInvocations(vm0));
    assertEquals(0, getObjectSizerInvocations(vm1));
    put(vm0, "a", new TestObject(100, 100000));
    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(1, getObjectSizerInvocations(vm1));

    long origEvictionSize0 = getSizeFromEvictionStats(vm0);
    long origEvictionSize1 = getSizeFromEvictionStats(vm1);


    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    assertTrue(origEvictionSize0 >= 100000);
    assertTrue(origEvictionSize1 >= 100000);

    put(vm0, "a", new TestObject(200, 200000));
    assertEquals(2, getObjectSizerInvocations(vm0));
    assertEquals(2, getObjectSizerInvocations(vm1));

    long finalEvictionSize0 = getSizeFromEvictionStats(vm0);
    long finalEvictionSize1 = getSizeFromEvictionStats(vm1);

    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    assertEquals(100000, finalEvictionSize0 - origEvictionSize0);
    assertEquals(100000, finalEvictionSize1 - origEvictionSize1);
  }

  private void doListenerTestPR(VM vm0, VM vm1) {
    assertEquals(0, getObjectSizerInvocations(vm0));
    assertEquals(0, getObjectSizerInvocations(vm1));
    put(vm0, "a", new TestObject(100, 100000));
    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(1, getObjectSizerInvocations(vm1));

    long origEvictionSize0 = getSizeFromEvictionStats(vm0);
    long origEvictionSize1 = getSizeFromEvictionStats(vm1);
    long origPRSize0 = getSizeFromPRStats(vm1);
    long origPRSize1 = getSizeFromPRStats(vm1);


    assertValueType(vm0, "a", ValueType.CD_DESERIALIZED);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    assertTrue(origEvictionSize1 >= 100000);
    assertTrue(origEvictionSize0 >= 100000);
    assertTrue(origPRSize0 <= 500);
    assertTrue(origPRSize1 <= 500);

    put(vm0, "a", new TestObject(200, 200000));
    assertEquals(2, getObjectSizerInvocations(vm0));
    assertEquals(2, getObjectSizerInvocations(vm1));

    long finalEvictionSize0 = getSizeFromEvictionStats(vm0);
    long finalEvictionSize1 = getSizeFromEvictionStats(vm1);
    long finalPRSize0 = getSizeFromPRStats(vm0);
    long finalPRSize1 = getSizeFromPRStats(vm1);

    assertValueType(vm0, "a", ValueType.CD_DESERIALIZED);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    assertEquals(100000, finalEvictionSize0 - origEvictionSize0);
    assertEquals(100000, finalEvictionSize1 - origEvictionSize1);
    assertEquals(100, finalPRSize0 - origPRSize0);
    assertEquals(100, finalPRSize1 - origPRSize1);
  }


  private void doRRMemLRUTest() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createRR(vm0);
    createRR(vm1);
    put(vm0, "a", new TestObject(100, 100000));
    long origEvictionSize0 = getSizeFromEvictionStats(vm0);
    long origEvictionSize1 = getSizeFromEvictionStats(vm1);
    put(vm0, "a", new TestObject(200, 200000));


    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_SERIALIZED);
    assertEquals(2, getObjectSizerInvocations(vm0));

    long finalEvictionSize0 = getSizeFromEvictionStats(vm0);
    long finalEvictionSize1 = getSizeFromEvictionStats(vm1);
    assertEquals(100000, finalEvictionSize0 - origEvictionSize0);
    assertEquals(100, finalEvictionSize1 - origEvictionSize1);

    assertEquals(0, getObjectSizerInvocations(vm1));

    // Do a get to make sure we deserialize the object and calculate
    // the size adjustment
    Object v = new TestObject(200, 200000);
    get(vm1, "a", v);
    int vSize = CachedDeserializableFactory.calcSerializedMemSize(v);

    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);
    long evictionSizeAfterGet = getSizeFromEvictionStats(vm1);
    assertEquals(1, getObjectSizerInvocations(vm1));
    assertEquals(200000 + CachedDeserializableFactory.overhead() - vSize,
        evictionSizeAfterGet - finalEvictionSize1);

    // Do a put that will trigger an eviction if it is deserialized
    put(vm0, "b", new TestObject(100, 1000000));

    assertEquals(1, getEvictions(vm0));
    assertEquals(0, getEvictions(vm1));

    // Do a get to make sure we deserialize the object and calculate
    // the size adjustment
    get(vm1, "b", new TestObject(100, 1000000));
    assertEquals(1, getEvictions(vm1));
  }

  private void doPRMemLRUTest() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    createPR(vm0, true);
    createPR(vm1, true);
    put(vm0, 0, new TestObject(100, 100000));
    assertValueType(vm0, 0, ValueType.CD_SERIALIZED);
    assertValueType(vm1, 0, ValueType.CD_SERIALIZED);
    long origEvictionSize0 = getSizeFromEvictionStats(vm0);
    long origEvictionSize1 = getSizeFromEvictionStats(vm1);
    long origPRSize0 = getSizeFromPRStats(vm0);
    long origPRSize1 = getSizeFromPRStats(vm1);
    put(vm0, 0, new TestObject(200, 200000));

    assertEquals(0, getObjectSizerInvocations(vm0));

    long finalEvictionSize0 = getSizeFromEvictionStats(vm0);
    long finalPRSize0 = getSizeFromPRStats(vm0);
    long finalEvictionSize1 = getSizeFromEvictionStats(vm1);
    long finalPRSize1 = getSizeFromPRStats(vm1);
    assertEquals(100, finalEvictionSize0 - origEvictionSize0);
    assertEquals(100, finalEvictionSize1 - origEvictionSize1);
    assertEquals(100, finalPRSize0 - origPRSize0);
    assertEquals(100, finalPRSize1 - origPRSize1);

    assertEquals(0, getObjectSizerInvocations(vm1));

    // Do a get to see if we deserialize the object and calculate
    // the size adjustment
    Object v = new TestObject(200, 200000);
    get(vm0, 0, v);
    int vSize = CachedDeserializableFactory.calcSerializedMemSize(v);
    assertValueType(vm0, 0, ValueType.CD_DESERIALIZED);
    assertValueType(vm1, 0, ValueType.CD_SERIALIZED);
    long evictionSizeAfterGet = getSizeFromEvictionStats(vm0);
    long prSizeAfterGet = getSizeFromPRStats(vm0);
    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(0, getObjectSizerInvocations(vm1));
    assertEquals(200000 + CachedDeserializableFactory.overhead() - vSize,
        evictionSizeAfterGet - finalEvictionSize0);
    assertEquals(0, prSizeAfterGet - finalPRSize0);

    // Do a put that will trigger an eviction if it is deserialized
    // It should not be deserialized.
    put(vm0, 113, new TestObject(100, 1024 * 1024));
    assertValueType(vm0, 113, ValueType.CD_SERIALIZED);
    assertValueType(vm1, 113, ValueType.CD_SERIALIZED);
    long evictionSizeAfterPutVm1 = getSizeFromEvictionStats(vm1);

    assertEquals(0, getEvictions(vm0));
    assertEquals(0, getEvictions(vm1));

    // Do a get to make sure we deserialize the object and calculate
    // the size adjustment which should force an eviction
    get(vm1, 113, new TestObject(100, 1024 * 1024));
    long evictionSizeAfterGetVm1 = getSizeFromEvictionStats(vm1);
    assertValueType(vm0, 113, ValueType.CD_SERIALIZED);
    assertValueType(vm1, 113, ValueType.EVICTED);
    assertEquals(1, getObjectSizerInvocations(vm0)); // from the get of key 0 on vm0
    assertEquals(0, getEvictions(vm0));
    assertEquals(1, getObjectSizerInvocations(vm1));
    assertEquals(2, getEvictions(vm1));
  }

  private void doRRMemLRUDeltaTest(boolean shouldSizeChange) {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    setDeltaRecalculatesSize(vm0, shouldSizeChange);
    setDeltaRecalculatesSize(vm1, shouldSizeChange);

    createRR(vm0);
    createRR(vm1);
    TestDelta delta1 = new TestDelta(false, "12345");
    put(vm0, "a", delta1);

    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_SERIALIZED);
    assertEquals(1, getObjectSizerInvocations(vm0));
    assertEquals(0, getObjectSizerInvocations(vm1));

    long origEvictionSize0 = getSizeFromEvictionStats(vm0);
    long origEvictionSize1 = getSizeFromEvictionStats(vm1);
    delta1.info = "1234567890";
    delta1.hasDelta = true;
    // Update the delta
    put(vm0, "a", delta1);

    assertValueType(vm0, "a", ValueType.RAW_VALUE);
    assertValueType(vm1, "a", ValueType.CD_DESERIALIZED);

    assertEquals(2, getObjectSizerInvocations(vm0));

    long finalEvictionSize0 = getSizeFromEvictionStats(vm0);
    long finalEvictionSize1 = getSizeFromEvictionStats(vm1);
    assertEquals(5, finalEvictionSize0 - origEvictionSize0);
    if (shouldSizeChange) {
      assertEquals(1, getObjectSizerInvocations(vm1));
      // I'm not sure what the change in size should be, because we went
      // from serialized to deserialized
      assertTrue(finalEvictionSize1 - origEvictionSize1 != 0);
    } else {
      // we invoke the sizer once when we deserialize the original to apply the delta to it
      assertEquals(0, getObjectSizerInvocations(vm1));
      assertEquals(0, finalEvictionSize1 - origEvictionSize1);
    }
  }

  private void doPRNoLRUDeltaTest(boolean shouldSizeChange) {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);

    setDeltaRecalculatesSize(vm0, shouldSizeChange);
    setDeltaRecalculatesSize(vm1, shouldSizeChange);

    createPR(vm0, false);
    createPR(vm1, false);
    TestDelta delta1 = new TestDelta(false, "12345");
    put(vm0, "a", delta1);
    long origPRSize0 = getSizeFromPRStats(vm0);
    long origPRSize1 = getSizeFromPRStats(vm1);
    delta1.info = "1234567890";
    delta1.hasDelta = true;
    // Update the delta
    put(vm0, "a", delta1);


    long finalPRSize0 = getSizeFromPRStats(vm0);
    long finalPRSize1 = getSizeFromPRStats(vm1);
    if (shouldSizeChange) {
      // I'm not sure what the change in size should be, because we went
      // from serialized to deserialized
      assertTrue(finalPRSize0 - origPRSize0 != 0);
      assertTrue(finalPRSize1 - origPRSize1 != 0);
    } else {
      assertEquals(0, finalPRSize0 - origPRSize0);
      assertEquals(0, finalPRSize1 - origPRSize1);
    }
  }

  /**
   * @param vm0
   * @return
   */
  private long getSizeFromPRStats(VM vm0) {
    return (Long) vm0.invoke(new SerializableCallable() {

      public Object call() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        if (region instanceof PartitionedRegion) {
          long total = 0;
          PartitionedRegion pr = ((PartitionedRegion) region);
          for (int i = 0; i < pr.getPartitionAttributes().getTotalNumBuckets(); i++) {
            total += pr.getDataStore().getBucketSize(i);
          }
          return total;
        } else {
          return 0L;
        }
      }
    });
  }



  private long getSizeFromEvictionStats(VM vm0) {
    return (Long) vm0.invoke(new SerializableCallable() {

      public Object call() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        return getSizeFromEvictionStats(region);
      }
    });
  }

  private long getEvictions(VM vm0) {
    return (Long) vm0.invoke(new SerializableCallable() {

      public Object call() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        return getEvictions(region);
      }
    });
  }

  private int getObjectSizerInvocations(VM vm0) {
    return (Integer) vm0.invoke(new SerializableCallable() {

      public Object call() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        return getObjectSizerInvocations(region);
      }
    });
  }

  private void assignPRBuckets(VM vm) {
    vm.invoke(new SerializableRunnable("assignPRBuckets") {
      public void run() {
        Cache cache = getCache();
        PartitionRegionHelper.assignBucketsToPartitions(cache.getRegion("region"));
      }
    });
  }

  private boolean prHostsBucketForKey(VM vm, final Object key) {
    Boolean result = (Boolean) vm.invoke(new SerializableCallable("prHostsBucketForKey") {
      public Object call() {
        Cache cache = getCache();
        DistributedMember myId = cache.getDistributedSystem().getDistributedMember();
        Region region = cache.getRegion("region");
        DistributedMember hostMember = PartitionRegionHelper.getPrimaryMemberForKey(region, key);
        if (hostMember == null) {
          throw new IllegalStateException("bucket for key " + key + " is not hosted!");
        }
        boolean res = Boolean.valueOf(myId.equals(hostMember));
        // cache.getLogger().info("DEBUG prHostsBucketForKey=" + res);
        return res;
      }
    });
    return result.booleanValue();
  }

  private void put(VM vm0, final Object key, final Object value) {
    vm0.invoke(new SerializableRunnable("Put data") {

      public void run() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        // cache.getLogger().info("DEBUG about to put(" + key + ", " + value + ")");
        region.put(key, value);
      }
    });
  }

  private void get(VM vm0, final Object key, final Object value) {
    vm0.invoke(new SerializableRunnable("Put data") {

      public void run() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        assertEquals(value, region.get(key));
      }
    });
  }

  protected int getObjectSizerInvocations(LocalRegion region) {
    TestObjectSizer sizer = (TestObjectSizer) region.getEvictionAttributes().getObjectSizer();
    int result = sizer.invocations.get();
    region.getCache().getLogger().info("objectSizerInvocations=" + result);
    return result;
  }

  private long getSizeFromEvictionStats(LocalRegion region) {
    long result = region.getEvictionController().getLRUHelper().getStats().getCounter();
    // region.getCache().getLogger().info("DEBUG evictionSize=" + result);
    return result;
  }

  private long getEvictions(LocalRegion region) {
    return region.getEvictionController().getLRUHelper().getStats().getEvictions();
  }

  private void setDeltaRecalculatesSize(VM vm, final boolean shouldSizeChange) {
    vm.invoke(new SerializableRunnable("setDeltaRecalculatesSize") {
      public void run() {
        GemFireCacheImpl.DELTAS_RECALCULATE_SIZE = shouldSizeChange;
      }
    });
  }

  private void createRR(VM vm) {
    vm.invoke(new SerializableRunnable("Create rr") {
      public void run() {
        Cache cache = getCache();
        AttributesFactory<Integer, TestDelta> attr = new AttributesFactory<Integer, TestDelta>();
        attr.setDiskSynchronous(true);
        attr.setDataPolicy(DataPolicy.REPLICATE);
        attr.setScope(Scope.DISTRIBUTED_ACK);
        attr.setEvictionAttributes(EvictionAttributes.createLRUMemoryAttributes(1,
            new TestObjectSizer(), EvictionAction.OVERFLOW_TO_DISK));
        attr.setDiskDirs(getMyDiskDirs());
        Region region = cache.createRegion("region", attr.create());
      }
    });

  }

  private void assertValueType(VM vm, final Object key, final ValueType expectedType) {
    vm.invoke(new SerializableRunnable("Create rr") {
      public void run() {
        Cache cache = getCache();
        LocalRegion region = (LocalRegion) cache.getRegion("region");
        Object value = region.getValueInVM(key);
        switch (expectedType) {
          case RAW_VALUE:
            assertTrue("Value was " + value + " type " + value.getClass(),
                !(value instanceof CachedDeserializable));
            break;
          case CD_SERIALIZED:
            assertTrue("Value was " + value + " type " + value.getClass(),
                value instanceof CachedDeserializable);
            assertTrue("Value not serialized",
                ((CachedDeserializable) value).getValue() instanceof byte[]);
            break;
          case CD_DESERIALIZED:
            assertTrue("Value was " + value + " type " + value.getClass(),
                value instanceof CachedDeserializable);
            assertTrue("Value was serialized",
                !(((CachedDeserializable) value).getValue() instanceof byte[]));
            break;
          case EVICTED:
            assertEquals(null, value);
            break;
        }
      }
    });
  }

  private File[] getMyDiskDirs() {
    long random = new Random().nextLong();
    File file = new File(Long.toString(random));
    file.mkdirs();
    return new File[] {file};
  }

  private void createPR(VM vm, final boolean enableLRU) {
    vm.invoke(new SerializableRunnable("Create pr") {
      public void run() {
        Cache cache = getCache();
        AttributesFactory<Integer, TestDelta> attr = new AttributesFactory<Integer, TestDelta>();
        attr.setDiskSynchronous(true);
        PartitionAttributesFactory<Integer, TestDelta> paf =
            new PartitionAttributesFactory<Integer, TestDelta>();
        paf.setRedundantCopies(1);
        if (enableLRU) {
          paf.setLocalMaxMemory(1); // memlru limit is 1 megabyte
          attr.setEvictionAttributes(EvictionAttributes
              .createLRUMemoryAttributes(new TestObjectSizer(), EvictionAction.OVERFLOW_TO_DISK));
          attr.setDiskDirs(getMyDiskDirs());
        }
        PartitionAttributes<Integer, TestDelta> prAttr = paf.create();
        attr.setPartitionAttributes(prAttr);
        attr.setDataPolicy(DataPolicy.PARTITION);
        attr.setSubscriptionAttributes(new SubscriptionAttributes(InterestPolicy.ALL));
        Region<Integer, TestDelta> region = cache.createRegion("region", attr.create());
      }
    });

  }

  private void createRRHeapLRU(VM vm) {
    vm.invoke(new SerializableRunnable("Create rr") {
      public void run() {
        Cache cache = getCache();
        ResourceManager manager = cache.getResourceManager();
        manager.setCriticalHeapPercentage(95);
        manager.setEvictionHeapPercentage(90);
        AttributesFactory<Integer, TestDelta> attr = new AttributesFactory<Integer, TestDelta>();
        attr.setEvictionAttributes(EvictionAttributes.createLRUHeapAttributes(new TestObjectSizer(),
            EvictionAction.OVERFLOW_TO_DISK));
        attr.setDiskDirs(getMyDiskDirs());
        attr.setDataPolicy(DataPolicy.REPLICATE);
        attr.setScope(Scope.DISTRIBUTED_ACK);
        attr.setDiskDirs(getMyDiskDirs());
        Region region = cache.createRegion("region", attr.create());
      }
    });

  }

  private void createPRHeapLRU(VM vm) {
    vm.invoke(new SerializableRunnable("Create pr") {
      public void run() {
        Cache cache = getCache();
        ResourceManager manager = cache.getResourceManager();
        manager.setCriticalHeapPercentage(95);
        manager.setEvictionHeapPercentage(90);

        AttributesFactory<Integer, TestDelta> attr = new AttributesFactory<Integer, TestDelta>();
        PartitionAttributesFactory<Integer, TestDelta> paf =
            new PartitionAttributesFactory<Integer, TestDelta>();
        paf.setRedundantCopies(1);
        attr.setEvictionAttributes(EvictionAttributes.createLRUHeapAttributes(new TestObjectSizer(),
            EvictionAction.LOCAL_DESTROY));
        PartitionAttributes<Integer, TestDelta> prAttr = paf.create();
        attr.setPartitionAttributes(prAttr);
        attr.setDataPolicy(DataPolicy.PARTITION);
        attr.setSubscriptionAttributes(new SubscriptionAttributes(InterestPolicy.ALL));
        Region<Integer, TestDelta> region = cache.createRegion("region", attr.create());
      }
    });

  }

  private static class TestObjectSizer implements ObjectSizer {
    private AtomicInteger invocations = new AtomicInteger();

    public int sizeof(Object o) {
      if (InternalDistributedSystem.getLoggerI18n().fineEnabled()) {
        InternalDistributedSystem.getLoggerI18n()
            .fine("TestObjectSizer invoked"/* , new Exception("stack trace") */);
      }
      if (o instanceof TestObject) {
        // org.apache.geode.internal.cache.GemFireCache.getInstance().getLogger().info("DEBUG
        // TestObjectSizer: sizeof o=" + o, new RuntimeException("STACK"));
        invocations.incrementAndGet();
        return ((TestObject) o).sizeForSizer;
      }
      if (o instanceof TestDelta) {
        // org.apache.geode.internal.cache.GemFireCache.getInstance().getLogger().info("DEBUG
        // TestObjectSizer: sizeof delta o=" + o, new RuntimeException("STACK"));
        invocations.incrementAndGet();
        return ((TestDelta) o).info.length();
      }
      // This is the key. Why don't we handle Integers internally?
      if (o instanceof Integer) {
        return 0;
      }
      if (o instanceof TestKey) {
        // org.apache.geode.internal.cache.GemFireCache.getInstance().getLogger().info("DEBUG
        // TestObjectSizer: sizeof TestKey o=" + o, new RuntimeException("STACK"));
        invocations.incrementAndGet();
        return ((TestKey) o).value.length();
      }
      throw new RuntimeException("Unpected type to be sized " + o.getClass() + ", object=" + o);
    }
  }

  private static class TestKey implements DataSerializable {
    String value;

    public TestKey() {

    }

    public TestKey(String value) {
      this.value = value;
    }

    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      value = DataSerializer.readString(in);
    }

    public void toData(DataOutput out) throws IOException {
      DataSerializer.writeString(value, out);
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((value == null) ? 0 : value.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof TestKey))
        return false;
      TestKey other = (TestKey) obj;
      if (value == null) {
        if (other.value != null)
          return false;
      } else if (!value.equals(other.value))
        return false;
      return true;
    }

  }

  private static class TestObject implements DataSerializable {
    public int sizeForSizer;
    public int sizeForSerialization;

    public TestObject() {

    }


    public TestObject(int sizeForSerialization, int sizeForSizer) {
      super();
      this.sizeForSizer = sizeForSizer;
      this.sizeForSerialization = sizeForSerialization;
    }

    public void fromData(DataInput in) throws IOException, ClassNotFoundException {
      sizeForSizer = in.readInt();
      sizeForSerialization = in.readInt();
      // We don't actually need these things.
      in.skipBytes(sizeForSerialization);
    }

    public void toData(DataOutput out) throws IOException {
      out.writeInt(sizeForSizer);
      out.writeInt(sizeForSerialization);
      out.write(new byte[sizeForSerialization]);

    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + sizeForSerialization;
      result = prime * result + sizeForSizer;
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof TestObject))
        return false;
      TestObject other = (TestObject) obj;
      if (sizeForSerialization != other.sizeForSerialization)
        return false;
      if (sizeForSizer != other.sizeForSizer)
        return false;
      return true;
    }

    @Override
    public String toString() {
      return "TestObject [sizeForSerialization=" + sizeForSerialization + ", sizeForSizer="
          + sizeForSizer + "]";
    }
  }

  public static class TestCacheListener extends CacheListenerAdapter {

    @Override
    public void afterCreate(EntryEvent event) {
      // Make sure we deserialize the new value
      event.getRegion().getCache().getLoggerI18n().fine("invoked afterCreate with " + event);
      event.getRegion().getCache().getLoggerI18n().info(LocalizedStrings.DEBUG,
          "value is " + event.getNewValue());
    }

    @Override
    public void afterUpdate(EntryEvent event) {
      // Make sure we deserialize the new value
      event.getRegion().getCache().getLoggerI18n().fine("invoked afterUpdate with ");
      event.getRegion().getCache().getLoggerI18n().info(LocalizedStrings.DEBUG,
          "value is " + event.getNewValue());
    }

  }

  enum ValueType {
    RAW_VALUE, CD_SERIALIZED, CD_DESERIALIZED, EVICTED
  }

}
