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

import static java.util.concurrent.TimeUnit.MINUTES;
import static org.apache.commons.lang.SystemUtils.LINE_SEPARATOR;
import static org.apache.geode.internal.process.ProcessUtils.isProcessAlive;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.junit.After;
import org.junit.Before;

import org.apache.geode.internal.process.ProcessStreamReader.ReadingMode;
import org.apache.geode.internal.util.StopWatch;

/**
 * Abstract base class for functional integration testing of {@link ProcessStreamReader}.
 */
public abstract class AbstractProcessStreamReaderIntegrationTest {

  /** Timeout to join to a running ProcessStreamReader thread */
  private static final int READER_JOIN_TIMEOUT_MILLIS = 2 * 60 * 1000;

  /** Sleep timeout for {@link ProcessSleeps} instead of sleeping Long.MAX_VALUE */
  private static final int PROCESS_FAIL_SAFE_TIMEOUT_MILLIS = 8 * 60 * 1000;

  /** Additional time for launched processes to live before terminating */
  private static final int PROCESS_TIME_TO_LIVE_MILLIS = 3 * 500;

  protected Process process;
  protected ProcessStreamReader stderr;
  protected ProcessStreamReader stdout;

  private StringBuffer stdoutBuffer;
  private StringBuffer stderrBuffer;

  @Before
  public void setUpAbstractProcessStreamReaderIntegrationTest() {
    stdoutBuffer = new StringBuffer();
    stderrBuffer = new StringBuffer();
  }

  @After
  public void afterProcessStreamReaderTestCase() throws Exception {
    if (stderr != null) {
      stderr.stop();
    }
    if (stdout != null) {
      stdout.stop();
    }
    if (process != null) {
      try {
        process.getErrorStream().close();
        process.getInputStream().close();
        process.getOutputStream().close();
      } finally {
        // this is async and can require more than 10 seconds on slower machines
        process.destroy();
      }
    }
  }

  protected abstract ReadingMode getReadingMode();

  protected void assertThatProcessAndReadersStopped() throws InterruptedException {
    assertThatProcessAndReadersStoppedWithExitValue(0);
  }

  protected void assertThatProcessAndReadersStoppedWithExitValue(final int exitValue)
      throws InterruptedException {
    assertThat(process.exitValue()).isEqualTo(exitValue);
    assertThat(stdout.join(READER_JOIN_TIMEOUT_MILLIS).isRunning()).isFalse();
    assertThat(stderr.join(READER_JOIN_TIMEOUT_MILLIS).isRunning()).isFalse();
  }

  protected void assertThatProcessAndReadersDied() throws InterruptedException {
    assertThat(process.exitValue()).isGreaterThan(0);
    assertThat(stdout.join(READER_JOIN_TIMEOUT_MILLIS).isRunning()).isFalse();
    assertThat(stderr.join(READER_JOIN_TIMEOUT_MILLIS).isRunning()).isFalse();
  }

  protected void assertThatProcessIsAlive(final Process process) {
    assertThat(process.isAlive()).isTrue();
  }

  protected void assertThatStdErrContains(final String value) {
    assertThat(stderrBuffer.toString()).contains(value);
  }

  protected void assertThatStdErrContainsExactly(final String value) {
    assertThat(stderrBuffer.toString()).isEqualTo(value);
  }

  protected void assertThatStdOutContainsExactly(final String value) {
    assertThat(stdoutBuffer.toString()).isEqualTo(value);
  }

  protected void givenRunningProcessWithStreamReaders(final Class<?> mainClass) {
    givenStartedProcess(mainClass);

    assertThat(process.isAlive()).isTrue();

    await().until(() -> assertThat(stdout.isRunning()).isTrue());
    await().until(() -> assertThat(stderr.isRunning()).isTrue());
  }

  private void givenStartedProcess(final Class<?> mainClass) {
    try {
      process = new ProcessBuilder(createCommandLine(mainClass)).start();
      stdout = buildProcessStreamReader(process.getInputStream(), getReadingMode());
      stderr = buildProcessStreamReader(process.getErrorStream(), getReadingMode());
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  protected void givenStartedProcessWithStreamListeners(final Class<?> mainClass) {
    try {
      process = new ProcessBuilder(createCommandLine(mainClass)).start();
      stdout = buildProcessStreamReader(process.getInputStream(), getReadingMode(), stdoutBuffer);
      stderr = buildProcessStreamReader(process.getErrorStream(), getReadingMode(), stderrBuffer);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  protected ConditionFactory await() {
    return Awaitility.await().atMost(10, MINUTES);
  }

  protected static String[] createCommandLine(final Class<?> clazz) {
    List<String> commandLine = new ArrayList<>();

    commandLine.add(getJavaPath());
    commandLine.add("-server");
    commandLine.add("-classpath");
    commandLine.add(getClassPath());
    commandLine.add("-D" + "java.awt.headless=true");
    commandLine.add(clazz.getName());

    return commandLine.toArray(new String[commandLine.size()]);
  }

  protected void waitUntilProcessStops() {
    await().until(() -> assertThat(isProcessAlive(process)).isFalse());
  }

  protected void waitUntilProcessStops(final long timeout, final TimeUnit unit) {
    Awaitility.await().atMost(timeout, unit)
        .until(() -> assertThat(isProcessAlive(process)).isFalse());
  }

  private ProcessStreamReader buildProcessStreamReader(final InputStream stream,
      final ReadingMode mode) {
    return new ProcessStreamReader.Builder(process).inputStream(stream).readingMode(mode).build()
        .start();
  }

  private ProcessStreamReader buildProcessStreamReader(final InputStream stream,
      final ReadingMode mode, final StringBuffer buffer) {
    ProcessStreamReader.Builder builder =
        new ProcessStreamReader.Builder(process).inputStream(stream).readingMode(mode);
    if (buffer != null) {
      builder.inputListener(buffer::append);
    }
    return builder.build().start();
  }

  private static String getClassPath() {
    return System.getProperty("java.class.path");
  }

  private static String getJavaPath() {
    String java = "java";
    return new File(new File(System.getProperty("java.home"), "bin"), java).getPath();
  }

  private static void sleepAtMost(final int duration) throws InterruptedException {
    StopWatch stopWatch = new StopWatch(true);
    while (stopWatch.elapsedTimeMillis() < duration) {
      Thread.sleep(1000);
    }
  }

  /**
   * Class with main that sleeps until destroyed.
   */
  protected static class ProcessSleeps {
    public static void main(final String... args) throws InterruptedException {
      sleepAtMost(PROCESS_FAIL_SAFE_TIMEOUT_MILLIS);
    }
  }

  /**
   * Class with main that throws Error.
   */
  protected static class ProcessThrowsError {
    private static final String[] LINES =
        new String[] {"ProcessThrowsError is starting" + LINE_SEPARATOR,
            "ProcessThrowsError is sleeping" + LINE_SEPARATOR,
            "ProcessThrowsError is throwing" + LINE_SEPARATOR};

    protected static final String STDOUT = "";

    protected static final String ERROR_MSG = "ProcessThrowsError throws Error";

    public static void main(final String... args) throws InterruptedException {
      System.err.print(LINES[0]);
      System.err.print(LINES[1]);
      sleepAtMost(PROCESS_TIME_TO_LIVE_MILLIS);
      System.err.print(LINES[2]);
      throw new Error(ERROR_MSG);
    }
  }

  /**
   * Class with main that prints to stdout and sleeps.
   */
  protected static class ProcessPrintsToStdout {
    private static final String[] LINES =
        new String[] {"ProcessPrintsToStdout is starting" + LINE_SEPARATOR,
            "ProcessPrintsToStdout is sleeping" + LINE_SEPARATOR,
            "ProcessPrintsToStdout is exiting" + LINE_SEPARATOR};

    protected static final String STDOUT =
        new StringBuilder().append(LINES[0]).append(LINES[1]).append(LINES[2]).toString();

    protected static final String STDERR = "";

    public static void main(final String... args) throws InterruptedException {
      System.out.print(LINES[0]);
      System.out.print(LINES[1]);
      sleepAtMost(PROCESS_TIME_TO_LIVE_MILLIS);
      System.out.print(LINES[2]);
    }
  }

  /**
   * Class with main that prints to stderr and sleeps.
   */
  protected static class ProcessPrintsToStderr {
    private static final String[] LINES =
        new String[] {"ProcessPrintsToStdout is starting" + LINE_SEPARATOR,
            "ProcessPrintsToStdout is sleeping" + LINE_SEPARATOR,
            "ProcessPrintsToStdout is exiting" + LINE_SEPARATOR};

    protected static final String STDOUT = "";

    protected static final String STDERR =
        new StringBuilder().append(LINES[0]).append(LINES[1]).append(LINES[2]).toString();

    public static void main(final String... args) throws InterruptedException {
      System.err.print(LINES[0]);
      System.err.print(LINES[1]);
      sleepAtMost(PROCESS_TIME_TO_LIVE_MILLIS);
      System.err.print(LINES[2]);
    }
  }

  /**
   * Class with main that prints to both stdout and stderr and sleeps.
   */
  protected static class ProcessPrintsToBoth {
    private static final String[] OUT_LINES =
        new String[] {"ProcessPrintsToBoth(out) is starting" + LINE_SEPARATOR,
            "ProcessPrintsToBoth(out) is sleeping" + LINE_SEPARATOR,
            "ProcessPrintsToBoth(out) is exiting" + LINE_SEPARATOR};

    private static final String[] ERR_LINES =
        new String[] {"ProcessPrintsToBoth(err) is starting" + LINE_SEPARATOR,
            "ProcessPrintsToBoth(err) is sleeping" + LINE_SEPARATOR,
            "ProcessPrintsToBoth(err) is exiting" + LINE_SEPARATOR};

    protected static final String STDOUT = new StringBuilder().append(OUT_LINES[0])
        .append(OUT_LINES[1]).append(OUT_LINES[2]).toString();

    protected static final String STDERR = new StringBuilder().append(ERR_LINES[0])
        .append(ERR_LINES[1]).append(ERR_LINES[2]).toString();

    public static void main(final String... args) throws InterruptedException {
      System.out.print(OUT_LINES[0]);
      System.err.print(ERR_LINES[0]);
      System.out.print(OUT_LINES[1]);
      System.err.print(ERR_LINES[1]);
      sleepAtMost(PROCESS_TIME_TO_LIVE_MILLIS);
      System.out.print(OUT_LINES[2]);
      System.err.print(ERR_LINES[2]);
    }
  }
}
