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

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.actions.ActionInputHelper.asTreeFileArtifacts;
import static org.junit.Assert.fail;

import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionInputHelper;
import com.google.devtools.build.lib.actions.ActionLookupData;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.MissingInputFileException;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.actions.cache.DigestUtils;
import com.google.devtools.build.lib.actions.cache.Metadata;
import com.google.devtools.build.lib.actions.util.TestAction.DummyAction;
import com.google.devtools.build.lib.events.NullEventHandler;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.EvaluationResult;
import com.google.devtools.build.skyframe.MemoizingEvaluator;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Test the behavior of ActionMetadataHandler and ArtifactFunction
 * with respect to TreeArtifacts.
 */
@RunWith(JUnit4.class)
public class TreeArtifactMetadataTest extends ArtifactFunctionTestCase {

  // A list of subpaths for the SetArtifact created by our custom ActionExecutionFunction.
  private List<PathFragment> testTreeArtifactContents;

  @Before
  public final void setUp() throws Exception  {
    delegateActionExecutionFunction = new TreeArtifactExecutionFunction();
  }

  private TreeArtifactValue evaluateTreeArtifact(Artifact treeArtifact,
      Iterable<PathFragment> children)
    throws Exception {
    testTreeArtifactContents = ImmutableList.copyOf(children);
    for (PathFragment child : children) {
      file(treeArtifact.getPath().getRelative(child), child.toString());
    }
    return (TreeArtifactValue) evaluateArtifactValue(treeArtifact, /*mandatory=*/ true);
  }

  private TreeArtifactValue doTestTreeArtifacts(Iterable<PathFragment> children) throws Exception {
    Artifact output = createTreeArtifact("output");
    return doTestTreeArtifacts(output, children);
  }

  private TreeArtifactValue doTestTreeArtifacts(Artifact tree,
      Iterable<PathFragment> children)
      throws Exception {
    TreeArtifactValue value = evaluateTreeArtifact(tree, children);
    assertThat(value.getChildPaths()).containsExactlyElementsIn(ImmutableSet.copyOf(children));
    assertThat(value.getChildren()).containsExactlyElementsIn(
        asTreeFileArtifacts(tree, children));

    // Assertions about digest. As of this writing this logic is essentially the same
    // as that in TreeArtifact, but it's good practice to unit test anyway to guard against
    // breaking changes.
    Map<String, Metadata> digestBuilder = new HashMap<>();
    for (PathFragment child : children) {
      Metadata subdigest = new Metadata(tree.getPath().getRelative(child).getMD5Digest());
      digestBuilder.put(child.getPathString(), subdigest);
    }
    assertThat(DigestUtils.fromMetadata(digestBuilder).getDigestBytesUnsafe())
        .isEqualTo(value.getDigest());
    return value;
  }

  @Test
  public void testEmptyTreeArtifacts() throws Exception {
    TreeArtifactValue value = doTestTreeArtifacts(ImmutableList.<PathFragment>of());
    // Additional test, only for this test method: we expect the Metadata is equal to
    // the digest [0, 0, ...]
    assertThat(value.getMetadata().digest).isEqualTo(value.getDigest());
    // Java zero-fills arrays.
    assertThat(value.getDigest()).isEqualTo(new byte[16]);
  }

  @Test
  public void testEqualTreeArtifacts() throws Exception {
    Artifact treeArtifact = createTreeArtifact("out");
    ImmutableList<PathFragment> children =
        ImmutableList.of(PathFragment.create("one"), PathFragment.create("two"));
    TreeArtifactValue valueOne = evaluateTreeArtifact(treeArtifact, children);
    MemoizingEvaluator evaluator = driver.getGraphForTesting();
    evaluator.delete(new Predicate<SkyKey>() {
      @Override
      public boolean apply(SkyKey key) {
        // Delete action execution node to force our artifacts to be re-evaluated.
        return actions.contains(key.argument());
      }
    });
    TreeArtifactValue valueTwo = evaluateTreeArtifact(treeArtifact, children);
    assertThat(valueOne.getDigest()).isNotSameAs(valueTwo.getDigest());
    assertThat(valueOne).isEqualTo(valueTwo);
  }

  @Test
  public void testTreeArtifactsWithDigests() throws Exception {
    fastDigest = true;
    doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one")));
  }

  @Test
  public void testTreeArtifactsWithoutDigests() throws Exception {
    fastDigest = false;
    doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one")));
  }

  @Test
  public void testTreeArtifactMultipleDigests() throws Exception {
    doTestTreeArtifacts(ImmutableList.of(PathFragment.create("one"), PathFragment.create("two")));
  }

  @Test
  public void testIdenticalTreeArtifactsProduceTheSameDigests() throws Exception {
    // Make sure different root dirs for set artifacts don't produce different digests.
    Artifact one = createTreeArtifact("outOne");
    Artifact two = createTreeArtifact("outTwo");
    ImmutableList<PathFragment> children =
        ImmutableList.of(PathFragment.create("one"), PathFragment.create("two"));
    TreeArtifactValue valueOne = evaluateTreeArtifact(one, children);
    TreeArtifactValue valueTwo = evaluateTreeArtifact(two, children);
    assertThat(valueOne.getDigest()).isEqualTo(valueTwo.getDigest());
  }

  /**
   * Tests that ArtifactFunction rethrows transitive {@link IOException}s as
   * {@link MissingInputFileException}s.
   */
  @Test
  public void testIOExceptionEndToEnd() throws Throwable {
    final IOException exception = new IOException("boop");
    setupRoot(
        new CustomInMemoryFs() {
          @Override
          public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
            if (path.getBaseName().equals("one")) {
              throw exception;
            }
            return super.stat(path, followSymlinks);
          }
        });
    try {
      Artifact artifact = createTreeArtifact("outOne");
      TreeArtifactValue value = evaluateTreeArtifact(artifact,
          ImmutableList.of(PathFragment.create("one")));
      fail("MissingInputFileException expected, got " + value);
    } catch (Exception e) {
      assertThat(Throwables.getRootCause(e).getMessage()).contains(exception.getMessage());
    }
  }

  private void file(Path path, String contents) throws Exception {
    FileSystemUtils.createDirectoryAndParents(path.getParentDirectory());
    writeFile(path, contents);
  }

  private Artifact createTreeArtifact(String path) throws IOException {
    PathFragment execPath = PathFragment.create("out").getRelative(path);
    Path fullPath = root.getRelative(execPath);
    Artifact output =
        new SpecialArtifact(
            fullPath, Root.asDerivedRoot(root, root.getRelative("out")), execPath, ALL_OWNER,
            SpecialArtifactType.TREE);
    actions.add(new DummyAction(ImmutableList.<Artifact>of(), output));
    FileSystemUtils.createDirectoryAndParents(fullPath);
    return output;
  }

  private SkyValue evaluateArtifactValue(Artifact artifact, boolean mandatory) throws Exception {
    SkyKey key = ArtifactSkyKey.key(artifact, mandatory);
    EvaluationResult<SkyValue> result = evaluate(key);
    if (result.hasError()) {
      throw result.getError().getException();
    }
    return result.get(key);
  }

  private void setGeneratingActions() {
    if (evaluator.getExistingValueForTesting(OWNER_KEY) == null) {
      differencer.inject(
          ImmutableMap.of(
              OWNER_KEY,
              new ActionLookupValue(ImmutableList.<ActionAnalysisMetadata>copyOf(actions), false)));
    }
  }

  private <E extends SkyValue> EvaluationResult<E> evaluate(SkyKey... keys)
      throws InterruptedException {
    setGeneratingActions();
    return driver.evaluate(
        Arrays.asList(keys), /*keepGoing=*/
        false,
        SkyframeExecutor.DEFAULT_THREAD_COUNT,
        NullEventHandler.INSTANCE);
  }

  private class TreeArtifactExecutionFunction implements SkyFunction {
    @Override
    public SkyValue compute(SkyKey skyKey, Environment env)
        throws SkyFunctionException, InterruptedException {
      Map<Artifact, FileValue> fileData = new HashMap<>();
      Map<TreeFileArtifact, FileArtifactValue> treeArtifactData = new HashMap<>();
      ActionLookupData actionLookupData = (ActionLookupData) skyKey.argument();
      ActionLookupValue actionLookupValue =
          (ActionLookupValue) env.getValue(actionLookupData.getActionLookupNode());
      Action action = actionLookupValue.getAction(actionLookupData.getActionIndex());
      Artifact output = Iterables.getOnlyElement(action.getOutputs());
      for (PathFragment subpath : testTreeArtifactContents) {
        try {
          TreeFileArtifact suboutput = ActionInputHelper.treeFileArtifact(output, subpath);
          FileValue fileValue = ActionMetadataHandler.fileValueFromArtifact(
              suboutput, null, null);
          fileData.put(suboutput, fileValue);
          treeArtifactData.put(suboutput, FileArtifactValue.create(suboutput, fileValue));
        } catch (IOException e) {
          throw new SkyFunctionException(e, Transience.TRANSIENT) {};
        }
      }

      TreeArtifactValue treeArtifactValue = TreeArtifactValue.create(treeArtifactData);

      return new ActionExecutionValue(
          fileData,
          ImmutableMap.of(output, treeArtifactValue),
          ImmutableMap.<Artifact, FileArtifactValue>of());
    }

    @Override
    public String extractTag(SkyKey skyKey) {
      return null;
    }
  }
}
