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

import static org.apache.geode.distributed.ConfigurationProperties.LOG_FILE;
import static org.apache.geode.distributed.ConfigurationProperties.LOG_LEVEL;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

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

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.EntryNotFoundException;
import org.apache.geode.cache.InterestResultPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache.util.CacheListenerAdapter;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.test.dunit.DistributedTestCase;
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.SerializableRunnable;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.junit.categories.ClientServerTest;

@Category({ClientServerTest.class})
public class RemoveAllCacheListenerClientServerRegressionTest extends DistributedTestCase {

  private final AtomicInteger serverPort = new AtomicInteger(0);
  private final String REGION_NAME = "TestRegion";
  private VM clientVM = null;
  private VM serverVM = null;
  private final String BB_MAILBOX_KEY = "error";
  private final String NON_EXISTENT_KEY = "nonExistentKey";

  @Override
  public final void postSetUp() throws Exception {
    disconnectAllFromDS();
    IgnoredException.addIgnoredException("java.net.ConnectException");
    reset();
  }

  @Override
  public final void preTearDown() throws Exception {
    disconnectAllFromDS();
    reset();
  }

  private void reset() {
    getBlackboard().initBlackboard();
    getBlackboard().setMailbox(BB_MAILBOX_KEY, "");
    clientVM = null;
    serverVM = null;
    serverPort.set(0);
  }

  // ================================================================================
  // replicate on server
  // caching proxy client
  // server concurrency checks enabled
  // client concurrency checks enabled
  @Test
  public void removeAllListenerReplicateCachingProxyServerChecksClientChecksTest() {
    doRemoveAllTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  @Test
  public void destroyListenerReplicateCachingProxyServerChecksClientChecksTest() {
    doDestroyTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  @Test
  public void removeListenerReplicateCachingProxyServerChecksClientChecksTest() {
    doRemoveTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  // ================================================================================
  // partitioned on server
  // caching proxy client
  // server concurrency checks enabled
  // client concurrency checks enabled
  @Test
  public void removeAllListenerPartitionCachingProxyServerChecksClientChecksTest() {
    doRemoveAllTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  @Test
  public void destroyListenerPartitionCachingProxyServerChecksClientChecksTest() {
    doDestroyTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  @Test
  public void removeListenerPartitionCachingProxyServerChecksClientChecksTest() {
    doRemoveTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, true);
  }

  // ================================================================================
  // replicate on server
  // caching proxy client
  // server concurrency checks disabled
  // client concurrency checks enabled
  @Test
  public void removeAllListenerReplicateCachingProxyClientChecksOnlyTest() {
    doRemoveAllTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  @Test
  public void destroyListenerReplicateCachingProxyClientChecksOnlyTest() {
    doDestroyTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  @Test
  public void removeListenerReplicateCachingProxyClientChecksOnlyTest() {
    doRemoveTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  // ================================================================================
  // partitioned on server
  // caching proxy client
  // server concurrency checks disabled
  // client concurrency checks enabled
  @Test
  public void removeAllListenerPartitionCachingProxyClientChecksOnlyTest() {
    doRemoveAllTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  @Test
  public void destroyListenerPartitionCachingProxyClientChecksOnlyTest() {
    doDestroyTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  @Test
  public void removeListenerPartitionCachingProxyClientChecksOnlyTest() {
    doRemoveTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, true);
  }

  // ================================================================================
  // replicate on server
  // caching proxy client
  // server concurrency checks enabled
  // client concurrency checks disabled
  @Test
  public void removeAllListenerReplicateCachingProxyServerChecksOnlyTest() {
    doRemoveAllTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  @Test
  public void destroyListenerReplicateCachingProxyServerChecksOnlyTest() {
    doDestroyTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  @Test
  public void removeListenerReplicateCachingProxyServerChecksOnlyTest() {
    doRemoveTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  // ================================================================================
  // partitioned on server
  // caching proxy client
  // server concurrency checks enabled
  // client concurrency checks disabled
  @Test
  public void removeAllListenerPartitionCachingProxyServerChecksOnlyTest() {
    doRemoveAllTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  @Test
  public void destroyListenerPartitionCachingProxyServerChecksOnlyTest() {
    doDestroyTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  @Test
  public void removeListenerPartitionCachingProxyServerChecksOnlyTest() {
    doRemoveTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, true, false);
  }

  // ================================================================================
  // replicate on server
  // caching proxy client
  // server concurrency checks disabled
  // client concurrency checks disabled
  @Test
  public void removeAllListenerReplicateCachingProxyChecksDisabledTest() {
    doRemoveAllTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  @Test
  public void destroyListenerReplicateCachingProxyChecksDisabledTest() {
    doDestroyTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  @Test
  public void removeListenerReplicateCachingProxyChecksDisabledTest() {
    doRemoveTest(RegionShortcut.REPLICATE, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  // ================================================================================
  // partitioned on server
  // caching proxy client
  // server concurrency checks disabled
  // client concurrency checks disabled
  @Test
  public void removeAllListenerPartitionCachingProxyChecksDisabledTest() {
    doRemoveAllTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  @Test
  public void destroyListenerPartitionCachingProxyChecksDisabledTest() {
    doDestroyTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  @Test
  public void removeListenerPartitionCachingProxyChecksDisabledTest() {
    doRemoveTest(RegionShortcut.PARTITION, ClientRegionShortcut.CACHING_PROXY, false, false);
  }

  // ================================================================================
  private void setupGemFireCacheServer(RegionShortcut shortcut, boolean concChecks) {
    serverVM = Host.getHost(0).getVM(0);
    serverPort.set(AvailablePortHelper.getRandomAvailableTCPPort());
    String serverName = this.getClass().getSimpleName() + "_server";
    serverVM.invoke(new SerializableRunnable() {
      @Override
      public void run() {
        try {
          Cache cache = new CacheFactory().set("name", serverName).set(MCAST_PORT, "0")
              .set(LOG_FILE, serverName + ".log").set(LOG_LEVEL, "config")
              .set("locators", "localhost[" + DistributedTestUtils.getDUnitLocatorPort() + "]")
              .create();

          RegionFactory<String, String> regionFactory = cache.createRegionFactory(shortcut);
          regionFactory.addCacheListener(new TestListener());
          regionFactory.setConcurrencyChecksEnabled(concChecks);
          regionFactory.create(REGION_NAME);

          CacheServer cacheServer = cache.addCacheServer();
          cacheServer.setPort(serverPort.get());
          cacheServer.setMaxConnections(10);
          cacheServer.start();
          assertTrue("Cache Server is not running!", cacheServer.isRunning());
        } catch (UnknownHostException ignore) {
          throw new RuntimeException(ignore);
        } catch (IOException e) {
          throw new RuntimeException("Failed to start cache server " + serverName + " on port "
              + serverPort.get() + ": " + e.getStackTrace());
        }
      }
    });
  }

  private void setupGemFireClientCache(ClientRegionShortcut shortcut, boolean concChecks) {
    clientVM = Host.getHost(0).getVM(1);
    clientVM.invoke(() -> {
      ClientCache clientCache = new ClientCacheFactory().create();

      PoolFactory poolFactory = PoolManager.createFactory();
      poolFactory.setMaxConnections(10);
      poolFactory.setMinConnections(1);
      poolFactory.setReadTimeout(5000);
      poolFactory.setSubscriptionEnabled(true);
      poolFactory.addServer("localhost", serverPort.get());
      Pool pool = poolFactory.create("serverConnectionPool");
      assertNotNull("The 'serverConnectionPool' was not properly configured and initialized!",
          pool);

      ClientRegionFactory<String, String> regionFactory =
          clientCache.createClientRegionFactory(shortcut);
      regionFactory.setPoolName(pool.getName());
      regionFactory.addCacheListener(new TestListener());
      regionFactory.setConcurrencyChecksEnabled(concChecks);
      Region<String, String> aRegion = regionFactory.create(REGION_NAME);
      assertNotNull("The region " + REGION_NAME + " was not properly configured and initialized",
          aRegion);

      aRegion.registerInterest("ALL_KEYS", InterestResultPolicy.KEYS_VALUES);
    });
  }

  private class TestListener extends CacheListenerAdapter<String, String> {

    @Override
    public void afterDestroy(EntryEvent event) {
      if (event.getKey().equals(NON_EXISTENT_KEY)) {
        String errStr = "event fired for non-existent key " + event.getKey() + "; " + event + "\n"
            + getStackTrace();
        getBlackboard().setMailbox(BB_MAILBOX_KEY, errStr);
      }
    }

  }

  /**
   * Add values to a region, then do a removeAll using both existent and non-existent keys. If a
   * listener event is invoked on a non-existent key, the listener writes it to the blackboard. Read
   * from the blackboard and fail the test if a listener event was invoked for a non-existent key.
   */
  private void doRemoveAllTest(RegionShortcut serverShortcut, ClientRegionShortcut clientShortcut,
      boolean serverConcChecks, boolean clientConcChecks) {
    setupGemFireCacheServer(serverShortcut, serverConcChecks);
    setupGemFireClientCache(clientShortcut, clientConcChecks);
    clientVM.invoke(() -> {
      Region<String, String> aRegion = ClientCacheFactory.getAnyInstance().getRegion(REGION_NAME);
      aRegion.put("key1", "value1");
      aRegion.put("key2", "value2");
      aRegion.put("key3", "value3");

      Set<String> removeAllSet = new HashSet<String>();
      removeAllSet.add("key1");
      removeAllSet.add("key2");
      removeAllSet.add(NON_EXISTENT_KEY);
      aRegion.removeAll(removeAllSet);
      Object error = getBlackboard().getMailbox(BB_MAILBOX_KEY);
      assertNotNull(error);
      assertEquals("", error);
    });
  }

  private void doDestroyTest(RegionShortcut serverShortcut, ClientRegionShortcut clientShortcut,
      boolean serverConcChecks, boolean clientConcChecks) {
    setupGemFireCacheServer(serverShortcut, serverConcChecks);
    setupGemFireClientCache(clientShortcut, clientConcChecks);
    clientVM.invoke(() -> {
      Region<String, String> aRegion = ClientCacheFactory.getAnyInstance().getRegion(REGION_NAME);
      try {
        aRegion.destroy(NON_EXISTENT_KEY);
      } catch (EntryNotFoundException e) {
        // expected
      }
      Object error = getBlackboard().getMailbox(BB_MAILBOX_KEY);
      assertNotNull(error);
      assertEquals("", error);
    });
  }

  private void doRemoveTest(RegionShortcut serverShortcut, ClientRegionShortcut clientShortcut,
      boolean serverConcChecks, boolean clientConcChecks) {
    setupGemFireCacheServer(serverShortcut, serverConcChecks);
    setupGemFireClientCache(clientShortcut, clientConcChecks);
    clientVM.invoke(() -> {
      getBlackboard().setMailbox(BB_MAILBOX_KEY, "");
      Region<String, String> aRegion = ClientCacheFactory.getAnyInstance().getRegion(REGION_NAME);
      Object returnedValue = aRegion.remove(NON_EXISTENT_KEY);
      assertNull(returnedValue);
      Object error = getBlackboard().getMailbox(BB_MAILBOX_KEY);
      assertNotNull(error);
      assertEquals("", error);
    });
  }

  /** Get a stack trace of the current stack and return it as a String */
  public static String getStackTrace() {
    try {
      throw new Exception("Exception to get stack trace");
    } catch (Exception e) {
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw, true));
      return sw.toString();
    }
  }

}
