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

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.fail;

import java.util.Iterator;
import java.util.Properties;

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

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.cache.client.internal.PoolImpl;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.cache30.ClientServerTestCase;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.test.dunit.Host;
import org.apache.geode.test.dunit.LogWriterUtils;
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;

/**
 * This is Dunit test for bug 36109. This test has a cache-client having a primary and a secondary
 * cache-server as its endpoint. Primary does some operations and is stopped, the client fails over
 * to secondary and does some operations and it is verified that the 'invalidates' stats at the
 * client is same as the total number of operations done by both primary and secondary. The bug was
 * appearing because invalidate stats was part of Endpoint which used to get closed during fail over
 * , with the failed endpoint getting closed. This bug has been fixed by moving the invalidate stat
 * to be part of our implementation.
 */
@Category({ClientSubscriptionTest.class})
@Ignore("Test was disabled by renaming to DisabledTest")
public class StatsBugDUnitTest extends JUnit4DistributedTestCase {

  /** primary cache server */
  VM primary = null;

  /** secondary cache server */
  VM secondary = null;

  /** the cache client */
  VM client1 = null;

  /** the cache */
  private static Cache cache = null;

  /** port for the primary cache server */
  private static int PORT1;

  /** port for the secondary cache server */
  private static int PORT2;

  /** name of the test region */
  private static final String REGION_NAME = StatsBugDUnitTest.class.getSimpleName() + "_Region";

  /** brige-writer instance( used to get connection proxy handle) */
  private static PoolImpl pool = null;

  /** total number of cache servers */
  private static final int TOTAL_SERVERS = 2;

  /** number of puts done by each server */
  private static final int PUTS_PER_SERVER = 10;

  /** prefix added to the keys of events generated on primary */
  private static final String primaryPrefix = "primary_";

  /** prefix added to the keys of events generated on secondary */
  private static final String secondaryPrefix = "secondary_";

  @Override
  public final void postSetUp() throws Exception {
    disconnectAllFromDS();
    final Host host = Host.getHost(0);
    primary = host.getVM(0);
    secondary = host.getVM(1);
    client1 = host.getVM(2);
    PORT1 = primary.invoke(StatsBugDUnitTest::createServerCache);
    PORT2 = secondary.invoke(StatsBugDUnitTest::createServerCache);
  }

  /**
   * Create the cache
   *
   * @param props - properties for DS
   * @return the cache instance
   * @throws Exception - thrown if any problem occurs in cache creation
   */
  private Cache createCache(Properties props) throws Exception {
    DistributedSystem ds = getSystem(props);
    ds.disconnect();
    ds = getSystem(props);
    Cache cache = null;
    cache = CacheFactory.create(ds);
    if (cache == null) {
      throw new Exception("CacheFactory.create() returned null ");
    }
    return cache;
  }

  @Override
  public final void preTearDown() throws Exception {
    // close client
    client1.invoke(StatsBugDUnitTest::closeCache);

    // close server
    primary.invoke(StatsBugDUnitTest::closeCache);
    secondary.invoke(StatsBugDUnitTest::closeCache);
  }

  /**
   * This test does the following:<br>
   * 1)Create and populate the client<br>
   * 2)Do some operations from the primary cache-server<br>
   * 3)Stop the primary cache-server<br>
   * 4)Wait some time to allow client to failover to secondary and do some operations from
   * secondary<br>
   * 5)Verify that the invalidates stats at the client accounts for the operations done by both,
   * primary and secondary.
   */
  @Test
  public void testBug36109() throws Exception {
    LogWriterUtils.getLogWriter().info("testBug36109 : BEGIN");
    client1.invoke(() -> StatsBugDUnitTest.createClientCacheForInvalidates(
        NetworkUtils.getServerHostName(Host.getHost(0)), PORT1, PORT2));
    client1.invoke(StatsBugDUnitTest::prepopulateClient);
    primary.invoke(() -> StatsBugDUnitTest.doEntryOperations(primaryPrefix));
    Wait.pause(3000);
    primary.invoke(StatsBugDUnitTest::stopServer);
    try {
      Thread.sleep(5000);
    } catch (InterruptedException ignore) {
      fail("interrupted");
    }

    secondary.invoke(() -> StatsBugDUnitTest.doEntryOperations(secondaryPrefix));
    try {
      Thread.sleep(5000);
    } catch (InterruptedException ignore) {
      fail("interrupted");
    }

    client1.invoke(StatsBugDUnitTest::verifyNumInvalidates);
    LogWriterUtils.getLogWriter().info("testBug36109 : END");
  }

  /**
   * Creates and starts the cache-server
   *
   * @return - the port on which cache-server is running
   * @throws Exception - thrown if any problem occurs in cache/server creation
   */
  public static Integer createServerCache() throws Exception {
    StatsBugDUnitTest test = new StatsBugDUnitTest();
    Properties props = new Properties();
    cache = test.createCache(props);
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    factory.setDataPolicy(DataPolicy.REPLICATE);

    RegionAttributes attrs = factory.create();

    cache.createRegion(REGION_NAME, attrs);
    CacheServer server = cache.addCacheServer();
    int port = getRandomAvailableTCPPort();
    server.setPort(port);
    server.setNotifyBySubscription(false);
    server.setSocketBufferSize(32768);
    server.start();
    LogWriterUtils.getLogWriter().info("Server started at PORT = " + port);
    return port;
  }

  /**
   * Initializes the cache client
   *
   * @param port1 - port for the primary cache-server
   * @param port2 for the secondary cache-server
   * @throws Exception-thrown if any problem occurs in initializing the client
   */
  public static void createClientCache(String host, Integer port1, Integer port2) throws Exception {
    StatsBugDUnitTest test = new StatsBugDUnitTest();
    cache = test.createCache(createProperties1());
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    pool = (PoolImpl) ClientServerTestCase.configureConnectionPool(factory, host,
        new int[] {port1, port2}, true, -1, 3, null);
    RegionAttributes attrs = factory.create();
    Region region = cache.createRegion(REGION_NAME, attrs);
    region.registerInterest("ALL_KEYS");
    LogWriterUtils.getLogWriter().info("Client cache created");
  }

  /**
   * Initializes the cache client
   *
   * @param port1 - port for the primary cache-server
   * @param port2 for the secondary cache-server
   * @throws Exception-thrown if any problem occurs in initializing the client
   */
  public static void createClientCacheForInvalidates(String host, Integer port1, Integer port2)
      throws Exception {
    StatsBugDUnitTest test = new StatsBugDUnitTest();
    cache = test.createCache(createProperties1());
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.DISTRIBUTED_ACK);
    pool = (PoolImpl) ClientServerTestCase.configureConnectionPool(factory, host,
        new int[] {port1, port2}, true, -1, 3, null);
    RegionAttributes attrs = factory.create();
    Region region = cache.createRegion(REGION_NAME, attrs);
    region.registerInterest("ALL_KEYS", false, false);
    LogWriterUtils.getLogWriter().info("Client cache created");
  }

  /**
   * Verify that the invalidates stats at the client accounts for the operations done by both,
   * primary and secondary.
   *
   */
  public static void verifyNumInvalidates() {
    long invalidatesRecordedByStats = pool.getInvalidateCount();
    LogWriterUtils.getLogWriter()
        .info("invalidatesRecordedByStats = " + invalidatesRecordedByStats);

    int expectedInvalidates = TOTAL_SERVERS * PUTS_PER_SERVER;
    LogWriterUtils.getLogWriter().info("expectedInvalidates = " + expectedInvalidates);

    if (invalidatesRecordedByStats != expectedInvalidates) {
      fail("Invalidates received by client(" + invalidatesRecordedByStats
          + ") does not match with the number of operations(" + expectedInvalidates
          + ") done at server");
    }
  }

  /**
   * Stops the cache server
   *
   */
  public static void stopServer() {
    try {
      Iterator iter = cache.getCacheServers().iterator();
      if (iter.hasNext()) {
        CacheServer server = (CacheServer) iter.next();
        server.stop();
      }
    } catch (Exception e) {
      fail("failed while stopServer()" + e);
    }
  }

  /**
   * create properties for a loner VM
   */
  private static Properties createProperties1() {
    Properties props = new Properties();
    props.setProperty(MCAST_PORT, "0");
    props.setProperty(LOCATORS, "");
    return props;
  }


  /**
   * Do PUT operations
   *
   * @param keyPrefix - string prefix for the keys for all the entries do be done
   * @throws Exception - thrown if any exception occurs in doing PUTs
   */
  public static void doEntryOperations(String keyPrefix) throws Exception {
    Region r1 = cache.getRegion(SEPARATOR + REGION_NAME);
    for (int i = 0; i < PUTS_PER_SERVER; i++) {
      r1.put(keyPrefix + i, keyPrefix + "val-" + i);
    }
  }

  /**
   * Prepopulate the client with the entries that will be done by cache-servers
   *
   */
  public static void prepopulateClient() throws Exception {
    doEntryOperations(primaryPrefix);
    doEntryOperations(secondaryPrefix);
  }

  /**
   * Close the cache
   *
   */
  public static void closeCache() {
    if (cache != null && !cache.isClosed()) {
      cache.close();
      cache.getDistributedSystem().disconnect();
    }
  }
}
