// Copyright 2016 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;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.testing.junit.runner.internal.SignalHandlers;
import com.google.testing.junit.runner.internal.SignalHandlersFactory;
import com.google.testing.junit.runner.junit4.CancellableRequestFactoryFactory;
import com.google.testing.junit.runner.junit4.ClockFactory;
import com.google.testing.junit.runner.junit4.CurrentRunningTestFactory;
import com.google.testing.junit.runner.junit4.JUnit4ConfigFactory;
import com.google.testing.junit.runner.junit4.JUnit4InstanceModules;
import com.google.testing.junit.runner.junit4.JUnit4OptionsFactory;
import com.google.testing.junit.runner.junit4.JUnit4Runner;
import com.google.testing.junit.runner.junit4.JUnit4RunnerFactory;
import com.google.testing.junit.runner.junit4.JUnit4RunnerModule;
import com.google.testing.junit.runner.junit4.JUnit4TestModelBuilderFactory;
import com.google.testing.junit.runner.junit4.JUnit4TestNameListenerFactory;
import com.google.testing.junit.runner.junit4.JUnit4TestStackTraceListenerFactory;
import com.google.testing.junit.runner.junit4.JUnit4TestXmlListenerFactory;
import com.google.testing.junit.runner.junit4.NameListenerFactory;
import com.google.testing.junit.runner.junit4.ProvideTextListenerFactory;
import com.google.testing.junit.runner.junit4.ProvideXmlStreamFactory;
import com.google.testing.junit.runner.junit4.RequestFactory;
import com.google.testing.junit.runner.junit4.ShardingFilterFactoryFactory;
import com.google.testing.junit.runner.junit4.SignalHandlerInstallerFactory;
import com.google.testing.junit.runner.junit4.StackTraceListenerFactory;
import com.google.testing.junit.runner.junit4.TestSuiteModelSupplierFactory;
import com.google.testing.junit.runner.junit4.TextListenerFactory;
import com.google.testing.junit.runner.junit4.TopLevelSuiteFactory;
import com.google.testing.junit.runner.junit4.TopLevelSuiteNameFactory;
import com.google.testing.junit.runner.junit4.XmlListenerFactory;
import com.google.testing.junit.runner.model.AntXmlResultWriterFactory;
import com.google.testing.junit.runner.model.TestSuiteModel;
import com.google.testing.junit.runner.model.TestSuiteModelBuilderFactory;
import com.google.testing.junit.runner.model.XmlResultWriter;
import com.google.testing.junit.runner.sharding.ShardingEnvironmentFactory;
import com.google.testing.junit.runner.sharding.ShardingFilters;
import com.google.testing.junit.runner.sharding.ShardingFiltersFactory;
import com.google.testing.junit.runner.util.MemoizingSupplier;
import com.google.testing.junit.runner.util.SetFactory;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Set;
import java.util.function.Supplier;
import org.junit.internal.TextListener;
import org.junit.runner.Request;
import org.junit.runner.notification.RunListener;

/**
 * Utility class to create a JUnit4Runner instance from a {@link Builder}. All required dependencies
 * are being injected automatically.
 */
public final class JUnit4Bazel {
  private Supplier<Class<?>> topLevelSuiteSupplier;

  private Supplier<Request> requestMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier cancellableRequestFactorySupplier;

  private Supplier<String> topLevelSuiteNameSupplier;

  private Supplier<ShardingFilters> shardingFiltersSupplier;

  private Supplier<XmlResultWriter> resultWriterSupplier;

  private Supplier<TestSuiteModel.Builder> builderSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier jUnit4TestModelBuilderMemoizingSupplier;

  private Supplier<Supplier<TestSuiteModel>> provideTestSuiteModelSupplierMemoizingSupplier;

  private Supplier<PrintStream> stdoutStreamMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier optionsMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier configMemoizingSupplier;

  private Supplier<SignalHandlers> signalHandlersSupplier;

  private Supplier<PrintStream> stderrStreamMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier jUnit4TestStackTraceListenerMemoizingSupplier;

  private Supplier<RunListener> stackTraceListenerSupplier;

  private Supplier<OutputStream> provideXmlStreamMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier jUnit4TestXmlListenerMemoizingSupplier;

  private Supplier<RunListener> xmlListenerSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier provideCurrentRunningTestMemoizingSupplier;

  @SuppressWarnings("rawtypes")
  private Supplier jUnit4TestNameListenerMemoizingSupplier;

  private Supplier<RunListener> nameListenerSupplier;

  private Supplier<TextListener> provideTextListenerMemoizingSupplier;

  private Supplier<RunListener> textListenerSupplier;

  private Supplier<Set<RunListener>> setOfRunListenerProvider;

  private Supplier<JUnit4Runner> jUnit4RunnerProvider;

  private JUnit4Bazel(Builder builder) {
    assert builder != null;
    initialize(builder);
  }

  public static Builder builder() {
    return new Builder();
  }

  @SuppressWarnings("unchecked")
  private void initialize(final Builder builder) {

    this.topLevelSuiteSupplier = TopLevelSuiteFactory.create(builder.suiteClass);

    this.requestMemoizingSupplier =
        new MemoizingSupplier<>(RequestFactory.create(topLevelSuiteSupplier));

    this.cancellableRequestFactorySupplier =
        new MemoizingSupplier<>(CancellableRequestFactoryFactory.create());

    this.topLevelSuiteNameSupplier = TopLevelSuiteNameFactory.create(topLevelSuiteSupplier);

    this.shardingFiltersSupplier = ShardingFiltersFactory.create(
            ShardingEnvironmentFactory.create(),
            ShardingFilterFactoryFactory.create());

    this.resultWriterSupplier = ResultWriterFactory.create(AntXmlResultWriterFactory.create());

    this.builderSupplier =
        TestSuiteModelBuilderFactory.create(
            ClockFactory.create(),
            shardingFiltersSupplier,
            ShardingEnvironmentFactory.create(),
            resultWriterSupplier);

    this.jUnit4TestModelBuilderMemoizingSupplier =
        new MemoizingSupplier<>(JUnit4TestModelBuilderFactory.create(
                requestMemoizingSupplier, topLevelSuiteNameSupplier, builderSupplier));

    this.provideTestSuiteModelSupplierMemoizingSupplier =
        new MemoizingSupplier<Supplier<TestSuiteModel>>(
            TestSuiteModelSupplierFactory.create(jUnit4TestModelBuilderMemoizingSupplier));

    this.stdoutStreamMemoizingSupplier = new MemoizingSupplier<>(StdoutStreamFactory.create());

    this.optionsMemoizingSupplier =
        new MemoizingSupplier<>(JUnit4OptionsFactory.create(builder.config));

    this.configMemoizingSupplier =
        new MemoizingSupplier<Object>(JUnit4ConfigFactory.create(optionsMemoizingSupplier));

    this.signalHandlersSupplier =
        SignalHandlersFactory.create(SignalHandlerInstallerFactory.create());

    this.stderrStreamMemoizingSupplier = new MemoizingSupplier<>(StderrStreamFactory.create());

    this.jUnit4TestStackTraceListenerMemoizingSupplier =
        new MemoizingSupplier<>(JUnit4TestStackTraceListenerFactory.create(
            signalHandlersSupplier, stderrStreamMemoizingSupplier));

    this.stackTraceListenerSupplier =
        StackTraceListenerFactory.create(jUnit4TestStackTraceListenerMemoizingSupplier);

    this.provideXmlStreamMemoizingSupplier =
        new MemoizingSupplier<OutputStream>(
            ProvideXmlStreamFactory.create(configMemoizingSupplier));

    this.jUnit4TestXmlListenerMemoizingSupplier =
        new MemoizingSupplier<Object>(JUnit4TestXmlListenerFactory.create(
            provideTestSuiteModelSupplierMemoizingSupplier,
            cancellableRequestFactorySupplier,
            signalHandlersSupplier,
            provideXmlStreamMemoizingSupplier,
            stderrStreamMemoizingSupplier));

    this.xmlListenerSupplier = XmlListenerFactory.create(jUnit4TestXmlListenerMemoizingSupplier);

    this.provideCurrentRunningTestMemoizingSupplier =
        new MemoizingSupplier<>(CurrentRunningTestFactory.create(builder.jUnit4RunnerModule));

    this.jUnit4TestNameListenerMemoizingSupplier =
        new MemoizingSupplier<Object>(
            JUnit4TestNameListenerFactory.create(provideCurrentRunningTestMemoizingSupplier));

    this.nameListenerSupplier = NameListenerFactory.create(jUnit4TestNameListenerMemoizingSupplier);

    this.provideTextListenerMemoizingSupplier =
        new MemoizingSupplier<>(ProvideTextListenerFactory.create(stdoutStreamMemoizingSupplier));

    this.textListenerSupplier =
        TextListenerFactory.create(provideTextListenerMemoizingSupplier);

    this.setOfRunListenerProvider =
        SetFactory.<RunListener>builder(4, 0)
            .addSupplier(stackTraceListenerSupplier)
            .addSupplier(xmlListenerSupplier)
            .addSupplier(nameListenerSupplier)
            .addSupplier(textListenerSupplier)
            .build();

    this.jUnit4RunnerProvider =
        JUnit4RunnerFactory.create(
            requestMemoizingSupplier,
            cancellableRequestFactorySupplier,
            provideTestSuiteModelSupplierMemoizingSupplier,
            stdoutStreamMemoizingSupplier,
            configMemoizingSupplier,
            setOfRunListenerProvider,
            SetFactory.<JUnit4Runner.Initializer>empty());
  }

  public JUnit4Runner runner() {
    return jUnit4RunnerProvider.get();
  }

  /**
   * A builder for instantiating {@link JUnit4Bazel}.
   */
  public static final class Builder {
    private JUnit4InstanceModules.SuiteClass suiteClass;

    private JUnit4InstanceModules.Config config;

    private JUnit4RunnerModule jUnit4RunnerModule;

    private Builder() {}

    public JUnit4Bazel build() {
      if (suiteClass == null) {
        throw new IllegalStateException(
            JUnit4InstanceModules.SuiteClass.class.getCanonicalName() + " must be set");
      }
      if (config == null) {
        throw new IllegalStateException(
            JUnit4InstanceModules.Config.class.getCanonicalName() + " must be set");
      }
      if (jUnit4RunnerModule == null) {
        this.jUnit4RunnerModule = new JUnit4RunnerModule();
      }
      return new JUnit4Bazel(this);
    }

    @CanIgnoreReturnValue
    public Builder jUnit4RunnerModule(JUnit4RunnerModule jUnit4RunnerModule) {
      this.jUnit4RunnerModule = checkNotNull(jUnit4RunnerModule);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder suiteClass(JUnit4InstanceModules.SuiteClass suiteClass) {
      this.suiteClass = checkNotNull(suiteClass);
      return this;
    }

    @CanIgnoreReturnValue
    public Builder config(JUnit4InstanceModules.Config config) {
      this.config = checkNotNull(config);
      return this;
    }
  }

  private static <T> T checkNotNull(T reference) {
    if (reference == null) {
      throw new NullPointerException();
    }
    return reference;
  }
}
