// Copyright 2015 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.model;

import static com.google.testing.junit.runner.util.TestPropertyExporter.INITIAL_INDEX_FOR_REPEATED_PROPERTY;

import com.google.testing.junit.runner.model.TestResult.Status;
import com.google.testing.junit.runner.util.TestClock.TestInstant;
import com.google.testing.junit.runner.util.TestIntegration;
import com.google.testing.junit.runner.util.TestIntegrationsExporter;
import com.google.testing.junit.runner.util.TestPropertyExporter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import javax.annotation.Nullable;
import org.junit.runner.Description;

/** A leaf in the test suite model. */
class TestCaseNode extends TestNode
    implements TestPropertyExporter.Callback, TestIntegrationsExporter.Callback {
  private static final Set<State> INITIAL_STATES = Collections.unmodifiableSet(
      EnumSet.of(State.INITIAL, State.PENDING));
  private final TestSuiteNode parent;
  private final Map<String, String> properties = new ConcurrentHashMap<>();
  private final Map<String, Integer> repeatedPropertyNamesToRepetitions = new HashMap<>();
  private final Queue<Throwable> globalFailures = new ConcurrentLinkedQueue<>();
  private final ConcurrentMap<Description, List<Throwable>> dynamicTestToFailures =
      new ConcurrentHashMap<>();
  private final Set<TestIntegration> integrations =
      Collections.newSetFromMap(new ConcurrentHashMap<TestIntegration, Boolean>());

  @Nullable private volatile TestInterval runTimeInterval = null;
  private volatile State state = State.INITIAL;

  TestCaseNode(Description description, TestSuiteNode parent) {
    super(description);
    this.parent = parent;
  }

  // VisibleForTesting
  @Override
  public List<TestNode> getChildren() {
    return Collections.emptyList();
  }

  /**
   * Indicates that the test represented by this node is scheduled to start.
   */
  void pending() {
    compareAndSetState(State.INITIAL, State.PENDING, TestInstant.UNKNOWN);
  }

  /**
   * Indicates that the test represented by this node has started.
   *
   * @param now Time that the test started
   */
  public void started(TestInstant now) {
    compareAndSetState(INITIAL_STATES, State.STARTED, now);
  }

  @Override
  public void testInterrupted(TestInstant now) {
    if (compareAndSetState(State.STARTED, State.INTERRUPTED, now)) {
      globalFailures.add(new Exception("Test interrupted"));
      return;
    }
    if (compareAndSetState(INITIAL_STATES, State.CANCELLED, now)) {
      globalFailures.add(new Exception("Test cancelled"));
    }
  }

  @Override
  public void exportProperty(String name, String value) {
    properties.put(name, value);
  }

  @Override
  public String exportRepeatedProperty(String name, String value) {
    String propertyName = getRepeatedPropertyName(name);
    properties.put(propertyName, value);
    return propertyName;
  }

  @Override
  public void exportTestIntegration(TestIntegration testIntegration) {
    integrations.add(testIntegration);
  }

  @Override
  public void testSkipped(TestInstant now) {
    compareAndSetState(State.STARTED, State.SKIPPED, now);
  }

  @Override
  public void testSuppressed(TestInstant now) {
    compareAndSetState(INITIAL_STATES, State.SUPPRESSED, now);
  }

  /**
   * Indicates that the test represented by this node has finished.
   *
   * @param now Time that the test finished
   */
  public void finished(TestInstant now) {
    compareAndSetState(State.STARTED, State.FINISHED, now);
  }

  @Override
  public void testFailure(Throwable throwable, TestInstant now) {
    compareAndSetState(INITIAL_STATES, State.FINISHED, now);
    globalFailures.add(throwable);
  }

  @Override
  public void dynamicTestFailure(Description test, Throwable throwable, TestInstant now) {
    compareAndSetState(INITIAL_STATES, State.FINISHED, now);
    addThrowableToDynamicTestToFailures(test, throwable);
  }

  private String getRepeatedPropertyName(String name) {
    int index = addNameToRepeatedPropertyNamesAndGetRepetitionsNr(name)
        + INITIAL_INDEX_FOR_REPEATED_PROPERTY;
    return name + index;
  }

  @Override
  public boolean isTestCase() {
    return true;
  }

  private synchronized void addThrowableToDynamicTestToFailures(
      Description test, Throwable throwable) {
    List<Throwable> throwables = dynamicTestToFailures.get(test);
    if (throwables == null) {
      throwables = new ArrayList<>();
      dynamicTestToFailures.put(test, throwables);
    }
    throwables.add(throwable);
  }

  private synchronized int addNameToRepeatedPropertyNamesAndGetRepetitionsNr(String name) {
    Integer previousRepetitionsNr = repeatedPropertyNamesToRepetitions.get(name);
    if (previousRepetitionsNr == null) {
      previousRepetitionsNr = 0;
    }
    repeatedPropertyNamesToRepetitions.put(name, previousRepetitionsNr + 1);
    return previousRepetitionsNr;
  }

  private boolean compareAndSetState(State fromState, State toState, TestInstant now) {
    if (fromState == null) {
      throw new NullPointerException();
    }
    return compareAndSetState(Collections.singleton(fromState), toState, now);
  }

  // TODO(bazel-team): Use AtomicReference instead of a synchronized method.
  private synchronized boolean compareAndSetState(
      Set<State> fromStates, State toState, TestInstant now) {
    if (fromStates == null || toState == null || state == null) {
      throw new NullPointerException();
    }
    if (fromStates.isEmpty()) {
      throw new IllegalArgumentException();
    }
    if (fromStates.contains(state) && toState != state) {
      state = toState;
      if (toState != State.PENDING) {
        runTimeInterval =
            runTimeInterval == null
            ? new TestInterval(now, now)
            : runTimeInterval.withEndMillis(now);
      }
      return true;
    }
    return false;
  }

  @Nullable
  public TestInterval getRuntime() {
    return runTimeInterval;
  }

  /**
   * @return The equivalent {@link TestResult.Status} if the test execution ends with the FSM
   *      at this state.
   */
  public TestResult.Status getTestResultStatus() {
    return state.getTestResultStatus();
  }

  @Override
  protected TestResult buildResult() {
    // Some test descriptions, like those provided by JavaScript tests, are
    // constructed by Description.createSuiteDescription, not
    // createTestDescription, because they don't have a "class" per se.
    // In this case, getMethodName returns null and we fill in the className
    // attribute with the name of the parent test suite.
    String name = getDescription().getMethodName();
    String className = getDescription().getClassName();
    if (name == null) {
      name = className;
      className = parent.getDescription().getDisplayName();
    }

    // For now, we give each dynamic test an empty properties map and the same
    // run time and status as its parent test case, but this may change.
    List<TestResult> childResults = new ArrayList<>();
    for (Description dynamicTest : getDescription().getChildren()) {
      childResults.add(buildDynamicResult(dynamicTest, getRuntime(), getTestResultStatus()));
    }

    int numTests = getDescription().isTest() ? 1 : getDescription().getChildren().size();
    int numFailures = globalFailures.isEmpty() ? dynamicTestToFailures.keySet().size() : numTests;
    return new TestResult.Builder()
        .name(name)
        .className(className)
        .properties(properties)
        .failures(new ArrayList<>(globalFailures))
        .runTimeInterval(getRuntime())
        .status(getTestResultStatus())
        .numTests(numTests)
        .numFailures(numFailures)
        .childResults(childResults)
        .integrations(integrations)
        .build();
  }

  private TestResult buildDynamicResult(
      Description test, @Nullable TestInterval runTime, TestResult.Status status) {
    // The dynamic test fails if the testcase itself fails or there is
    // a dynamic failure specifically for the dynamic test.
    List<Throwable> dynamicFailures = dynamicTestToFailures.get(test);
    if (dynamicFailures == null) {
      dynamicFailures = new ArrayList<>();
    }
    boolean failed = !globalFailures.isEmpty() || !dynamicFailures.isEmpty();
    return new TestResult.Builder()
        .name(test.getDisplayName())
        .className(getDescription().getDisplayName())
        .properties(Collections.<String, String>emptyMap())
        .failures(dynamicFailures)
        .runTimeInterval(runTime)
        .status(status)
        .numTests(1)
        .numFailures(failed ? 1 : 0)
        .childResults(Collections.<TestResult>emptyList())
        .integrations(Collections.<TestIntegration>emptySet())
        .build();
  }

  /**
   * States of a TestCaseNode (see (link) for all the transitions and states descriptions).
   */
  private static enum State {
    INITIAL(TestResult.Status.FILTERED),
    PENDING(TestResult.Status.CANCELLED),
    STARTED(TestResult.Status.INTERRUPTED),
    SKIPPED(TestResult.Status.SKIPPED),
    SUPPRESSED(TestResult.Status.SUPPRESSED),
    CANCELLED(TestResult.Status.CANCELLED),
    INTERRUPTED(TestResult.Status.INTERRUPTED),
    FINISHED(TestResult.Status.COMPLETED);

    private final Status status;

    State(TestResult.Status status) {
      this.status = status;
    }

    /**
     * @return The equivalent {@link TestResult.Status} if the test execution ends with the FSM
     *      at this state.
     */
    public TestResult.Status getTestResultStatus() {
      return status;
    }
  }
}
