/*
 * 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.
 */
/*
 * ClearRvvLockingDUnitTest.java
 *
 * Created on September 6, 2005, 2:57 PM
 */
package org.apache.geode.internal.cache;

import static org.apache.geode.test.dunit.Assert.fail;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

import org.apache.logging.log4j.Logger;
import org.assertj.core.api.JUnitSoftAssertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheEvent;
import org.apache.geode.cache.CacheFactory;
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.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.logging.LogService;
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.SerializableRunnableIF;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;
import org.apache.geode.test.junit.categories.DistributedTest;

/**
 * Test class to verify proper RVV locking interaction between entry operations such as PUT/REMOVE
 * and the CLEAR region operation
 * 
 * GEODE-599: After an operation completed, it would unlock the RVV. This was occurring before the
 * operation was distributed to other members which created a window in which another operation
 * could be performed prior to that operation being distributed.
 * 
 * The fix for GEODE-599 was to not release the lock until after distributing the operation to the
 * other members.
 * 
 */

@SuppressWarnings("serial")
@Category(DistributedTest.class)
public class ClearRvvLockingDUnitTest extends JUnit4CacheTestCase {

  @Rule
  public transient JUnitSoftAssertions softly = new JUnitSoftAssertions();
  /*
   * The tests perform a single operation and a concurrent clear.
   * 
   * opsVM determines where the single operation will be performed, null will perform the op on the
   * test VM (vm0) clearVM determines where the clear operation will be performed, null will perform
   * the clear on the test VM (vm0)
   * 
   * Specifying NULL/NULL for opsVM and clearVM has the effect of performing both in the same thread
   * whereas specifying vm0/vm0 for example will run them both on the same vm, but different
   * threads. NULL/NULL is not tested here since the same thread performing a clear prior to
   * returning from a put is not possible using the public API.
   * 
   * Each test is performed twice once with operation and clear on the same vm, once on different
   * vms.
   * 
   */
  VM vm0, vm1, opsVM, clearVM;

  static Cache cache;
  static LocalRegion region;
  DistributedMember vm0ID, vm1ID;

  static AbstractRegionMap.ARMLockTestHook theHook;

  static final String THE_KEY = "theKey";
  static final String THE_VALUE = "theValue";

  private static final Logger logger = LogService.getLogger();

  // test methods

  @Test
  public void testPutOperationSameVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm0); // first arg is where to perform operation, second arg where to
                                 // perform clear
      opsVM.invoke(() -> setBasicHook(opsVM));
      runConsistencyTest(vm0, performPutOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testPutOperationDifferentVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm1); // first arg is where to perform operation, second arg where to
                                 // perform clear
      opsVM.invoke(() -> setBasicHook(clearVM));
      runConsistencyTest(vm0, performPutOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testPutOperationNoAck() {
    try {
      setupNoAckMembers();
      setOpAndClearVM(vm0, vm0);
      vm0.invoke(() -> setLocalNoAckHook(vm1));
      vm1.invoke(() -> setRemoteNoAckHook(vm0));
      vm0.invoke(() -> primeStep1(1));
      vm1.invoke(() -> primeStep2(1));
      runConsistencyTest(vm0, performNoAckPutOperation);
      checkForConsistencyErrors();
    } finally {
      vm0.invoke(() -> resetHook());
      vm1.invoke(() -> resetHook());
    }
  }

  @Test
  public void testRemoveOperationSameVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm0);
      opsVM.invoke(() -> setRemoveAndInvalidateHook(clearVM));
      runConsistencyTest(vm0, performRemoveOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testRemoveOperationDifferentVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm1);
      opsVM.invoke(() -> setRemoveAndInvalidateHook(clearVM));
      runConsistencyTest(vm0, performRemoveOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testInvalidateOperationSameVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm0);
      opsVM.invoke(() -> setRemoveAndInvalidateHook(clearVM));
      runConsistencyTest(vm0, performInvalidateOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testInvalidateOperationDifferentVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm1);
      opsVM.invoke(() -> setRemoveAndInvalidateHook(clearVM));
      runConsistencyTest(vm0, performInvalidateOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testPutAllOperationSameVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm0);
      opsVM.invoke(() -> setBulkHook(vm0));
      runConsistencyTest(vm0, performPutAllOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testPutAllOperationDifferentVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm1);
      opsVM.invoke(() -> setBulkHook(vm0));
      runConsistencyTest(vm0, performPutAllOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testRemoveAllOperationSameVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm0);
      opsVM.invoke(() -> setBulkHook(vm0));
      runConsistencyTest(vm0, performRemoveAllOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }

  @Test
  public void testRemoveAllOperationDifferentVM() {
    try {
      setupMembers();
      setOpAndClearVM(vm0, vm1);
      opsVM.invoke(() -> setBulkHook(vm0));
      runConsistencyTest(vm0, performRemoveAllOperation);
      checkForConsistencyErrors();
    } finally {
      opsVM.invoke(() -> resetHook());
    }
  }


  private void invokePut(VM whichVM) {
    if (whichVM == null) {
      doPut();
    } else {
      whichVM.invoke(() -> doPut());
    }
  }

  private void invokeRemove(VM whichVM) {
    if (whichVM == null) {
      doRemove();
    } else {
      whichVM.invoke(() -> doRemove());
    }
  }

  private void invokeInvalidate(VM whichVM) {
    if (whichVM == null) {
      doInvalidate();
    } else {
      whichVM.invoke(() -> doInvalidate());
    }
  }

  private void invokePutAll(VM whichVM) {
    if (whichVM == null) {
      doPutAll();
    } else {
      whichVM.invoke(() -> doPutAll());
    }
  }

  private void invokeRemoveAll(VM whichVM) {
    if (whichVM == null) {
      doRemoveAll();
    } else {
      whichVM.invoke(() -> doRemoveAll());
    }
  }

  private static void invokeClear(VM whichVM) {
    if (whichVM == null) {
      doClear();
    } else {
      whichVM.invoke(() -> doClear());
    }
  }

  // remote test methods

  private static boolean doesRegionEntryExist(String key) {
    return region.getRegionEntry(key) != null;
  }

  private static void doPut() {
    region.put(THE_KEY, THE_VALUE);
  }

  private static void doRemove() {
    region.remove(THE_KEY);
  }

  private static void doInvalidate() {
    region.invalidate(THE_KEY);
  }

  private static void doPutAll() {
    Map<Object, Object> map = generateKeyValues();
    region.putAll(map, "putAllCallback");
  }

  private static void doRemoveAll() {
    Map<Object, Object> map = generateKeyValues();
    region.removeAll(map.keySet(), "removeAllCallback");
  }

  private static void doClear() {
    region.clear();
  }

  private static void primeStep1(int cnt) {
    primeStep1Latch(cnt);
  }

  private static void primeStep2(int cnt) {
    primeStep2Latch(cnt);
  }

  private static void releaseStep1() {
    decrementStep1Latch();
  }

  SerializableRunnable performPutOperation = new SerializableRunnable("perform PUT") {
    @Override
    public void run() {
      try {
        invokePut(opsVM);
      } catch (Exception e) {
        fail("while performing PUT", e);
      }
    }
  };

  SerializableRunnable performNoAckPutOperation = new SerializableRunnable("perform NoAckPUT") {
    @Override
    public void run() throws InterruptedException {
      Runnable putThread1 = new Runnable() {
        public void run() {
          DistributedSystem.setThreadsSocketPolicy(false);
          doPut();
          DistributedSystem.releaseThreadsSockets();
        }
      };

      Runnable putThread2 = new Runnable() {
        public void run() {
          DistributedSystem.setThreadsSocketPolicy(false);
          awaitStep1Latch();
          doClear();
          DistributedSystem.releaseThreadsSockets();
        }
      };

      Thread t1 = new Thread(putThread1);
      Thread t2 = new Thread(putThread2);
      t2.start();
      t1.start();
      t1.join();
      t2.join();
    }
  };

  SerializableRunnable performRemoveOperation = new SerializableRunnable("perform REMOVE") {
    @Override
    public void run() {
      try {
        invokePut(opsVM);
        invokeRemove(opsVM);
      } catch (Exception e) {
        fail("while performing REMOVE", e);
      }
    }
  };

  SerializableRunnable performInvalidateOperation = new SerializableRunnable("perform INVALIDATE") {
    @Override
    public void run() {
      try {
        invokePut(opsVM);
        invokeInvalidate(opsVM);
      } catch (Exception e) {
        fail("while performing INVALIDATE", e);
      }
    }
  };

  SerializableRunnable performPutAllOperation = new SerializableRunnable("perform PUTALL") {
    @Override
    public void run() {
      try {
        invokePutAll(opsVM);
      } catch (Exception e) {
        fail("while performing PUTALL", e);
      }
    }
  };

  SerializableRunnable performRemoveAllOperation = new SerializableRunnable("perform REMOVEALL") {
    @Override
    public void run() {
      try {
        invokePutAll(opsVM);
        invokeRemoveAll(opsVM);
      } catch (Exception e) {
        fail("while performing REMOVEALL", e);
      }
    }
  };

  // helper methods

  private void setOpAndClearVM(VM opsTarget, VM clearTarget) {
    opsVM = opsTarget;
    clearVM = clearTarget;
  }

  private void setupMembers() {
    Host host = Host.getHost(0);
    vm0 = host.getVM(0);
    vm1 = host.getVM(1);
    vm0ID = createCache(vm0);
    vm1ID = createCache(vm1);
    String testName = getName();
    vm0.invoke(() -> createRegion(testName));
    vm1.invoke(() -> createRegion(testName));
  }

  private void setupNoAckMembers() {
    Host host = Host.getHost(0);
    vm0 = host.getVM(0);
    vm1 = host.getVM(1);
    vm0ID = createNoConserveSocketsCache(vm0);
    vm1ID = createNoConserveSocketsCache(vm1);
    String testName = getName();
    vm0.invoke(() -> createNOACKRegion(testName));
    vm1.invoke(() -> createNOACKRegion(testName));
  }

  private void runConsistencyTest(VM vm, SerializableRunnableIF theTest) {
    vm.invoke(theTest);
  }

  private void checkForConsistencyErrors() {
    Map<Object, Object> r0Contents = (Map<Object, Object>) vm0.invoke(() -> getRegionContents());
    Map<Object, Object> r1Contents = (Map<Object, Object>) vm1.invoke(() -> getRegionContents());

    String key = THE_KEY;
    softly.assertThat(r1Contents.get(key)).as("region contents are not consistent for key %s", key)
        .isEqualTo(r0Contents.get(key));
    softly.assertThat(checkRegionEntry(vm1, key))
        .as("region entries are not consistent for key %s", key)
        .isEqualTo(checkRegionEntry(vm0, key));

    for (int subi = 1; subi < 3; subi++) {
      String subkey = key + "-" + subi;
      if (r0Contents.containsKey(subkey)) {
        softly.assertThat(r1Contents.get(subkey))
            .as("region contents are not consistent for key %s", subkey)
            .isEqualTo(r0Contents.get(subkey));
      } else {
        softly.assertThat(r1Contents).as("expected containsKey for %s to return false", subkey)
            .doesNotContainKey(subkey);
      }
    }
  }

  public void resetHook() {
    ((AbstractRegionMap) region.entries).setARMLockTestHook(null);
  }

  public void setBasicHook(VM whichVM) {
    theOtherVM = whichVM;
    theHook = new ArmBasicClearHook();
    ((AbstractRegionMap) region.entries).setARMLockTestHook(theHook);
  }

  public void setRemoveAndInvalidateHook(VM whichVM) {
    theOtherVM = whichVM;
    theHook = new ArmRemoveAndInvalidateClearHook();
    ((AbstractRegionMap) region.entries).setARMLockTestHook(theHook);
  }

  public void setRemoteNoAckHook(VM whichVM) {
    theOtherVM = whichVM;
    theHook = new ArmNoAckRemoteHook();
    ((AbstractRegionMap) region.entries).setARMLockTestHook(theHook);
  }

  public void setLocalNoAckHook(VM whichVM) {
    theOtherVM = whichVM;
    theHook = new ArmNoAckLocalHook();
    ((AbstractRegionMap) region.entries).setARMLockTestHook(theHook);
  }

  public void setBulkHook(VM whichVM) {
    theOtherVM = whichVM;
    theHook = new ArmBulkClearHook();
    ((AbstractRegionMap) region.entries).setARMLockTestHook(theHook);
  }

  private InternalDistributedMember createCache(VM vm) {
    return (InternalDistributedMember) vm.invoke(new SerializableCallable<Object>() {
      public Object call() {
        cache = getCache(new CacheFactory().set("conserve-sockets", "true"));
        return getSystem().getDistributedMember();
      }
    });
  }

  private InternalDistributedMember createNoConserveSocketsCache(VM vm) {
    return (InternalDistributedMember) vm.invoke(new SerializableCallable<Object>() {
      public Object call() {
        cache = getCache(new CacheFactory().set("conserve-sockets", "false"));
        return getSystem().getDistributedMember();
      }
    });
  }

  private static void createRegion(String rgnName) {
    RegionFactory<Object, Object> rf = cache.createRegionFactory(RegionShortcut.REPLICATE);
    rf.setConcurrencyChecksEnabled(true);
    rf.setScope(Scope.DISTRIBUTED_ACK);
    region = (LocalRegion) rf.create(rgnName);
  }

  private static void createNOACKRegion(String rgnName) {
    RegionFactory<Object, Object> rf = cache.createRegionFactory(RegionShortcut.REPLICATE);
    rf.setConcurrencyChecksEnabled(true);
    rf.setScope(Scope.DISTRIBUTED_NO_ACK);
    region = (LocalRegion) rf.create(rgnName);
  }

  private static Map<Object, Object> generateKeyValues() {
    String key = THE_KEY;
    String value = THE_VALUE;
    Map<Object, Object> map = new HashMap<>();
    map.put(key, value);
    map.put(key + "-1", value + "-1");
    map.put(key + "-2", value + "-2");
    return map;
  }

  @SuppressWarnings("rawtypes")
  private static Map<Object, Object> getRegionContents() {
    Map<Object, Object> result = new HashMap<>();
    for (Iterator i = region.entrySet().iterator(); i.hasNext();) {
      Region.Entry e = (Region.Entry) i.next();
      result.put(e.getKey(), e.getValue());
    }
    return result;
  }

  private boolean checkRegionEntry(VM vm, String key) {
    boolean target = vm.invoke(() -> doesRegionEntryExist(key));
    return target;
  }

  static VM theOtherVM;
  transient static CountDownLatch step1Latch, step2Latch;

  public static void primeStep1Latch(int waitCount) {
    step1Latch = new CountDownLatch(waitCount);
  }

  public static void awaitStep1Latch() {
    try {
      step1Latch.await();
    } catch (InterruptedException e) {
    }
  }

  public static void decrementStep1Latch() {
    step1Latch.countDown();
  }

  public static void decrementRemoteStep1Latch() {
    theOtherVM.invoke(() -> decrementStep1Latch());
  }

  public static void primeStep2Latch(int waitCount) {
    step2Latch = new CountDownLatch(waitCount);
  }

  public static void awaitStep2Latch() {
    try {
      step2Latch.await();
    } catch (InterruptedException e) {
    }
  }

  public static void decrementStep2Latch() {
    step2Latch.countDown();
  }

  public static void decrementRemoteStep2Latch() {
    theOtherVM.invoke(() -> decrementStep2Latch());
  }

  /*
   * Test callback class used to hook the rvv locking mechanism with basic operations.
   */
  public static class ArmBasicClearHook extends ARMLockTestHookAdapter {
    @Override
    public void afterRelease(LocalRegion owner, CacheEvent event) {
      if ((event.getOperation().isCreate()) && owner.getName().startsWith("test")) {
        invokeClear(theOtherVM);
      }
    }
  }

  /*
   * Test callback class used to hook the rvv locking mechanism with basic operations.
   */
  public static class ArmRemoveAndInvalidateClearHook extends ARMLockTestHookAdapter {

    @Override
    public void afterRelease(LocalRegion owner, CacheEvent event) {
      if ((event.getOperation().isDestroy() || event.getOperation().isInvalidate())
          && owner.getName().startsWith("test")) {
        invokeClear(theOtherVM);
      }
    }
  }

  /*
   * Test callback class used to hook the rvv locking mechanism for NOACK testing.
   */
  public static class ArmNoAckRemoteHook extends ARMLockTestHookAdapter {
    @Override
    public void beforeLock(LocalRegion owner, CacheEvent event) {
      if (event.isOriginRemote() && event.getOperation().isCreate()
          && owner.getName().startsWith("test")) {
        theOtherVM.invoke(() -> releaseStep1()); // start clear
        awaitStep2Latch(); // wait for clear to complete
      }
    }
  }

  public static class ArmNoAckLocalHook extends ARMLockTestHookAdapter {
    @Override
    public void beforeStateFlushWait() {
      decrementRemoteStep2Latch();
    }
  }

  /*
   * Test callback class used to hook the rvv locking mechanism with bulk operations.
   */
  public static class ArmBulkClearHook extends ARMLockTestHookAdapter {
    @Override
    public void afterBulkRelease(LocalRegion region) {
      invokeClear(theOtherVM);
    }
  }
}
