/*
 * 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.apache.geode.distributed.ConfigurationProperties.*;
import static org.junit.Assert.*;

import java.util.Map;
import java.util.Properties;

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

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.CacheException;
import org.apache.geode.cache.CacheListener;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.EntryNotFoundException;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.cache.LocalRegion;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.AsyncInvocation;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
import org.apache.geode.test.dunit.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.junit.categories.DistributedTest;
import org.apache.geode.test.junit.categories.FlakyTest;

@Category(DistributedTest.class)
public class DistributedNoAckRegionCCEDUnitTest extends DistributedNoAckRegionDUnitTest {

  static volatile boolean ListenerBlocking;

  @Override
  public Properties getDistributedSystemProperties() {
    Properties p = super.getDistributedSystemProperties();
    p.put(CONSERVE_SOCKETS, "false");
    if (distributedSystemID > 0) {
      p.put(DISTRIBUTED_SYSTEM_ID, "" + distributedSystemID);
    }
    p.put(SOCKET_BUFFER_SIZE, "" + 2000000);
    return p;
  }

  /**
   * Returns region attributes for a <code>GLOBAL</code> region
   */
  protected RegionAttributes getRegionAttributes() {
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_NO_ACK);
    factory.setDataPolicy(DataPolicy.REPLICATE);
    factory.setConcurrencyChecksEnabled(true);
    return factory.create();
  }

  protected RegionAttributes getRegionAttributes(String type) {
    RegionAttributes ra = getCache().getRegionAttributes(type);
    if (ra == null) {
      throw new IllegalStateException("The region shortcut " + type + " has been removed.");
    }
    AttributesFactory factory = new AttributesFactory(ra);
    factory.setScope(Scope.DISTRIBUTED_NO_ACK);
    factory.setConcurrencyChecksEnabled(true);
    return factory.create();
  }

  @Override
  public void sendSerialMessageToAll() {
    try {
      org.apache.geode.distributed.internal.SerialAckedMessage msg =
          new org.apache.geode.distributed.internal.SerialAckedMessage();
      msg.send(InternalDistributedSystem.getConnectedInstance().getDM()
          .getNormalDistributionManagerIds(), false);
    } catch (Exception e) {
      throw new RuntimeException("Unable to send serial message due to exception", e);
    }
  }


  @Override
  @Test
  public void testLocalDestroy() throws InterruptedException {
    // replicates don't allow local destroy
  }

  @Override
  @Test
  public void testEntryTtlLocalDestroy() throws InterruptedException {
    // replicates don't allow local destroy
  }

  @Test
  public void testClearWithManyEventsInFlight() throws Exception {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);
    VM vm2 = host.getVM(2);
    VM vm3 = host.getVM(3);

    // create replicated regions in VM 0 and 1, then perform concurrent ops
    // on the same key while creating the region in VM2. Afterward make
    // sure that all three regions are consistent

    final String name = this.getUniqueName() + "-CC";
    createRegionWithAttribute(vm0, name, false);
    createRegionWithAttribute(vm1, name, false);
    createRegionWithAttribute(vm2, name, false);
    createRegionWithAttribute(vm3, name, false);
    vm0.invoke(() -> DistributedNoAckRegionCCEDUnitTest.addBlockingListener());
    vm1.invoke(() -> DistributedNoAckRegionCCEDUnitTest.addBlockingListener());
    vm2.invoke(() -> DistributedNoAckRegionCCEDUnitTest.addBlockingListener());
    AsyncInvocation vm0Ops = vm0.invokeAsync(() -> DistributedNoAckRegionCCEDUnitTest.doManyOps());
    AsyncInvocation vm1Ops = vm1.invokeAsync(() -> DistributedNoAckRegionCCEDUnitTest.doManyOps());
    AsyncInvocation vm2Ops = vm2.invokeAsync(() -> DistributedNoAckRegionCCEDUnitTest.doManyOps());
    // pause to let a bunch of operations build up
    Wait.pause(5000);
    AsyncInvocation a0 = vm3.invokeAsync(() -> DistributedNoAckRegionCCEDUnitTest.clearRegion());
    vm0.invoke(() -> DistributedNoAckRegionCCEDUnitTest.unblockListener());
    vm1.invoke(() -> DistributedNoAckRegionCCEDUnitTest.unblockListener());
    vm2.invoke(() -> DistributedNoAckRegionCCEDUnitTest.unblockListener());
    waitForAsyncProcessing(a0, "");
    waitForAsyncProcessing(vm0Ops, "");
    waitForAsyncProcessing(vm1Ops, "");
    waitForAsyncProcessing(vm2Ops, "");

    // if (a0failed && a1failed) {
    // fail("neither member saw event conflation - check stats for " + name);
    // }
    Wait.pause(2000);// this test has with noack, thus we should wait before validating entries
    // check consistency of the regions
    Map r0Contents = (Map) vm0.invoke(() -> this.getCCRegionContents());
    Map r1Contents = (Map) vm1.invoke(() -> this.getCCRegionContents());
    Map r2Contents = (Map) vm2.invoke(() -> this.getCCRegionContents());
    Map r3Contents = (Map) vm3.invoke(() -> this.getCCRegionContents());

    for (int i = 0; i < 10; i++) {
      String key = "cckey" + i;
      assertEquals("region contents are not consistent", r0Contents.get(key), r1Contents.get(key));
      assertEquals("region contents are not consistent", r1Contents.get(key), r2Contents.get(key));
      assertEquals("region contents are not consistent", r2Contents.get(key), r3Contents.get(key));
      for (int subi = 1; subi < 3; subi++) {
        String subkey = key + "-" + subi;
        assertEquals("region contents are not consistent", r0Contents.get(subkey),
            r1Contents.get(subkey));
        assertEquals("region contents are not consistent", r1Contents.get(subkey),
            r2Contents.get(subkey));
        assertEquals("region contents are not consistent", r2Contents.get(subkey),
            r3Contents.get(subkey));
      }
    }
  }

  static void addBlockingListener() {
    ListenerBlocking = true;
    CCRegion.getAttributesMutator().addCacheListener(new CacheListenerAdapter() {
      public void afterCreate(EntryEvent event) {
        onEvent(event);
      }

      private void onEvent(EntryEvent event) {
        boolean blocked = false;
        if (event.isOriginRemote()) {
          synchronized (this) {
            while (ListenerBlocking) {
              LogWriterUtils.getLogWriter()
                  .info("blocking cache operations for " + event.getDistributedMember());
              blocked = true;
              try {
                wait();
              } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                LogWriterUtils.getLogWriter().info("blocking cache listener interrupted");
                return;
              }
            }
          }
          if (blocked) {
            LogWriterUtils.getLogWriter()
                .info("allowing cache operations for " + event.getDistributedMember());
          }
        }
      }

      @Override
      public void close() {
        LogWriterUtils.getLogWriter().info("closing blocking listener");
        ListenerBlocking = false;
        synchronized (this) {
          notifyAll();
        }
      }

      @Override
      public void afterUpdate(EntryEvent event) {
        onEvent(event);
      }

      @Override
      public void afterInvalidate(EntryEvent event) {
        onEvent(event);
      }

      @Override
      public void afterDestroy(EntryEvent event) {
        onEvent(event);
      }
    });
  }

  static void doManyOps() {
    // do not include putAll, which requires an Ack to detect failures
    doOpsLoopNoFlush(5000, false, false);
  }

  static void unblockListener() {
    CacheListener listener = CCRegion.getCacheListener();
    ListenerBlocking = false;
    synchronized (listener) {
      listener.notifyAll();
    }
  }

  static void clearRegion() {
    CCRegion.clear();
  }

  /**
   * This test creates a server cache in vm0 and a peer cache in vm1. It then tests to see if GII
   * transferred tombstones to vm1 like it's supposed to. A client cache is created in vm2 and the
   * same sort of check is performed for register-interest.
   */

  @Test
  public void testGIISendsTombstones() throws Exception {
    versionTestGIISendsTombstones();
  }


  protected void do_version_recovery_if_necessary(final VM vm0, final VM vm1, final VM vm2,
      final Object[] params) {
    // do nothing here
  }

  /**
   * This tests the concurrency versioning system to ensure that event conflation happens correctly
   * and that the statistic is being updated properly
   */
  @Test
  public void testConcurrentEvents() throws Exception {
    versionTestConcurrentEvents();
  }


  @Test
  public void testClearWithConcurrentEvents() throws Exception {
    // need to figure out how to flush clear() ops for verification steps
  }

  @Test
  public void testClearWithConcurrentEventsAsync() throws Exception {
    // need to figure out how to flush clear() ops for verification steps
  }

  @Test
  public void testClearOnNonReplicateWithConcurrentEvents() throws Exception {
    // need to figure out how to flush clear() ops for verification steps
  }


  @Test
  public void testTombstones() throws Exception {
    // for (int i=0; i<1000; i++) {
    // System.out.println("starting run #"+i);
    versionTestTombstones();
    // if (i < 999) {
    // tearDown();
    // setUp();
    // }
    // }
  }



  @Test
  public void testOneHopKnownIssues() {
    Host host = Host.getHost(0);
    VM vm0 = host.getVM(0);
    VM vm1 = host.getVM(1);
    VM vm2 = host.getVM(2);
    VM vm3 = host.getVM(3); // this VM, but treat as a remote for uniformity

    // create an empty region in vm0 and replicated regions in VM 1 and 3,
    // then perform concurrent ops
    // on the same key while creating the region in VM2. Afterward make
    // sure that all three regions are consistent

    final String name = this.getUniqueName() + "-CC";
    SerializableRunnable createRegion = new SerializableRunnable("Create Region") {
      public void run() {
        try {
          final RegionFactory f;
          int vmNumber = VM.getCurrentVMNum();
          switch (vmNumber) {
            case 0:
              f = getCache().createRegionFactory(
                  getRegionAttributes(RegionShortcut.REPLICATE_PROXY.toString()));
              break;
            case 1:
              f = getCache()
                  .createRegionFactory(getRegionAttributes(RegionShortcut.REPLICATE.toString()));
              f.setDataPolicy(DataPolicy.NORMAL);
              break;
            default:
              f = getCache().createRegionFactory(getRegionAttributes());
              break;
          }
          CCRegion = (LocalRegion) f.create(name);
        } catch (CacheException ex) {
          Assert.fail("While creating region", ex);
        }
      }
    };

    vm0.invoke(createRegion); // empty
    vm1.invoke(createRegion); // normal
    vm2.invoke(createRegion); // replicate

    // case 1: entry already invalid on vm2 (replicate) is invalidated by vm0 (empty)
    final String invalidationKey = "invalidationKey";
    final String destroyKey = "destroyKey";
    SerializableRunnable test =
        new SerializableRunnable("case 1: second invalidation not applied or distributed") {
          public void run() {
            CCRegion.put(invalidationKey, "initialValue");

            int invalidationCount = CCRegion.getCachePerfStats().getInvalidates();
            CCRegion.invalidate(invalidationKey);
            CCRegion.invalidate(invalidationKey);
            assertEquals(invalidationCount + 1, CCRegion.getCachePerfStats().getInvalidates());

            // also test destroy() while we're at it. It should throw an exception
            int destroyCount = CCRegion.getCachePerfStats().getDestroys();
            CCRegion.destroy(invalidationKey);
            try {
              CCRegion.destroy(invalidationKey);
              fail("expected an EntryNotFoundException");
            } catch (EntryNotFoundException e) {
              // expected
            }
            assertEquals(destroyCount + 1, CCRegion.getCachePerfStats().getDestroys());
          }
        };
    vm0.invoke(test);

    // now do the same with the datapolicy=normal region
    test.setName("case 2: second invalidation not applied or distributed");
    vm1.invoke(test);
  }

  /**
   * This tests the concurrency versioning system to ensure that event conflation happens correctly
   * and that the statistic is being updated properly
   */
  @Category(FlakyTest.class) // GEODE-976: time sensitive, thread sleeps, relies on stat values
  @Test
  public void testConcurrentEventsOnEmptyRegion() {
    versionTestConcurrentEventsOnEmptyRegion();
  }

  /**
   * This tests the concurrency versioning system to ensure that event conflation happens correctly
   * and that the statistic is being updated properly
   */
  @Test
  public void testConcurrentEventsOnNonReplicatedRegion() {
    versionTestConcurrentEventsOnNonReplicatedRegion();
  }


  @Test
  public void testGetAllWithVersions() {
    versionTestGetAllWithVersions();
  }



  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // these methods can be uncommented to inhibit test execution
  // when new tests are added
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  // @Override
  // public void testNonblockingGetInitialImage() throws Throwable {
  // }
  // @Override
  // public void testConcurrentOperations() throws Exception {
  // }
  //
  // @Override
  // public void testDistributedUpdate() {
  // }
  //
  // @Override
  // public void testDistributedGet() {
  // }
  //
  // @Override
  // public void testDistributedPutNoUpdate() throws InterruptedException {
  // }
  //
  // @Override
  // public void testDefinedEntryUpdated() {
  // }
  //
  // @Override
  // public void testDistributedDestroy() throws InterruptedException {
  // }
  //
  // @Override
  // public void testDistributedRegionDestroy() throws InterruptedException {
  // }
  //
  // @Override
  // public void testLocalRegionDestroy() throws InterruptedException {
  // }
  //
  // @Override
  // public void testDistributedInvalidate() {
  // }
  //
  // @Override
  // public void testDistributedInvalidate4() throws InterruptedException {
  // }
  //
  // @Override
  // public void testDistributedRegionInvalidate() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteCacheListener() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteCacheListenerInSubregion() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteCacheLoader() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteCacheLoaderArg() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteCacheLoaderException() throws InterruptedException {
  // }
  //
  // @Override
  // public void testCacheLoaderWithNetSearch() throws CacheException {
  // }
  //
  // @Override
  // public void testCacheLoaderWithNetLoad() throws CacheException {
  // }
  //
  // @Override
  // public void testNoRemoteCacheLoader() throws InterruptedException {
  // }
  //
  // @Override
  // public void testNoLoaderWithInvalidEntry() {
  // }
  //
  // @Override
  // public void testRemoteCacheWriter() throws InterruptedException {
  // }
  //
  // @Override
  // public void testLocalAndRemoteCacheWriters() throws InterruptedException {
  // }
  //
  // @Override
  // public void testCacheLoaderModifyingArgument() throws InterruptedException {
  // }
  //
  // @Override
  // public void testRemoteLoaderNetSearch() throws CacheException {
  // }
  //
  // @Override
  // public void testLocalCacheLoader() {
  // }
  //
  // @Override
  // public void testDistributedPut() throws Exception {
  // }
  //
  // @Override
  // public void testReplicate() throws InterruptedException {
  // }
  //
  // @Override
  // public void testDeltaWithReplicate() throws InterruptedException {
  // }
  //
  // @Override
  // public void testGetInitialImage() {
  // }
  //
  // @Override
  // public void testLargeGetInitialImage() {
  // }
  //
  // @Override
  // public void testMirroredDataFromNonMirrored() throws InterruptedException {
  // }
  //
  // @Override
  // public void testNoMirroredDataToNonMirrored() throws InterruptedException {
  // }
  //
  // @Override
  // public void testMirroredLocalLoad() {
  // }
  //
  // @Override
  // public void testMirroredNetLoad() {
  // }
  //
  // @Override
  // public void testNoRegionKeepAlive() throws InterruptedException {
  // }
  //
  // @Override
  // public void testNetSearchObservesTtl() throws InterruptedException {
  // }
  //
  // @Override
  // public void testNetSearchObservesIdleTime() throws InterruptedException {
  // }
  //
  // @Override
  // public void testEntryTtlDestroyEvent() throws InterruptedException {
  // }
  //
  // @Override
  // public void testUpdateResetsIdleTime() throws InterruptedException {
  // }
  // @Override
  // public void testTXNonblockingGetInitialImage() throws Throwable {
  // }
  //
  // @Override
  // public void testNBRegionInvalidationDuringGetInitialImage() throws Throwable {
  // }
  //
  // @Override
  // public void testNBRegionDestructionDuringGetInitialImage() throws Throwable {
  // }
  //
  // @Override
  // public void testNoDataSerializer() {
  // }
  //
  // @Override
  // public void testNoInstantiator() {
  // }
  //
  // @Override
  // public void testTXSimpleOps() throws Exception {
  // }
  //
  // @Override
  // public void testTXUpdateLoadNoConflict() throws Exception {
  // }
  //
  // @Override
  // public void testTXMultiRegion() throws Exception {
  // }
  //
  // @Override
  // public void testTXRmtMirror() throws Exception {
  // }


}
