/*
 * 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.test.junit.rules.gfsh;

import static org.apache.commons.lang.SystemUtils.PATH_SEPARATOR;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.junit.rules.ExternalResource;
import org.junit.rules.TemporaryFolder;

import org.apache.geode.test.junit.rules.RequiresGeodeHome;

/**
 * The {@code GfshRule} allows a test to execute Gfsh commands via the actual (fully-assembled) gfsh
 * binaries. Each call to {@link GfshRule#execute(GfshScript)} will invoke the given gfsh script in
 * a forked JVM. The {@link GfshRule#after()} method will attempt to clean up all forked JVMs.
 */
public class GfshRule extends ExternalResource {

  private static final String DOUBLE_QUOTE = "\"";

  private TemporaryFolder temporaryFolder = new TemporaryFolder();
  private List<GfshExecution> gfshExecutions;
  private Path gfsh;

  @Override
  protected void before() throws IOException {
    gfsh = new RequiresGeodeHome().getGeodeHome().toPath().resolve("bin/gfsh");
    assertThat(gfsh).exists();

    gfshExecutions = new ArrayList<>();
    temporaryFolder.create();
  }

  /**
   * Attempts to stop any started servers/locators via pid file and tears down any remaining gfsh
   * JVMs.
   */
  @Override
  protected void after() {
    gfshExecutions.stream().collect(Collectors.toList()).forEach(this::stopMembersQuietly);

    gfshExecutions.stream().map(GfshExecution::getProcess).map(Process::destroyForcibly)
        .forEach((Process process) -> {
          try {
            // Process.destroyForcibly() may not terminate immediately
            process.waitFor(1, TimeUnit.MINUTES);
          } catch (InterruptedException ignore) {
            // We ignore this exception so that we still attempt the rest of the cleanup.
          }
        });

    temporaryFolder.delete();
  }

  public TemporaryFolder getTemporaryFolder() {
    return temporaryFolder;
  }

  public Path getGfshPath() {
    return gfsh;
  }

  public GfshExecution execute(String... commands) {
    return execute(GfshScript.of(commands));
  }

  public GfshExecution execute(GfshScript gfshScript) {
    GfshExecution gfshExecution;
    try {
      File workingDir = new File(temporaryFolder.getRoot(), gfshScript.getName());
      workingDir.mkdirs();
      Process process = toProcessBuilder(gfshScript, gfsh, workingDir).start();
      gfshExecution = new GfshExecution(process, workingDir);
      gfshExecutions.add(gfshExecution);
      gfshScript.awaitIfNecessary(gfshExecution);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }

    return gfshExecution;
  }

  protected ProcessBuilder toProcessBuilder(GfshScript gfshScript, Path gfshPath, File workingDir) {
    List<String> commandsToExecute = new ArrayList<>();
    commandsToExecute.add(gfshPath.toAbsolutePath().toString());

    for (String command : gfshScript.getCommands()) {
      commandsToExecute.add("-e " + command);
    }

    ProcessBuilder processBuilder = new ProcessBuilder(commandsToExecute);
    processBuilder.directory(workingDir);

    List<String> extendedClasspath = gfshScript.getExtendedClasspath();
    if (!extendedClasspath.isEmpty()) {
      Map<String, String> environmentMap = processBuilder.environment();
      String classpathKey = "CLASSPATH";
      String existingJavaArgs = environmentMap.get(classpathKey);
      String specified = String.join(PATH_SEPARATOR, extendedClasspath);
      String newValue =
          String.format("%s%s", existingJavaArgs == null ? "" : existingJavaArgs + ":", specified);
      environmentMap.put(classpathKey, newValue);
    }

    return processBuilder;
  }

  private void stopMembersQuietly(GfshExecution gfshExecution) {
    gfshExecution.getServerDirs().forEach(this::stopServerInDir);
    gfshExecution.getLocatorDirs().forEach(this::stopLocatorInDir);
  }

  private void stopServerInDir(File dir) {
    String stopServerCommand = "stop server --dir=" + quoteArgument(dir.toString());

    GfshScript stopServerScript =
        new GfshScript(stopServerCommand).withName("teardown-stop-server").awaitQuietly();
    execute(stopServerScript);
  }

  private void stopLocatorInDir(File dir) {
    String stopLocatorCommand = "stop locator --dir=" + quoteArgument(dir.toString());

    GfshScript stopServerScript =
        new GfshScript(stopLocatorCommand).withName("teardown-stop-locator").awaitQuietly();
    execute(stopServerScript);
  }

  private String quoteArgument(String argument) {
    if (!argument.startsWith(DOUBLE_QUOTE)) {
      argument = DOUBLE_QUOTE + argument;
    }

    if (!argument.endsWith(DOUBLE_QUOTE)) {
      argument = argument + DOUBLE_QUOTE;
    }

    return argument;
  }
}

@@ -1765,16 +1765,101 @@
  = %22%5C%22%22;
+%0A  private static final String LINE_SEPARATOR = System.getProperty(%22line.separator%22);
 %0A%0A  priv
@@ -2482,24 +2482,99 @@
 sQuietly);%0A%0A
+    final List%3CString%3E shutdownExceptions = new ArrayList%3C%3E();%0A    try %7B%0A  
     gfshExec
@@ -2658,16 +2658,18 @@
         
+  
 .forEach
@@ -2698,30 +2698,34 @@
 %7B%0A          
+  
 try %7B%0A
+  
             
@@ -2791,24 +2791,26 @@
             
+  
 process.wait
@@ -2841,24 +2841,26 @@
 ;%0A          
+  
 %7D catch (Int
@@ -2878,20 +2878,16 @@
 eption i
-gnor
 e) %7B%0A   
@@ -2899,111 +2899,376 @@
     
-// We ignore this exception so that we still attempt the rest of the cleanup.%0A          %7D%0A        %7D);%0A%0A
+  shutdownExceptions%0A                  .add(process.toString() + %22 failed to shutdown: %22 + ie.getMessage());%0A            %7D%0A          %7D);%0A      if (!shutdownExceptions.isEmpty()) %7B%0A        throw new RuntimeException(%22gfshExecutions processes failed to shutdown%22 + LINE_SEPARATOR%0A            + String.join(LINE_SEPARATOR, shutdownExceptions));%0A      %7D%0A    %7D finally %7B%0A  
     
@@ -3289,24 +3289,30 @@
 r.delete();%0A
+    %7D%0A
   %7D%0A%0A  publi

Throw exception if shutdown fails in GfshRule (#1699)