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

import static org.junit.Assert.*;

import java.util.*;

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

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.EvictionAction;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.Operation;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.distributed.*;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.distributed.internal.membership.gms.MembershipManagerHelper;
import org.apache.geode.internal.cache.BucketRegion;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.cache.VMCachedDeserializable;
import org.apache.geode.internal.cache.versions.VMVersionTag;
import org.apache.geode.internal.cache.versions.VersionSource;
import org.apache.geode.internal.cache.versions.VersionTag;
import org.apache.geode.test.dunit.DistributedTestUtils;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableCallable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.WaitCriterion;
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;

/**
 * concurrency-control tests for client/server
 *
 *
 */
@Category(DistributedTest.class)
public class PRBucketSynchronizationDUnitTest extends JUnit4CacheTestCase {
  static enum TestType {
    IN_MEMORY, OVERFLOW, PERSISTENT
  };

  public static LocalRegion TestRegion;

  public PRBucketSynchronizationDUnitTest() {
    super();
  }

  @Override
  public Properties getDistributedSystemProperties() {
    Properties result = super.getDistributedSystemProperties();
    result.put(ConfigurationProperties.ENABLE_NETWORK_PARTITION_DETECTION, "false");
    return result;
  }

  @Test
  public void testThatBucketSyncOnPrimaryLoss() {
    doBucketsSyncOnPrimaryLoss(TestType.IN_MEMORY);
  }

  @Test
  public void testThatBucketsSyncOnPrimaryLossWithPersistence() {
    doBucketsSyncOnPrimaryLoss(TestType.PERSISTENT);
  }

  @Test
  public void testThatBucketsSyncOnPrimaryLossWithOverflow() {
    doBucketsSyncOnPrimaryLoss(TestType.OVERFLOW);
  }

  /**
   * We hit this problem in bug #45669. A primary was lost and we did not see secondary buckets
   * perform a delta-GII.
   */
  public void doBucketsSyncOnPrimaryLoss(TestType typeOfTest) {
    IgnoredException.addIgnoredException("killing member's ds");
    IgnoredException.addIgnoredException("killing member's ds");
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);
    VM vm2 = host.getVM(2);
    Set<VM> verifyVMs = new HashSet<VM>();

    final String name = this.getUniqueName() + "Region";

    verifyVMs.add(vm0);
    verifyVMs.add(vm1);
    verifyVMs.add(vm2);

    disconnectAllFromDS();
    try {
      createRegion(vm0, name, typeOfTest);
      createRegion(vm1, name, typeOfTest);
      createRegion(vm2, name, typeOfTest);

      createEntry1(vm0);

      VM primaryOwner;
      if (isPrimaryForBucket0(vm0))
        primaryOwner = vm0;
      else if (isPrimaryForBucket0(vm1))
        primaryOwner = vm1;
      else
        primaryOwner = vm2;

      verifyVMs.remove(primaryOwner);

      // cause one of the VMs to throw away the next operation
      VM creatorVM = null;
      InternalDistributedMember primaryID = getID(primaryOwner);
      VersionSource primaryVersionID = getVersionID(primaryOwner);
      for (VM vm : verifyVMs) {
        creatorVM = vm;
        createEntry2(creatorVM, primaryID, primaryVersionID);
        break;
      }

      verifyVMs.remove(creatorVM);

      // Now we crash the primary bucket owner simulating death during distribution.
      // The backup buckets should perform a delta-GII for the lost member and
      // get back in sync
      DistributedTestUtils.crashDistributedSystem(primaryOwner);

      for (VM vm : verifyVMs) {
        verifySynchronized(vm, primaryID);
      }
    } finally {
      disconnectAllFromDS();
    }
  }


  // private void closeCache(VM vm) {
  // vm.invoke(new SerializableCallable() {
  // public Object call() throws Exception {
  // closeCache();
  // return null;
  // }
  // });
  // }

  private boolean createEntry1(VM vm) {
    return (Boolean) vm.invoke(new SerializableCallable("create entry1") {
      public Object call() {
        TestRegion.create("Object1", Integer.valueOf(1));
        return true;
      }
    });
  }

  private boolean isPrimaryForBucket0(VM vm) {
    return (Boolean) vm.invoke(new SerializableCallable("is primary?") {
      public Object call() {
        PartitionedRegion pr = (PartitionedRegion) TestRegion;
        return pr.getDataStore().getLocalBucketById(0).getBucketAdvisor().isPrimary();
      }
    });
  }

  private InternalDistributedMember getID(VM vm) {
    return (InternalDistributedMember) vm.invoke(new SerializableCallable("get dmID") {
      public Object call() {
        return TestRegion.getCache().getMyId();
      }
    });
  }


  private VersionSource getVersionID(VM vm) {
    return (VersionSource) vm.invoke(new SerializableCallable("get versionID") {
      public Object call() {
        return TestRegion.getVersionMember();
      }
    });
  }

  private boolean createEntry2(VM vm, final InternalDistributedMember primary,
      final VersionSource primaryVersionID) {
    return (Boolean) vm.invoke(new SerializableCallable("create entry2") {
      public Object call() {
        // create a fake event that looks like it came from the primary and apply it to
        // this cache
        PartitionedRegion pr = (PartitionedRegion) TestRegion;
        BucketRegion bucket = pr.getDataStore().getLocalBucketById(0);
        VersionTag tag = new VMVersionTag();
        tag.setMemberID(primaryVersionID);
        tag.setRegionVersion(2);
        tag.setEntryVersion(1);
        tag.setIsRemoteForTesting();
        EntryEventImpl event =
            EntryEventImpl.create(bucket, Operation.CREATE, "Object3", true, primary, true, false);
        LogWriterUtils.getLogWriter().info("applying this event to the cache: " + event);
        event.setNewValue(new VMCachedDeserializable("value3", 12));
        event.setVersionTag(tag);
        bucket.getRegionMap().basicPut(event, System.currentTimeMillis(), true, false, null, false,
            false);
        event.release();

        // now create a tombstone so we can be sure these are transferred in delta-GII
        tag = new VMVersionTag();
        tag.setMemberID(primaryVersionID);
        tag.setRegionVersion(3);
        tag.setEntryVersion(1);
        tag.setIsRemoteForTesting();
        event =
            EntryEventImpl.create(bucket, Operation.CREATE, "Object5", true, primary, true, false);
        event.setNewValue(Token.TOMBSTONE);
        event.setVersionTag(tag);
        LogWriterUtils.getLogWriter().info("applying this event to the cache: " + event);
        bucket.getRegionMap().basicPut(event, System.currentTimeMillis(), true, false, null, false,
            false);
        event.release();

        bucket.dumpBackingMap();
        LogWriterUtils.getLogWriter()
            .info("bucket version vector is now " + bucket.getVersionVector().fullToString());
        assertTrue("bucket should hold entry Object3 now", bucket.containsKey("Object3"));
        return true;
      }
    });
  }

  private void verifySynchronized(VM vm, final InternalDistributedMember crashedMember) {
    vm.invoke(new SerializableCallable("check that synchronization happened") {
      public Object call() throws Exception {
        PartitionedRegion pr = (PartitionedRegion) TestRegion;
        final BucketRegion bucket = pr.getDataStore().getLocalBucketById(0);
        Wait.waitForCriterion(new WaitCriterion() {
          String waitingFor = "primary is still in membership view: " + crashedMember;
          boolean dumped = false;

          public boolean done() {
            if (TestRegion.getCache().getDistributionManager().isCurrentMember(crashedMember)) {
              LogWriterUtils.getLogWriter().info(waitingFor);
              return false;
            }
            if (!TestRegion.containsKey("Object3")) {
              waitingFor = "entry for Object3 not found";
              LogWriterUtils.getLogWriter().info(waitingFor);
              return false;
            }
            RegionEntry re = bucket.getRegionMap().getEntry("Object5");
            if (re == null) {
              if (!dumped) {
                dumped = true;
                bucket.dumpBackingMap();
              }
              waitingFor = "entry for Object5 not found";
              LogWriterUtils.getLogWriter().info(waitingFor);
              return false;
            }
            if (!re.isTombstone()) {
              if (!dumped) {
                dumped = true;
                bucket.dumpBackingMap();
              }
              waitingFor = "Object5 is not a tombstone but should be: " + re;
              LogWriterUtils.getLogWriter().info(waitingFor);
              return false;
            }
            return true;
          }

          public String description() {
            return waitingFor;
          }

        }, 30000, 5000, true);
        return null;
      }
    });
  }


  private void createRegion(VM vm, final String regionName, final TestType typeOfTest) {
    SerializableCallable createRegion = new SerializableCallable() {
      @SuppressWarnings("deprecation")
      public Object call() throws Exception {
        // TombstoneService.VERBOSE = true;
        AttributesFactory af = new AttributesFactory();
        af.setDataPolicy(DataPolicy.PARTITION);
        af.setPartitionAttributes((new PartitionAttributesFactory()).setTotalNumBuckets(2)
            .setRedundantCopies(3).create());
        switch (typeOfTest) {
          case IN_MEMORY:
            break;
          case PERSISTENT:
            af.setDataPolicy(DataPolicy.PERSISTENT_PARTITION);
            break;
          case OVERFLOW:
            af.setEvictionAttributes(
                EvictionAttributes.createLRUEntryAttributes(5, EvictionAction.OVERFLOW_TO_DISK));
            break;
        }
        TestRegion = (LocalRegion) createRootRegion(regionName, af.create());
        return null;
      }
    };
    vm.invoke(createRegion);
  }

}
