// Copyright 2014 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.devtools.build.skyframe;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for the non-trivial creation logic of {@link ErrorInfo}. */
@RunWith(JUnit4.class)
public class ErrorInfoTest {

  /** Dummy SkyFunctionException implementation for the sake of testing. */
  private static class DummySkyFunctionException extends SkyFunctionException {
    private final boolean isCatastrophic;

    public DummySkyFunctionException(Exception cause, boolean isTransient,
        boolean isCatastrophic) {
      super(cause, isTransient ? Transience.TRANSIENT : Transience.PERSISTENT);
      this.isCatastrophic = isCatastrophic;
    }

    @Override
    public boolean isCatastrophic() {
      return isCatastrophic;
    }
  }

  private void runTestFromException(boolean isDirectlyTransient, boolean isTransitivelyTransient) {
    Exception exception = new IOException("ehhhhh");
    SkyKey causeOfException = GraphTester.toSkyKey("CAUSE, 1234");
    DummySkyFunctionException dummyException =
        new DummySkyFunctionException(exception, isDirectlyTransient, /*isCatastrophic=*/ false);

    ErrorInfo errorInfo = ErrorInfo.fromException(
        new ReifiedSkyFunctionException(dummyException, causeOfException),
        isTransitivelyTransient);

    assertThat(errorInfo.getRootCauses()).containsExactly(causeOfException);
    assertThat(errorInfo.getException()).isSameAs(exception);
    assertThat(errorInfo.getRootCauseOfException()).isSameAs(causeOfException);
    assertThat(errorInfo.getCycleInfo()).isEmpty();
    assertThat(errorInfo.isDirectlyTransient()).isEqualTo(isDirectlyTransient);
    assertThat(errorInfo.isTransitivelyTransient()).isEqualTo(
        isDirectlyTransient || isTransitivelyTransient);
    assertThat(errorInfo.isCatastrophic()).isFalse();
  }

  @Test
  public void testFromException_NonTransient() {
    runTestFromException(/*isDirectlyTransient=*/ false, /*isTransitivelyTransient= */ false);
  }

  @Test
  public void testFromException_DirectlyTransient() {
    runTestFromException(/*isDirectlyTransient=*/ true, /*isTransitivelyTransient= */ false);
  }

  @Test
  public void testFromException_TransitivelyTransient() {
    runTestFromException(/*isDirectlyTransient=*/ false, /*isTransitivelyTransient= */ true);
  }

  @Test
  public void testFromException_DirectlyAndTransitivelyTransient() {
    runTestFromException(/*isDirectlyTransient=*/ true, /*isTransitivelyTransient= */ true);
  }

  @Test
  public void testFromCycle() {
    CycleInfo cycle =
        new CycleInfo(
            ImmutableList.of(GraphTester.toSkyKey("PATH, 1234")),
            ImmutableList.of(GraphTester.toSkyKey("CYCLE, 4321")));

    ErrorInfo errorInfo = ErrorInfo.fromCycle(cycle);

    assertThat(errorInfo.getRootCauses()).isEmpty();
    assertThat(errorInfo.getException()).isNull();
    assertThat(errorInfo.getRootCauseOfException()).isNull();
    assertThat(errorInfo.isTransitivelyTransient()).isFalse();
    assertThat(errorInfo.isCatastrophic()).isFalse();
  }

  @Test
  public void testFromChildErrors() {
    CycleInfo cycle =
        new CycleInfo(
            ImmutableList.of(GraphTester.toSkyKey("PATH, 1234")),
            ImmutableList.of(GraphTester.toSkyKey("CYCLE, 4321")));
    ErrorInfo cycleErrorInfo = ErrorInfo.fromCycle(cycle);

    Exception exception1 = new IOException("ehhhhh");
    SkyKey causeOfException1 = GraphTester.toSkyKey("CAUSE1, 1234");
    DummySkyFunctionException dummyException1 =
        new DummySkyFunctionException(exception1, /*isTransient=*/ true, /*isCatastrophic=*/ false);
    ErrorInfo exceptionErrorInfo1 = ErrorInfo.fromException(
        new ReifiedSkyFunctionException(dummyException1, causeOfException1),
        /*isTransitivelyTransient=*/ false);

    // N.B this ErrorInfo will be catastrophic.
    Exception exception2 = new IOException("blahhhhh");
    SkyKey causeOfException2 = GraphTester.toSkyKey("CAUSE2, 5678");
    DummySkyFunctionException dummyException2 =
        new DummySkyFunctionException(exception2, /*isTransient=*/ false, /*isCatastrophic=*/ true);
    ErrorInfo exceptionErrorInfo2 = ErrorInfo.fromException(
        new ReifiedSkyFunctionException(dummyException2, causeOfException2),
        /*isTransitivelyTransient=*/ false);

    SkyKey currentKey = GraphTester.toSkyKey("CURRENT, 9876");

    ErrorInfo errorInfo = ErrorInfo.fromChildErrors(
        currentKey, ImmutableList.of(cycleErrorInfo, exceptionErrorInfo1, exceptionErrorInfo2));

    assertThat(errorInfo.getRootCauses()).containsExactly(causeOfException1, causeOfException2);

    // For simplicity we test the current implementation detail that we choose the first non-null
    // (exception, cause) pair that we encounter. This isn't necessarily a requirement of the
    // interface, but it makes the test convenient and is a way to document the current behavior.
    assertThat(errorInfo.getException()).isSameAs(exception1);
    assertThat(errorInfo.getRootCauseOfException()).isSameAs(causeOfException1);

    assertThat(errorInfo.getCycleInfo()).containsExactly(
        new CycleInfo(
            ImmutableList.of(currentKey, Iterables.getOnlyElement(cycle.getPathToCycle())),
            cycle.getCycle()));
    assertThat(errorInfo.isTransitivelyTransient()).isTrue();
    assertThat(errorInfo.isCatastrophic()).isTrue();
  }

  @Test
  public void testCannotCreateErrorInfoWithoutExceptionOrCycle() {
    try {
      new ErrorInfo(
          NestedSetBuilder.<SkyKey>emptySet(Order.COMPILE_ORDER),
          /*exception=*/ null,
          /*rootCauseOfException=*/ null,
          ImmutableList.<CycleInfo>of(),
          false,
          false,
          false);
    } catch (IllegalStateException e) {
      // Brittle, but confirms we failed for the right reason.
      assertThat(e)
          .hasMessage("At least one of exception and cycles must be non-null/empty, respectively");
    }
  }

  @Test
  public void testCannotCreateErrorInfoWithExceptionButNoRootCause() {
    try {
      new ErrorInfo(
          NestedSetBuilder.<SkyKey>emptySet(Order.COMPILE_ORDER),
          new IOException("foo"),
          /*rootCauseOfException=*/ null,
          ImmutableList.<CycleInfo>of(),
          false,
          false,
          false);
    } catch (IllegalStateException e) {
      // Brittle, but confirms we failed for the right reason.
      assertThat(e)
          .hasMessageThat()
          .startsWith("exception and rootCauseOfException must both be null or non-null");
    }
  }
}
