/*
 * 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.test.dunit.Assert.*;

import java.io.IOException;
import java.util.Iterator;
import java.util.Properties;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.LoaderHelper;
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.PoolFactory;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.internal.AvailablePortHelper;
import org.apache.geode.test.dunit.DistributedTestUtils;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.Wait;
import org.apache.geode.test.dunit.WaitCriterion;
import org.apache.geode.test.dunit.cache.internal.JUnit4CacheTestCase;

/**
 * Provides helper methods for testing clients and servers. This test case was created by
 * refactoring methods from ConnectionPoolDUnitTest into this class.
 *
 * @since GemFire 4.2.1
 */
public abstract class ClientServerTestCase extends JUnit4CacheTestCase {

  public static String NON_EXISTENT_KEY = "NON_EXISTENT_KEY";

  public static boolean AUTO_LOAD_BALANCE = false;

  @Override
  public final void postSetUp() throws Exception {
    // this makes sure we don't have any connection left over from previous tests
    disconnectAllFromDS();
    postSetUpClientServerTestCase();
  }

  protected void postSetUpClientServerTestCase() throws Exception {}

  @Override
  public final void preTearDownCacheTestCase() throws Exception {
    preTearDownClientServerTestCase();
    // this makes sure we don't leave anything for the next tests
    disconnectAllFromDS();
  }

  protected void preTearDownClientServerTestCase() throws Exception {}

  /**
   * Starts a bridge server on the given port
   *
   * @since GemFire 4.0
   */
  public int startBridgeServer(int port) throws IOException {

    Cache cache = getCache();
    CacheServer bridge = cache.addCacheServer();
    bridge.setPort(port);
    bridge.setMaxThreads(getMaxThreads());
    bridge.start();
    return bridge.getPort();
  }

  /**
   * Defaults to 0 which means no selector in server. Subclasses can override setting this to a
   * value > 0 to enable selector.
   */
  protected int getMaxThreads() {
    return 0;
  }

  /**
   * Stops the bridge server that serves up the given cache.
   *
   * @since GemFire 4.0
   */
  public void stopBridgeServers(Cache cache) {
    CacheServer bridge = null;
    for (Iterator bsI = cache.getCacheServers().iterator(); bsI.hasNext();) {
      bridge = (CacheServer) bsI.next();
      bridge.stop();
      assertFalse(bridge.isRunning());
    }
  }

  /**
   * Returns region attributes for a <code>LOCAL</code> region
   */
  protected RegionAttributes getRegionAttributes() {
    AttributesFactory factory = new AttributesFactory();
    factory.setScope(Scope.LOCAL);
    return factory.create();
  }

  public static String createBridgeClientConnection(String host, int[] ports) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < ports.length; i++) {
      if (i > 0) {
        sb.append(",");
      }
      sb.append("name" + i + "=");
      sb.append(host + ":" + ports[i]);
    }
    return sb.toString();
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int port1,
      int port2, boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      int pingInterval, int idleTimeout, boolean threadLocalCnxs, int lifetimeTimeout) {
    int[] ports;
    if (port2 != -1) {
      ports = new int[] {port1, port2};
    } else {
      ports = new int[] {port1};
    }
    return configureConnectionPool(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, pingInterval, idleTimeout, threadLocalCnxs,
        lifetimeTimeout);
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int port1,
      int port2, boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      int pingInterval, int idleTimeout, boolean threadLocalCnxs) {
    return configureConnectionPool(factory, host, port1, port2, establish, redundancy,
        connectionsPerServer, serverGroup, pingInterval, idleTimeout, threadLocalCnxs,
        -2/* lifetimeTimeout */);
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int port1,
      int port2, boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      int pingInterval) {
    return configureConnectionPool(factory, host, port1, port2, establish, redundancy,
        connectionsPerServer, serverGroup, pingInterval, -1, false);
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int port1,
      int port2, boolean establish, int redundancy, int connectionsPerServer, String serverGroup) {
    return configureConnectionPool(factory, host, port1, port2, establish, redundancy,
        connectionsPerServer, serverGroup, -1/* pingInterval */);
  }

  public static Pool configureConnectionPoolWithName(AttributesFactory factory, String host,
      int[] ports, boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      String poolName) {
    return configureConnectionPoolWithNameAndFactory(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, poolName, PoolManager.createFactory(), -1, -1, false, -2,
        -1);
  }

  public static Pool configureConnectionPoolWithName(AttributesFactory factory, String host,
      int[] ports, boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      String poolName, int pingInterval, int idleTimeout, boolean threadLocalCnxs,
      int lifetimeTimeout, int statisticInterval) {
    return configureConnectionPoolWithNameAndFactory(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, poolName, PoolManager.createFactory(), pingInterval,
        idleTimeout, threadLocalCnxs, lifetimeTimeout, statisticInterval);
  }

  public static Pool configureConnectionPoolWithNameAndFactory(AttributesFactory factory,
      String host, int[] ports, boolean establish, int redundancy, int connectionsPerServer,
      String serverGroup, String poolName, PoolFactory pf) {
    return configureConnectionPoolWithNameAndFactory(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, poolName, pf, -1, -1, false, -2, -1);
  }

  /**
   * this method creates a client connection pool and configures it. If the ports array is not empty
   * it is used to configure the client pool. Otherwise the pool is configured to use the dunit
   * locator.
   */
  public static Pool configureConnectionPoolWithNameAndFactory(AttributesFactory factory,
      String host, int[] ports, boolean establish, int redundancy, int connectionsPerServer,
      String serverGroup, String poolName, PoolFactory pf, int pingInterval, int idleTimeout,
      boolean threadLocalCnxs, int lifetimeTimeout, int statisticInterval) {

    if (AUTO_LOAD_BALANCE || ports.length == 0) {
      pf.addLocator(host, DistributedTestUtils.getDUnitLocatorPort());
    } else {
      for (int z = 0; z < ports.length; z++) {
        pf.addServer(host, ports[z]);
      }
    }

    // TODO - probably should pass in minConnections rather than connecions per server
    if (connectionsPerServer != -1 && ports != null) {
      pf.setMinConnections(connectionsPerServer * ports.length);
    }
    if (threadLocalCnxs) {
      pf.setThreadLocalConnections(true);
    }
    if (pingInterval != -1) {
      pf.setPingInterval(pingInterval);
    }
    if (idleTimeout != -1) {
      pf.setIdleTimeout(idleTimeout);
    }
    if (statisticInterval != -1) {
      pf.setStatisticInterval(statisticInterval);
    }
    if (lifetimeTimeout != -2) {
      pf.setLoadConditioningInterval(lifetimeTimeout);
    }
    if (establish) {
      pf.setSubscriptionEnabled(true);
      pf.setSubscriptionRedundancy(redundancy);
      pf.setSubscriptionAckInterval(1);
    }
    if (serverGroup != null) {
      pf.setServerGroup(serverGroup);
    }
    String rpoolName = "testPool";
    if (poolName != null) {
      rpoolName = poolName;
    }
    Pool pool = pf.create(rpoolName);
    if (factory != null) {
      factory.setPoolName(rpoolName);
    }
    return pool;
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int[] ports,
      boolean establish, int redundancy, int connectionsPerServer, String serverGroup) {
    return configureConnectionPool(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, -1/* pingInterval */, -1/* idleTimeout */,
        false/* threadLocalCnxs */, -2/* lifetimeTimeout */);
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int[] ports,
      boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      int pingInterval, int idleTimeout, boolean threadLocalCnxs, int lifetimeTimeout) {
    return configureConnectionPoolWithName(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, null/* poolName */, pingInterval, idleTimeout,
        threadLocalCnxs, lifetimeTimeout, -1);
  }

  public static Pool configureConnectionPool(AttributesFactory factory, String host, int[] ports,
      boolean establish, int redundancy, int connectionsPerServer, String serverGroup,
      int pingInterval, int idleTimeout, boolean threadLocalCnxs, int lifetimeTimeout,
      int statisticInterval) {
    return configureConnectionPoolWithName(factory, host, ports, establish, redundancy,
        connectionsPerServer, serverGroup, null/* poolName */, pingInterval, idleTimeout,
        threadLocalCnxs, lifetimeTimeout, statisticInterval);
  }

  protected static DistributedMember getMemberId() {
    WaitCriterion w = new WaitCriterion() {

      public String description() {
        return "client never finished connecting: " + getSystemStatic().getMemberId();
      }

      public boolean done() {
        return getSystemStatic().getDistributedMember().getPort() > 0;
      }

    };
    int waitMillis = 20000;
    int interval = 100;
    boolean throwException = true;
    Wait.waitForCriterion(w, waitMillis, interval, throwException);
    return getSystemStatic().getDistributedMember();
  }

  public static class CacheServerCacheLoader extends TestCacheLoader implements Declarable {

    public CacheServerCacheLoader() {}

    @Override
    public Object load2(LoaderHelper helper) {
      if (helper.getArgument() instanceof Integer) {
        try {
          Thread.sleep(((Integer) helper.getArgument()).intValue());
        } catch (InterruptedException ugh) {
          fail("interrupted");
        }
      }
      Object ret = helper.getKey();

      if (ret instanceof String) {
        if (ret != null && ret.equals(NON_EXISTENT_KEY))
          return null;// return null
      }
      return ret;

    }

    public void init(Properties props) {}
  }

  public static final String BridgeServerKey = "BridgeServerKey";

  /**
   * Create a server that has a value for every key queried and a unique key/value in the specified
   * Region that uniquely identifies each instance.
   *
   * @param vm the VM on which to create the server
   * @param rName the name of the Region to create on the server
   * @param port the TCP port on which the server should listen
   */
  public void createBridgeServer(VM vm, final String rName, final int port) {
    vm.invoke(new CacheSerializableRunnable("Create Region on Server") {
      @Override
      public void run2() {
        try {
          AttributesFactory factory = new AttributesFactory();
          factory.setScope(Scope.DISTRIBUTED_ACK); // can't be local since used with
                                                   // registerInterest
          factory.setCacheLoader(new CacheServerCacheLoader());
          beginCacheXml();
          createRootRegion(rName, factory.create());
          startBridgeServer(port);
          finishCacheXml(rName + "-" + port);

          Region region = getRootRegion(rName);
          assertNotNull(region);
          region.put(BridgeServerKey, new Integer(port)); // A unique key/value to identify the
                                                          // BridgeServer
        } catch (Exception e) {
          getSystem().getLogWriter().severe(e);
          fail("Failed to start CacheServer " + e);
        }
      }
    });
  }

  public static int[] createUniquePorts(int numToCreate) {
    return AvailablePortHelper.getRandomAvailableTCPPorts(numToCreate);
  }

}
