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

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPort;
import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

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

import org.apache.geode.LogWriter;
import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.CacheWriterException;
import org.apache.geode.cache.InterestResultPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.client.ServerConnectivityException;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.test.dunit.Assert;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.NetworkUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.internal.JUnit4DistributedTestCase;
import org.apache.geode.test.junit.categories.ClientSubscriptionTest;

/**
 * DUnit Test for use-cases of various {@link InterestResultPolicy} types.
 */
@Category({ClientSubscriptionTest.class})
public class InterestResultPolicyDUnitTest extends JUnit4DistributedTestCase {

  /** test VM */
  VM vm0, vm1 = null;

  /** total entries pre-populated on server */
  static final int PREPOPULATED_ENTRIES = 10;

  /** the cache instance for the test */
  private static Cache cache = null;

  private int PORT;

  private static final String REGION_NAME =
      InterestResultPolicyDUnitTest.class.getSimpleName() + "_region";

  private IgnoredException expectedEx;

  /**
   * Creates the server cache and populates it with some entries
   */
  @Override
  public final void postSetUp() throws Exception {
    disconnectAllFromDS();
    Wait.pause(5000);
    final Host host = Host.getHost(0);
    vm0 = host.getVM(0);
    vm1 = host.getVM(1);
    PORT =
        vm0.invoke(InterestResultPolicyDUnitTest::createServerCache);
    vm0.invoke(InterestResultPolicyDUnitTest::populateServerCache);
  }

  /**
   * Closes the cache on server and client
   */
  @Override
  public final void preTearDown() throws Exception {
    // might get ServerConnectivityExceptions during shutdown
    expectedEx =
        IgnoredException.addIgnoredException(ServerConnectivityException.class.getName());
    // close server
    vm0.invoke(InterestResultPolicyDUnitTest::closeCache);
    // close client
    vm1.invoke(InterestResultPolicyDUnitTest::closeCache);
  }

  @Override
  public final void postTearDown() throws Exception {
    if (expectedEx != null) {
      expectedEx.remove();
    }
  }

  /**
   * This test does the following<br>
   * 1)Create client cache and region<br>
   * 2)Call registerInterest with all keys pre-populated on server and policy:NONE<br>
   * 3)At the end of registerInterest call, verify that no entries are created in the client
   * cache<br>
   */
  @Test
  public void testPolicyNone() {
    LogWriter logger = getSystem().getLogWriter();
    logger.fine("testPolicyNone BEGIN");
    Object[] objArr = new Object[2];
    objArr[0] = InterestResultPolicy.NONE;
    objArr[1] = PREPOPULATED_ENTRIES;
    Host host = Host.getHost(0);
    vm1.invoke(() -> InterestResultPolicyDUnitTest
        .createClientCache(NetworkUtils.getServerHostName(host), PORT));
    vm1.invoke(InterestResultPolicyDUnitTest.class, "registerInterest", objArr);
    vm1.invoke(InterestResultPolicyDUnitTest.class, "verifyResult", objArr);
    logger.fine("testPolicyNone END");
  }

  /**
   * This test does the following<br>
   * 1)Create client cache and region<br>
   * 2)Call registerInterest with all keys pre-populated on server and policy:KEYS<br>
   * 3)At the end of registerInterest call, verify that entries are created in the client cache with
   * value null<br>
   */
  @Test
  public void testPolicyKeys() {
    LogWriter logger = getSystem().getLogWriter();
    logger.fine("testPolicyKeys BEGIN");
    Object[] objArr = new Object[2];
    objArr[0] = InterestResultPolicy.KEYS;
    objArr[1] = PREPOPULATED_ENTRIES;
    Host host = Host.getHost(0);
    vm1.invoke(() -> InterestResultPolicyDUnitTest
        .createClientCache(NetworkUtils.getServerHostName(host), PORT));
    vm1.invoke(InterestResultPolicyDUnitTest.class, "registerInterest", objArr);
    vm1.invoke(InterestResultPolicyDUnitTest.class, "verifyResult", objArr);
    logger.fine("testPolicyKeys END");
  }

  /**
   * This test does the following<br>
   * 1)Create client cache and region<br>
   * 2)Call registerInterest with all keys pre-populated on server and policy:KEYS_VALUES<br>
   * 3)At the end of registerInterest call, verify that all entries are created in the client cache
   * with values<br>
   */
  @Test
  public void testPolicyKeysValues() {
    LogWriter logger = getSystem().getLogWriter();
    logger.fine("testPolicyKeyValues BEGIN");
    Object[] objArr = new Object[2];
    objArr[0] = InterestResultPolicy.KEYS_VALUES;
    objArr[1] = PREPOPULATED_ENTRIES;
    Host host = Host.getHost(0);
    vm1.invoke(() -> InterestResultPolicyDUnitTest
        .createClientCache(NetworkUtils.getServerHostName(host), PORT));
    vm1.invoke(InterestResultPolicyDUnitTest.class, "registerInterest", objArr);
    vm1.invoke(InterestResultPolicyDUnitTest.class, "verifyResult", objArr);
    logger.fine("testPolicyKeyValues END");
  }

  /**
   * This test does the following<br>
   * 1)Create client cache and region<br>
   * 2)Call registerInterest with specific keys (having extra keys other than those pre-populated on
   * server cache) and policy:KEYS<br>
   * 3)At the end of registerInterest call, verify that only prepolulated entries are created in the
   * client cache with null values (entries for keys in the keylist which are not on the server
   * should not be created on the client as a result of registerInterest call)<br>
   */
  @Test
  public void testBug35358() {
    Host host = Host.getHost(0);
    LogWriter logger = getSystem().getLogWriter();
    logger.fine("testBug35358 BEGIN");
    Object[] objArr = new Object[2];
    objArr[0] = InterestResultPolicy.KEYS;
    /* registering for 5 extra keys */
    objArr[1] = PREPOPULATED_ENTRIES + 5;
    vm1.invoke(() -> InterestResultPolicyDUnitTest
        .createClientCache(NetworkUtils.getServerHostName(host), PORT));
    vm1.invoke(InterestResultPolicyDUnitTest.class, "registerInterest", objArr);
    vm1.invoke(InterestResultPolicyDUnitTest.class, "verifyResult", objArr);
    Integer cnt = (Integer) vm0.invoke(InterestResultPolicyDUnitTest::getEntryCount);
    assertEquals(cnt.intValue(), PREPOPULATED_ENTRIES);
    logger.fine("testBug35358 END");
  }

  /**
   * Creates cache instance
   *
   * @param props - properties of the distributed system
   * @throws Exception - thrown if any problem occurs while connecting to distributed system or
   *         creating cache
   */
  private Cache createCache(Properties props) throws Exception {
    DistributedSystem ds = getSystem(props);
    Cache cache = null;
    cache = CacheFactory.create(ds);
    if (cache == null) {
      throw new Exception("CacheFactory.create() returned null ");
    }
    return cache;
  }

  /**
   * Closes the cache instance created and disconnects from the distributed system.
   */
  public static void closeCache() {
    if (cache != null && !cache.isClosed()) {
      cache.close();
      cache.getDistributedSystem().disconnect();
    }
  }

  /**
   * Creates the cache and starts the server
   *
   * @throws Exception - thrown if any problem occurs in creating server cache or starting the
   *         server
   */
  public static Integer createServerCache() throws Exception {
    InterestResultPolicyDUnitTest test = new InterestResultPolicyDUnitTest();
    cache = test.createCache(new Properties());
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    RegionAttributes attrs = factory.create();
    cache.createRegion(REGION_NAME, attrs);
    CacheServer server = cache.addCacheServer();
    int port = getRandomAvailableTCPPort();
    server.setPort(port);
    server.setNotifyBySubscription(true);
    server.setSocketBufferSize(32768);
    server.start();
    return port;
  }

  /**
   * Populates the server cache by putting some entries
   *
   * @throws Exception - thrown if any problem occurs while creating entries at server cache
   */
  public static void populateServerCache() throws Exception {
    Region region1 = cache.getRegion(SEPARATOR + REGION_NAME);
    for (int i = 0; i < PREPOPULATED_ENTRIES; i++) {
      region1.put("key-" + i, "val-" + i);
    }
  }

  /**
   * Creates the client cache and region.
   *
   * @throws Exception - thrown if any problem occurs in creating cache or region
   */
  public static void createClientCache(String host, Integer port) throws Exception {
    int PORT = port;
    InterestResultPolicyDUnitTest test = new InterestResultPolicyDUnitTest();
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");
    cache = test.createCache(props);
    Pool p = PoolManager.createFactory().addServer(host, port)
        .setSubscriptionEnabled(true).setSubscriptionRedundancy(-1).setReadTimeout(10000)
        .setSocketBufferSize(32768).setMinConnections(3)
        // .setRetryAttempts(5)
        // .setRetryInterval(10000)
        .create("InterestResultPolicyDUnitTestPool");
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    factory.setPoolName(p.getName());
    RegionAttributes attrs = factory.create();
    cache.createRegion(REGION_NAME, attrs);
  }

  /**
   * Registers the test region on client with a keylist containing all keys pre-populated on server
   * and a given {@link InterestResultPolicy} type.
   *
   * @param interestPolicy - InterestResultPolicy type specified (NONE,KEYS,KEY_VALUES or DEFAULT)
   */
  public static void registerInterest(Object interestPolicy, Object totalKeysToRegister) {
    InterestResultPolicy policy = (InterestResultPolicy) interestPolicy;
    int totalKeys = (Integer) totalKeysToRegister;
    Region region1 = cache.getRegion(SEPARATOR + REGION_NAME);
    LogWriter logger = cache.getLogger();
    logger.fine("Registering interest in " + totalKeys + " keys");
    List keylist = new ArrayList();
    for (int i = 0; i < totalKeys; i++) {
      keylist.add("key-" + i);
    }

    try {
      region1.registerInterest(keylist, policy);
    } catch (CacheWriterException e) {
      Assert.fail("failed to register interestlist for the client", e);
    }
  }

  /**
   * Verifies the number of entries (including values) created on the test region at the end of
   * {@link Region#registerInterest} call depending on the type of {@link InterestResultPolicy}
   * registered for the region.
   *
   * @param interestPolicy - {@link InterestResultPolicy} registered for the region
   */
  public static void verifyResult(Object interestPolicy, Object totalKeysRegistered) {
    Region region1 = cache.getRegion(SEPARATOR + REGION_NAME);
    int entriesSize = region1.entrySet(false).size();
    int keysSize = region1.keySet().size();
    int valuesSize = region1.values().size();
    InterestResultPolicy policy = (InterestResultPolicy) interestPolicy;
    LogWriter logger = cache.getLogger();

    logger.fine("policy = " + policy + " ==> entries = " + entriesSize + " ;keys = " + keysSize
        + ";values = " + valuesSize);

    if (policy.isNone()) {
      // nothing should be created on client cache
      assertEquals(0, entriesSize);
      assertEquals(0, keysSize);
      assertEquals(0, valuesSize);
    } else if (policy.isKeys()) {
      // all keys should be created with values null
      assertEquals(PREPOPULATED_ENTRIES, entriesSize);
      assertEquals(PREPOPULATED_ENTRIES, keysSize);
      assertEquals(0, valuesSize);
    } else if (policy.isKeysValues()) {
      // all the keys and values should be created
      assertEquals(PREPOPULATED_ENTRIES, entriesSize);
      assertEquals(PREPOPULATED_ENTRIES, keysSize);
      assertEquals(PREPOPULATED_ENTRIES, valuesSize);
    }
  }

  /**
   * Gets the total number of keys in the test region
   *
   * @return - total keys
   */
  public static Object getEntryCount() {
    Region region1 = cache.getRegion(SEPARATOR + REGION_NAME);
    int keysSize = region1.keySet().size();
    return keysSize;
  }
}
