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

import static java.lang.System.lineSeparator;
import static java.nio.charset.Charset.defaultCharset;
import static org.apache.commons.io.FileUtils.readLines;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntSupplier;

import org.junit.Assume;

/**
 * Container for a generic app server
 *
 * Extends {@link ServerContainer} to form a basic container which sets up a GenericAppServer
 * container. Currently being used solely for Jetty 12 containers.
 *
 * The container modifies a copy of the session testing war using the modify_war_file script in
 * order to properly implement geode session replication for generic application servers. That means
 * that tests using this container will only run on linux.
 *
 * In theory, adding support for additional containers should just be a matter of changing the
 * {@link GenericAppServerInstall} to support other installations, since this container does not
 * depend upon the type of appserver in any way.
 */
public class GenericAppServerContainer extends ServerContainer {
  private final File modifyWarScript;
  private final File modifyWarScriptLog;

  /**
   * Setup the generic appserver container
   *
   * Sets up a configuration for the container using the specified installation and configuration
   * home. Finds the script needed to modify the war file, sets up the new WAR file to modify by
   * creating a temporary WAR file to use, deploys the war to the Cargo container, and sets various
   * container properties (i.e. locator, local cache, etc.)
   */
  public GenericAppServerContainer(GenericAppServerInstall install, Path rootDir,
      Path containerConfigHome,
      String containerDescriptors, IntSupplier portSupplier) throws IOException {
    super(install, rootDir, containerConfigHome, containerDescriptors, portSupplier);

    // Set Jetty 12 EE version for Jakarta EE 10 compatibility
    // Jetty 12 requires the cargo.jetty.deployer.ee.version property to properly configure
    // the correct Jakarta EE environment modules (ee10-annotations, ee10-plus, ee10-jsp,
    // ee10-deploy)
    if (install
        .getGenericAppServerVersion() == GenericAppServerInstall.GenericAppServerVersion.JETTY12) {
      getConfiguration().setProperty("cargo.jetty.deployer.ee.version", "ee10");
    }

    // Setup modify war script file so that it is executable and easily findable
    modifyWarScript = new File(install.getModulePath() + "/bin/modify_war");
    modifyWarScript.setExecutable(true);

    // Setup modify_war script logging file
    modifyWarScriptLog = new File(cargoLogDir + "/warScript.log");
    modifyWarScriptLog.createNewFile();

    // Ignore tests that are running on windows, since they can't run the modify war script
    Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("win"));

    // Create temp war file to use
    Path defaultGenericAppserverWarDir = rootDir.resolve("cargo_wars");
    Files.createDirectories(defaultGenericAppserverWarDir);
    setWarFile(Files.createFile(defaultGenericAppserverWarDir.resolve(description + ".war")));

    // Deploy war file to container configuration
    deployWar();
    // Setup the default installations locators
    setLocator(install.getDefaultLocatorAddress(), install.getDefaultLocatorPort());

    // Make sure that local caches are disabled by default
    setCacheProperty("enable_local_cache",
        Boolean.toString(install.getConnectionType().enableLocalCache()));
  }

  /**
   * Builds the command needed to run the {@link GenericAppServerContainer#modifyWarScript}
   *
   * The command is built as an array list of strings with each element representing a string
   * separated by a space on the command line. For example, the list {'-t', 'geode'} would represent
   * 'modify_war -t geode' on the command line.
   *
   * The command built points towards {@link ContainerInstall#getWarFilePath()} as the starting file
   * and {@link ServerContainer#getWarFile()} as the output file. Cache and system properties are
   * specified when modifying the WAR, so the elements contained within the {@link #cacheProperties}
   * and {@link #systemProperties} maps are also added to the command built.
   */
  private List<String> buildCommand() throws IOException {
    ContainerInstall install = getInstall();

    // Start command list
    List<String> command = new ArrayList<>();
    // Path to the modify war script to run
    command.add(modifyWarScript.getAbsolutePath());
    // Path to the WAR file to modify
    command.add("-w");
    command.add(install.getWarFilePath().toString());
    // Get connection type for the WAR (peer-to-peer or client-server)
    command.add("-t");
    command.add(install.getConnectionType().getName());
    // Path to the modified version of the origin WAR file
    command.add("-o");
    command.add(getWarFile().toString());
    // Add all the cache properties setup to the WAR file
    for (String property : cacheProperties.keySet()) {
      command.add("-p");
      command.add("gemfire.cache." + property + "=" + getCacheProperty(property));
    }
    // Add all the system properties to the WAR file
    for (String property : systemProperties.keySet()) {
      command.add("-p");
      command.add("gemfire.property." + property + "=" + getSystemProperty(property));
    }

    return command;
  }

  /**
   * Modifies the {@link ContainerInstall#getWarFilePath()} for container use, by simulating a
   * command line execution of the modify_war_file script using the commands built from
   * {@link #buildCommand()}
   *
   * The modified WAR file is sent to {@link #getWarFile()}.
   *
   * @throws IOException If the command executed returns with a non-zero exit code.
   */
  private void modifyWarFile() throws IOException, InterruptedException {
    // Build the environment to run the command
    ProcessBuilder builder = new ProcessBuilder();
    builder.environment().put("GEODE", ContainerInstall.GEODE_HOME_PATH.toString());
    builder.inheritIO();
    // Setup the environment builder with the command
    builder.command(buildCommand());
    // Redirect the command line logging to a file

    builder.redirectError(modifyWarScriptLog);
    builder.redirectOutput(modifyWarScriptLog);
    logger.info("Running command: " + String.join(" ", builder.command()));

    // Run the command
    Process process = builder.start();

    // Wait for the command to finish
    int exitCode = process.waitFor();
    // Throw error if bad exit
    if (exitCode != 0) {
      StringBuilder sb = new StringBuilder();
      sb.append("Unable to run modify_war script, command: ").append(builder.command());
      sb.append(lineSeparator());
      sb.append("check log file: ");
      for (String line : readLines(modifyWarScriptLog, defaultCharset())) {
        sb.append(lineSeparator());
        sb.append(line);
      }
      throw new IOException(sb.toString());
    }
  }

  /**
   * Update the container's settings by calling {@link #modifyWarFile()} method
   */
  @Override
  public void writeSettings() throws Exception {
    modifyWarFile();
  }
}
