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

import static org.apache.geode.distributed.ConfigurationProperties.LOCATORS;
import static org.apache.geode.distributed.ConfigurationProperties.MCAST_PORT;
import static org.apache.geode.distributed.ConfigurationProperties.SERIALIZABLE_OBJECT_FILTER;
import static org.apache.geode.test.dunit.DistributedTestUtils.getLocatorPort;
import static org.apache.geode.test.dunit.Invoke.invokeInEveryVM;
import static org.apache.geode.test.dunit.VM.getVM;
import static org.apache.geode.util.internal.GeodeGlossary.GEMFIRE_PREFIX;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

import junitparams.Parameters;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.internal.InternalClientCache;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionContext;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.cache.server.CacheServer;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.cache.InternalCacheServer;
import org.apache.geode.internal.cache.tier.ServerSideHandshake;
import org.apache.geode.internal.cache.tier.sockets.AcceptorImpl;
import org.apache.geode.internal.cache.tier.sockets.ServerConnection;
import org.apache.geode.test.dunit.VM;
import org.apache.geode.test.dunit.rules.DistributedRestoreSystemProperties;
import org.apache.geode.test.dunit.rules.DistributedRule;
import org.apache.geode.test.junit.categories.FunctionServiceTest;
import org.apache.geode.test.junit.runners.GeodeParamsRunner;

/**
 * Server should terminate client function execution when it times out. Client sends
 * CLIENT_FUNCTION_TIMEOUT to server when default is overridden by client.
 *
 * <p>
 * TRAC #51193: The function execution connection on the server is never terminated even if the
 * gemfire.CLIENT_FUNCTION_TIMEOUT property is set
 */
@Category(FunctionServiceTest.class)
@RunWith(GeodeParamsRunner.class)
@SuppressWarnings("serial")
public class ClientFunctionTimeoutRegressionTest implements Serializable {

  private static final int TOTAL_NUM_BUCKETS = 4;
  private static final int REDUNDANT_COPIES = 1;

  private static InternalCache serverCache;
  private static InternalClientCache clientCache;

  private String regionName;

  private VM server;
  private VM client;

  @Rule
  public DistributedRule distributedRule = new DistributedRule();

  @Rule
  public DistributedRestoreSystemProperties restoreSystemProperties =
      new DistributedRestoreSystemProperties();

  @Before
  public void setUp() throws Exception {
    server = getVM(0);
    client = getVM(1);

    regionName = getClass().getSimpleName();
  }

  @After
  public void tearDown() throws Exception {
    invokeInEveryVM(() -> {
      if (clientCache != null) {
        clientCache.close();
      }
      if (serverCache != null) {
        serverCache.close();
      }
      clientCache = null;
      serverCache = null;
    });

    // This test modifies system properties, which are statically cached
    client.bounce();
  }

  @Test
  @Parameters({"SERVER,REPLICATE,0,0", "SERVER,REPLICATE,6000,0", "REGION,REPLICATE,0,0",
      "REGION,REPLICATE,6000,0", "REGION,PARTITION,0,0", "REGION,PARTITION,6000,0",
      "SERVER,REPLICATE,0,4000", "SERVER,REPLICATE,6000,4000",
      "REGION,REPLICATE,0,4000", "REGION,REPLICATE,6000,4000",
      "REGION,PARTITION,0,4000", "REGION,PARTITION,6000,4000",
  })
  public void executeFunctionUsesClientTimeoutOnServer(final ExecutionTarget executionTarget,
      final RegionType regionType, final int propertyTimeout, final int executeTimeout) {
    int port = server.invoke(() -> createServerCache(regionType));
    client.invoke(() -> createClientCache(client.getHost().getHostName(), port, propertyTimeout));

    client.invoke(() -> executeFunctionToVerifyClientTimeoutOnServer(executionTarget,
        propertyTimeout, executeTimeout));
  }

  private void createClientCache(final String hostName, final int port, final int timeout) {
    if (timeout > 0) {
      System.setProperty(GEMFIRE_PREFIX + "CLIENT_FUNCTION_TIMEOUT", String.valueOf(timeout));
    }

    Properties config = new Properties();
    config.setProperty(LOCATORS, "");
    config.setProperty(MCAST_PORT, "0");

    ClientCacheFactory clientCacheFactory = new ClientCacheFactory(config);
    clientCacheFactory.addPoolServer(hostName, port);

    clientCache = (InternalClientCache) clientCacheFactory.create();

    ClientRegionFactory<String, String> clientRegionFactory =
        clientCache.createClientRegionFactory(ClientRegionShortcut.CACHING_PROXY);

    clientRegionFactory.create(regionName);
  }

  private int createServerCache(final RegionType regionType) throws IOException {
    assertThat(regionType).isNotNull();

    Properties config = new Properties();
    config.setProperty(LOCATORS, "localhost[" + getLocatorPort() + "]");
    config.setProperty(SERIALIZABLE_OBJECT_FILTER,
        "org.apache.geode.internal.cache.execute.ClientFunctionTimeoutRegressionTest*");

    serverCache = (InternalCache) new CacheFactory(config).create();

    RegionFactory<String, String> regionFactory;

    if (regionType == RegionType.PARTITION) {
      PartitionAttributesFactory<String, String> paf = new PartitionAttributesFactory<>();
      paf.setRedundantCopies(REDUNDANT_COPIES);
      paf.setTotalNumBuckets(TOTAL_NUM_BUCKETS);

      regionFactory = serverCache.createRegionFactory(RegionShortcut.PARTITION);
      regionFactory.setPartitionAttributes(paf.create());

    } else {
      regionFactory = serverCache.createRegionFactory(RegionShortcut.REPLICATE);
    }

    regionFactory.create(regionName);

    CacheServer server = serverCache.addCacheServer();
    server.setPort(0);
    server.start();
    return server.getPort();
  }

  private void executeFunctionToVerifyClientTimeoutOnServer(
      final ExecutionTarget functionServiceTarget, final int propertyTimeout,
      final int executeTimeout) {
    assertThat(functionServiceTarget).isNotNull();

    int timeout;
    if (executeTimeout > 0) {
      timeout = executeTimeout;
    } else {
      timeout = propertyTimeout;
    }

    Function<Integer> function = new CheckClientReadTimeout();
    FunctionService.registerFunction(function);
    Execution<Integer, Boolean, List<Boolean>> execution = null;

    if (functionServiceTarget == ExecutionTarget.REGION) {
      execution =
          FunctionService.onRegion(clientCache.getRegion(regionName)).setArguments(timeout);
    } else {
      execution = FunctionService.onServer(clientCache.getDefaultPool()).setArguments(timeout);
    }

    ResultCollector<Boolean, List<Boolean>> resultCollector;
    if (executeTimeout > 0) {
      resultCollector = execution.execute(function, timeout, TimeUnit.MILLISECONDS);
    } else {
      resultCollector = execution.execute(function);
    }

    String description = "Server did not read client_function_timeout from client.";
    assertThat(resultCollector.getResult().get(0)).as(description).isTrue();
  }

  private enum RegionType {
    PARTITION, REPLICATE
  }

  private enum ExecutionTarget {
    REGION, SERVER
  }

  /**
   * Input: client function timeout <br>
   * Output: true if server has client timeout equal to the input
   */
  private static class CheckClientReadTimeout implements Function<Integer> {

    public CheckClientReadTimeout() {
      // nothing
    }

    @Override
    public void execute(FunctionContext<Integer> context) {
      boolean timeoutMatches = false;
      int expected = context.getArguments();

      InternalCacheServer cacheServer =
          (InternalCacheServer) context.getCache().getCacheServers().get(0);
      AcceptorImpl acceptor = (AcceptorImpl) cacheServer.getAcceptor();
      ServerConnection[] scs = acceptor.getAllServerConnectionList();

      for (ServerConnection sc : scs) {
        ServerSideHandshake hs = sc.getHandshake();
        if (hs != null && expected == hs.getClientReadTimeout()) {
          timeoutMatches = true;
        }
      }

      context.getResultSender().lastResult(timeoutMatches);
    }

    @Override
    public String getId() {
      return CheckClientReadTimeout.class.getName();
    }

    @Override
    public boolean isHA() {
      return false;
    }
  }
}
