/*
 * 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.metrics.function.executions;

import static java.io.File.pathSeparatorChar;
import static java.lang.Boolean.TRUE;
import static java.lang.String.valueOf;
import static java.nio.file.Files.createDirectories;
import static java.util.stream.Collectors.toList;
import static org.apache.geode.internal.AvailablePortHelper.getRandomAvailableTCPPorts;
import static org.apache.geode.test.compiler.ClassBuilder.writeJarFromClasses;
import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.metrics.MetricsPublishingService;
import org.apache.geode.metrics.SimpleMetricsPublishingService;
import org.apache.geode.rules.ServiceJarRule;
import org.apache.geode.test.junit.rules.FolderRule;
import org.apache.geode.test.junit.rules.gfsh.GfshExecution;
import org.apache.geode.test.junit.rules.gfsh.GfshRule;
import org.apache.geode.test.junit.rules.gfsh.GfshScript;

/**
 * Acceptance tests for function executions timer on a loner server with no locator
 */
public class FunctionExecutionsTimerLonerTest {

  private int serverPort;
  private int jmxRmiPort;
  private Path serverFolder;
  private Path serviceJarPath;
  private Path functionHelpersJarPath;
  private ClientCache clientCache;
  private Pool serverPool;
  private String connectCommand;
  private String startServerCommandWithStatsEnabled;
  private String startServerCommandWithStatsDisabled;
  private String startServerCommandWithTimeStatsDisabled;
  private String stopServerCommand;
  private Path rootFolder;

  @Rule(order = 0)
  public FolderRule folderRule = new FolderRule();
  @Rule(order = 1)
  public GfshRule gfshRule = new GfshRule(folderRule::getFolder);
  @Rule
  public ServiceJarRule serviceJarRule = new ServiceJarRule();

  @Before
  public void setUp() throws IOException {
    rootFolder = folderRule.getFolder().toPath().toAbsolutePath();

    int[] ports = getRandomAvailableTCPPorts(2);

    serverPort = ports[0];
    jmxRmiPort = ports[1];

    serverFolder = createDirectories(rootFolder.resolve("server"));

    serviceJarPath = serviceJarRule.createJarFor("services.jar",
        MetricsPublishingService.class, SimpleMetricsPublishingService.class);

    functionHelpersJarPath = rootFolder.resolve("function-helpers.jar");
    writeJarFromClasses(functionHelpersJarPath.toFile(), FunctionToTimeWithResult.class,
        GetFunctionExecutionTimerValues.class, ExecutionsTimerValues.class, ThreadSleep.class);

    startServerCommandWithStatsEnabled = startServerCommand(false, true);
    startServerCommandWithStatsDisabled = startServerCommand(true, true);
    startServerCommandWithTimeStatsDisabled = startServerCommand(false, false);

    stopServerCommand = "stop server --dir=" + serverFolder;
    connectCommand = "connect --jmx-manager=localhost[" + jmxRmiPort + "]";
  }

  @After
  public void tearDown() {
    stopServer();
  }

  @Test
  public void timersNotRegisteredIfFunctionDeployedButNotExecuted() {
    startServerWithStatsEnabled();

    deployFunction(FunctionToTimeWithResult.class);

    assertThat(getExecutionsTimerValuesFor(FunctionToTimeWithResult.ID))
        .as("Function executions timers on server")
        .isEmpty();
  }

  @Test
  public void timersRegisteredIfFunctionDeployedAndThenExecuted() {
    startServerWithStatsEnabled();
    deployFunction(FunctionToTimeWithResult.class);

    Duration functionDuration = Duration.ofSeconds(1);
    executeFunctionById(FunctionToTimeWithResult.ID, functionDuration);

    assertThat(getExecutionsTimerValuesFor(FunctionToTimeWithResult.ID))
        .as("Function executions timers on server")
        .hasSize(2);
  }

  @Test
  public void timersRegisteredIfStatsDisabled() {
    startServerWithStatsDisabled();

    executeFunctionThatSucceeds(new FunctionToTimeWithResult(), Duration.ofMillis(1));

    assertThat(getExecutionsTimerValuesFor(FunctionToTimeWithResult.ID))
        .as("Function executions timers on server")
        .hasSize(2);
  }

  @Test
  public void timersUnregisteredIfServerRestarts() {
    startServerWithStatsEnabled();
    executeFunctionThatSucceeds(new FunctionToTimeWithResult(), Duration.ofMillis(1));

    restartServer();

    assertThat(getExecutionsTimerValuesFor(FunctionToTimeWithResult.ID))
        .as("Function executions timers on server")
        .isEmpty();
  }

  @Test
  public void successTimerRecordsCountAndTotalTimeIfFunctionSucceeds() {
    startServerWithStatsEnabled();
    FunctionToTimeWithResult function = new FunctionToTimeWithResult();
    Duration functionDuration = Duration.ofSeconds(1);
    executeFunctionThatSucceeds(function, functionDuration);

    ExecutionsTimerValues successTimerValues = getSuccessTimerValues(function.getId());

    assertThat(successTimerValues.count)
        .as("Successful function executions count")
        .isEqualTo(1);

    assertThat(successTimerValues.totalTime)
        .as("Successful function executions total time")
        .isGreaterThan(functionDuration.toNanos());
  }

  @Test
  public void successTimerRecordsCountAndTotalTimeIfTimeStatsDisabled() {
    startServerWithTimeStatsDisabled();
    FunctionToTimeWithResult function = new FunctionToTimeWithResult();
    Duration functionDuration = Duration.ofSeconds(1);
    executeFunctionThatSucceeds(function, functionDuration);

    ExecutionsTimerValues successTimerValues = getSuccessTimerValues(function.getId());

    assertThat(successTimerValues.count)
        .as("Successful function executions count")
        .isEqualTo(1);

    assertThat(successTimerValues.totalTime)
        .as("Successful function executions total time")
        .isGreaterThan(functionDuration.toNanos());
  }

  @Test
  public void failureTimerRecordsCountAndTotalTimeIfFunctionThrows() {
    startServerWithStatsEnabled();
    FunctionToTimeWithResult function = new FunctionToTimeWithResult();
    Duration functionDuration = Duration.ofSeconds(1);
    executeFunctionThatThrows(function, functionDuration);

    ExecutionsTimerValues failureTimerValues = getFailureTimerValues(function.getId());

    assertThat(failureTimerValues.count)
        .as("Failed function executions count")
        .isEqualTo(1);

    assertThat(failureTimerValues.totalTime)
        .as("Failed function executions total time")
        .isGreaterThan(functionDuration.toNanos());
  }

  private String startServerCommand(boolean statsDisabled, boolean enableTimeStatistics) {
    return String.join(" ",
        "start server",
        "--name=server",
        "--dir=" + serverFolder,
        "--server-port=" + serverPort,
        "--classpath=" + serviceJarPath + pathSeparatorChar + functionHelpersJarPath,
        "--enable-time-statistics=" + enableTimeStatistics,
        "--http-service-port=0",
        "--J=-Dgemfire.enable-cluster-config=true",
        "--J=-Dgemfire.jmx-manager=true",
        "--J=-Dgemfire.jmx-manager-start=true",
        "--J=-Dgemfire.jmx-manager-port=" + jmxRmiPort,
        "--J=-Dgemfire.statsDisabled=" + statsDisabled);
  }

  private void createClientAndPool() {
    clientCache = new ClientCacheFactory()
        .addPoolServer("localhost", serverPort)
        .create();

    serverPool = PoolManager.createFactory()
        .addServer("localhost", serverPort)
        .create("server-pool");
  }

  private void closeClientAndPool() {
    if (serverPool != null) {
      serverPool.destroy();
    }

    if (clientCache != null) {
      clientCache.close();
    }
  }

  private void stopServer() {
    closeClientAndPool();
    GfshExecution execution = GfshScript.of(stopServerCommand).execute(gfshRule);
    execution.serverStopper().awaitStop(serverFolder);
  }

  private void startServerWithStatsEnabled() {
    startServer(startServerCommandWithStatsEnabled);
  }

  private void startServerWithStatsDisabled() {
    startServer(startServerCommandWithStatsDisabled);
  }

  private void startServerWithTimeStatsDisabled() {
    startServer(startServerCommandWithTimeStatsDisabled);
  }

  private void startServer(String command) {
    gfshRule.execute(command);
    createClientAndPool();
  }

  private void restartServer() {
    stopServer();
    startServerWithStatsEnabled();
  }

  @SuppressWarnings("SameParameterValue")
  private <T> void deployFunction(Class<? extends Function<T>> functionClass) {
    Path functionJarPath = rootFolder.resolve(functionClass.getSimpleName() + ".jar");

    Throwable thrown =
        catchThrowable(() -> writeJarFromClasses(functionJarPath.toFile(), functionClass));

    assertThat(thrown)
        .as("Exception from writing function JAR")
        .isNull();

    String deployFunctionCommand = "deploy --jar=" + functionJarPath;
    gfshRule.execute(connectCommand, deployFunctionCommand);
  }

  private void executeFunctionThatSucceeds(Function<? super String[]> function, Duration duration) {
    Throwable thrown = catchThrowable(() -> executeFunction(function, duration, true));

    assertThat(thrown)
        .as("Exception from function expected to succeed")
        .isNull();
  }

  private void executeFunctionThatThrows(Function<? super String[]> function, Duration duration) {
    Throwable thrown = catchThrowable(() -> executeFunction(function, duration, false));

    assertThat(thrown)
        .withFailMessage("Expected function to throw but it did not")
        .isNotNull();
  }

  private void executeFunction(Function<? super String[]> function, Duration duration,
      boolean successful) {
    Execution<String[], Object, List<Object>> execution =
        uncheckedCast(FunctionService.onServer(serverPool));

    execution
        .setArguments(new String[] {valueOf(duration.toMillis()), valueOf(successful)})
        .execute(function)
        .getResult();
  }

  @SuppressWarnings("SameParameterValue")
  private void executeFunctionById(String functionId, Duration duration) {
    Execution<String[], Object, List<Object>> execution =
        uncheckedCast(FunctionService.onServer(serverPool));

    Throwable thrown = catchThrowable(() -> execution
        .setArguments(new String[] {valueOf(duration.toMillis()), TRUE.toString()})
        .execute(functionId)
        .getResult());

    assertThat(thrown)
        .as("Exception from function expected to succeed")
        .isNull();
  }

  private ExecutionsTimerValues getSuccessTimerValues(String functionId) {
    return getExecutionsTimerValues(functionId, true);
  }

  private ExecutionsTimerValues getFailureTimerValues(String functionId) {
    return getExecutionsTimerValues(functionId, false);
  }

  private ExecutionsTimerValues getExecutionsTimerValues(String functionId,
      boolean succeededTagValue) {
    List<ExecutionsTimerValues> executionsTimerValues = getExecutionsTimerValuesFor(functionId)
        .stream()
        .filter(v -> v.succeeded == succeededTagValue)
        .collect(toList());

    assertThat(executionsTimerValues)
        .hasSize(1);

    return executionsTimerValues.get(0);
  }

  private List<ExecutionsTimerValues> getExecutionsTimerValuesFor(String functionId) {
    Execution<Void, List<ExecutionsTimerValues>, List<List<ExecutionsTimerValues>>> functionExecution =
        uncheckedCast(FunctionService.onServer(serverPool));

    List<List<ExecutionsTimerValues>> results = functionExecution
        .execute(new GetFunctionExecutionTimerValues())
        .getResult();

    assertThat(results)
        .hasSize(1);

    return results.get(0).stream()
        .filter(v -> v.functionId.equals(functionId))
        .collect(toList());
  }
}
