// Copyright 2010 The Bazel Authors. All Rights Reserved.
//
// Licensed 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 com.google.testing.junit.runner.junit4;

import com.google.testing.junit.junit4.runner.RegExTestCaseFilter;
import com.google.testing.junit.junit4.runner.SuiteTrimmingFilter;
import com.google.testing.junit.runner.internal.Stdout;
import com.google.testing.junit.runner.internal.junit4.CancellableRequestFactory;
import com.google.testing.junit.runner.model.TestSuiteModel;
import com.google.testing.junit.runner.util.GoogleTestSecurityManager;
import com.google.testing.junit.runner.util.Supplier;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.junit.internal.runners.ErrorReportingRunner;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;

/**
 * Main entry point for running JUnit4 tests.<p>
 */
public class JUnit4Runner {
  private final Request request;
  private final CancellableRequestFactory requestFactory;
  private final Supplier<TestSuiteModel> modelSupplier;
  private final PrintStream testRunnerOut;
  private final JUnit4Config config;
  private final Set<RunListener> runListeners;
  private final Set<Initializer> initializers;

  private GoogleTestSecurityManager googleTestSecurityManager;
  private SecurityManager previousSecurityManager;

  /**
   * Creates a runner.
   */
  @Inject
  JUnit4Runner(
      Request request,
      CancellableRequestFactory requestFactory,
      Supplier<TestSuiteModel> modelSupplier,
      @Stdout PrintStream testRunnerOut,
      JUnit4Config config,
      Set<RunListener> runListeners,
      Set<Initializer> initializers) {
    this.request = request;
    this.requestFactory = requestFactory;
    this.modelSupplier = modelSupplier;
    this.config = config;
    this.testRunnerOut = testRunnerOut;
    this.runListeners = runListeners;
    this.initializers = initializers;
  }

  /**
   * Runs the JUnit4 test.
   *
   * @return Result of running the test
   */
  public Result run() {
    testRunnerOut.println("JUnit4 Test Runner");
    checkJUnitRunnerApiVersion();

    for (Initializer init : initializers) {
      init.initialize();
    }

    // Sharding
    TestSuiteModel model = modelSupplier.get();
    Filter shardingFilter = model.getShardingFilter();

    Request filteredRequest = applyFilters(request, shardingFilter,
        config.getTestIncludeFilterRegexp(),
        config.getTestExcludeFilterRegexp());

    JUnitCore core = new JUnitCore();
    for (RunListener runListener : runListeners) {
      core.addListener(runListener);
    }

    File exitFile = getExitFile();
    exitFileActive(exitFile);
    try {
      try {
        if (config.shouldInstallSecurityManager()) {
          installSecurityManager();
        }
        Request cancellableRequest = requestFactory.createRequest(filteredRequest);
        return core.run(cancellableRequest);
      } finally {
        disableSecurityManager();
      }
    } finally {
      exitFileInactive(exitFile);
    }
  }

  // Support for "premature exit files": Tests may write this to communicate
  // to the runner in case of premature exit.
  private static File getExitFile() {
    String exitFile = System.getenv("TEST_PREMATURE_EXIT_FILE");
    return exitFile == null ? null : new File(exitFile);
  }

  private static void exitFileActive(@Nullable File file) {
    if (file != null) {
      try (FileOutputStream outputStream = new FileOutputStream(file, false)) {
        // Overwrite file content.
        outputStream.write(new byte[0]);
        outputStream.close();
      } catch (IOException e) {
        throw new RuntimeException("Could not write exit file at " + file, e);
      }
    }
  }

  private void exitFileInactive(@Nullable File file) {
    if (file != null) {
      try {
        file.delete();
      } catch (Throwable t) {
        // Just print the stack trace, to avoid masking a real test failure.
        t.printStackTrace(testRunnerOut);
      }
    }
  }

  // VisibleForTesting
  TestSuiteModel getModel() {
    return modelSupplier.get();
  }

  private static Request applyFilter(Request request, Filter filter)
      throws NoTestsRemainException {
    Runner runner = request.getRunner();
    new SuiteTrimmingFilter(filter).apply(runner);
    return Request.runner(runner);
  }

  /**
   * Apply command-line and sharding filters, if appropriate.<p>
   *
   * Note that this is carefully written to avoid running into potential
   * problems with the way runners implement filtering. The JavaDoc for
   * {@link org.junit.runner.manipulation.Filterable} states that tests that
   * don't match the filter should be removed, which implies if you apply two
   * filters, you will always get an intersection of the two. Unfortunately, the
   * filtering implementation of {@link org.junit.runners.ParentRunner} does not
   * do this, and instead uses a "last applied filter wins" strategy.<p>
   *
   * We work around potential problems by ensuring that if we apply a second
   * filter, the filter is more restrictive than the first. We also assume that
   * if filtering fails, the request will have a runner that is a
   * {@link ErrorReportingRunner}. Luckily, we can cover this with tests to make
   * sure we don't break if JUnit changes in the future.
   *
   * @param request Request to filter
   * @param shardingFilter Sharding filter to use; {@link Filter#ALL} to not do sharding
   * @param testIncludeFilterRegexp String denoting a regular expression with which
   *     to filter tests.  Only test descriptions that match this regular
   *     expression will be run.  If {@code null}, tests will not be filtered.
   * @param testExcludeFilterRegexp String denoting a regular expression with which
   *     to filter tests.  Only test descriptions that do not match this regular
   *     expression will be run.  If {@code null}, tests will not be filtered.
   * @return Filtered request (may be a request that delegates to
   *         {@link ErrorReportingRunner}
   */
  private static Request applyFilters(Request request, Filter shardingFilter,
      @Nullable String testIncludeFilterRegexp, @Nullable String testExcludeFilterRegexp) {
    // Allow the user to specify a filter on the command line
    boolean allowNoTests = false;
    Filter filter = Filter.ALL;
    if (testIncludeFilterRegexp != null) {
      filter = RegExTestCaseFilter.include(testIncludeFilterRegexp);
    }

    if (testExcludeFilterRegexp != null) {
      Filter excludeFilter = RegExTestCaseFilter.exclude(testExcludeFilterRegexp);
      filter = filter.intersect(excludeFilter);
    }

    if (testIncludeFilterRegexp != null || testExcludeFilterRegexp != null) {
      try {
        request = applyFilter(request, filter);
      } catch (NoTestsRemainException e) {
        return createErrorReportingRequestForFilterError(filter);
      }

      /*
       * If you filter a sharded test to run one test, we don't want all the
       * shards but one to fail.
       */
      allowNoTests = (shardingFilter != Filter.ALL);
    }

    // Sharding
    if (shardingFilter != Filter.ALL) {
      filter = filter.intersect(shardingFilter);
    }

    if (filter != Filter.ALL) {
      try {
        request = applyFilter(request, filter);
      } catch (NoTestsRemainException e) {
        if (allowNoTests) {
          return Request.runner(new NoOpRunner());
        } else {
          return createErrorReportingRequestForFilterError(filter);
        }
      }
    }
    return request;
  }

  @SuppressWarnings({"ThrowableInstanceNeverThrown"})
  private static Request createErrorReportingRequestForFilterError(Filter filter) {
    ErrorReportingRunner runner = new ErrorReportingRunner(Filter.class, new Exception(
        String.format("No tests found matching %s", filter.describe())));
    return Request.runner(runner);
  }

  private void checkJUnitRunnerApiVersion() {
    config.getJUnitRunnerApiVersion();
  }

  private void installSecurityManager() {
    previousSecurityManager = System.getSecurityManager();
    GoogleTestSecurityManager newSecurityManager = new GoogleTestSecurityManager();
    System.setSecurityManager(newSecurityManager);

    // set field after call to setSecurityManager() in case that call fails
    googleTestSecurityManager = newSecurityManager;
  }

  private void disableSecurityManager() {
    if (googleTestSecurityManager != null) {
      GoogleTestSecurityManager.uninstallIfInstalled();
      System.setSecurityManager(previousSecurityManager);
    }
  }

  static class NoOpRunner extends Runner {
    @Override
    public Description getDescription() {
      return Description.createTestDescription(getClass(), "nothingToDo");
    }

    @Override
    public void run(RunNotifier notifier) {
    }
  }

  /**
   * A simple initializer which can be used to provide additional initialization logic in custom
   * runners.
   *
   * <p>Initializers will be run in unspecified order. If an exception is thrown it will not be
   * deemed recoverable and will cause the runner to error-out.
   */
  public interface Initializer {
    void initialize();
  }
}
