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

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ACTION_OWNER;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.NULL_ARTIFACT_OWNER;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.testutil.MoreAsserts;
import com.google.devtools.build.lib.testutil.Scratch;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests {@link ArtifactFactory}. Also see {@link ArtifactTest} for a test
 * of individual artifacts.
 */
@RunWith(JUnit4.class)
public class ArtifactFactoryTest {

  private static final RepositoryName MAIN = RepositoryName.MAIN;

  private Scratch scratch = new Scratch();

  private Path execRoot;
  private Root clientRoot;
  private Root clientRoRoot;
  private Root alienRoot;
  private ArtifactRoot outRoot;

  private PathFragment fooPath;
  private PackageIdentifier fooPackage;
  private PathFragment fooRelative;

  private PathFragment barPath;
  private PackageIdentifier barPackage;
  private PathFragment barRelative;

  private PathFragment alienPath;
  private PackageIdentifier alienPackage;
  private PathFragment alienRelative;

  private ArtifactFactory artifactFactory;
  private final ActionKeyContext actionKeyContext = new ActionKeyContext();

  @Before
  public final void createFiles() throws Exception  {
    execRoot = scratch.dir("/output/workspace");
    clientRoot = Root.fromPath(scratch.dir("/client/workspace"));
    clientRoRoot = Root.fromPath(scratch.dir("/client/RO/workspace"));
    alienRoot = Root.fromPath(scratch.dir("/client/workspace"));
    outRoot = ArtifactRoot.asDerivedRoot(execRoot, execRoot.getRelative("out-root/x/bin"));

    fooPath = PathFragment.create("foo");
    fooPackage = PackageIdentifier.createInMainRepo(fooPath);
    fooRelative = fooPath.getRelative("foosource.txt");

    barPath = PathFragment.create("foo/bar");
    barPackage = PackageIdentifier.createInMainRepo(barPath);
    barRelative = barPath.getRelative("barsource.txt");

    alienPath = PathFragment.create("external/alien");
    alienPackage = PackageIdentifier.create("@alien", alienPath);
    alienRelative = alienPath.getRelative("alien.txt");

    artifactFactory = new ArtifactFactory(execRoot, "bazel-out");
    setupRoots();
  }

  private void setupRoots() {
    Map<PackageIdentifier, Root> packageRootMap = new HashMap<>();
    packageRootMap.put(fooPackage, clientRoot);
    packageRootMap.put(barPackage, clientRoRoot);
    packageRootMap.put(alienPackage, alienRoot);
    artifactFactory.setPackageRoots(packageRootMap::get);
    Root absoluteRoot = Root.absoluteRoot(clientRoot.asPath().getFileSystem());
    artifactFactory.setSourceArtifactRoots(
        ImmutableMap.of(
            clientRoot,
            ArtifactRoot.asSourceRoot(clientRoot),
            clientRoRoot,
            ArtifactRoot.asSourceRoot(clientRoRoot),
            absoluteRoot,
            ArtifactRoot.asSourceRoot(absoluteRoot)));
  }

  @Test
  public void testGetSourceArtifactYieldsSameArtifact() throws Exception {
    assertThat(artifactFactory.getSourceArtifact(fooRelative, clientRoot))
        .isSameAs(artifactFactory.getSourceArtifact(fooRelative, clientRoot));
  }

  @Test
  public void testGetSourceArtifactUnnormalized() throws Exception {
    assertThat(
            artifactFactory.getSourceArtifact(
                PathFragment.create("foo/./foosource.txt"), clientRoot))
        .isSameAs(artifactFactory.getSourceArtifact(fooRelative, clientRoot));
  }

  @Test
  public void testResolveArtifact_noDerived_simpleSource() throws Exception {
    assertThat(artifactFactory.resolveSourceArtifact(fooRelative, MAIN))
        .isSameAs(artifactFactory.getSourceArtifact(fooRelative, clientRoot));
    assertThat(artifactFactory.resolveSourceArtifact(barRelative, MAIN))
        .isSameAs(artifactFactory.getSourceArtifact(barRelative, clientRoRoot));
  }

  @Test
  public void testResolveArtifact_inExternalRepo() throws Exception {
    Artifact a1 = artifactFactory.getSourceArtifact(alienRelative, alienRoot);
    Artifact a2 = artifactFactory.resolveSourceArtifact(alienRelative, MAIN);
    assertThat(a1).isSameAs(a2);
  }

  @Test
  public void testResolveArtifact_noDerived_derivedRoot() throws Exception {
    assertThat(
            artifactFactory.resolveSourceArtifact(
                outRoot.getRoot().getRelative(fooRelative).relativeTo(execRoot), MAIN))
        .isNull();
    assertThat(
            artifactFactory.resolveSourceArtifact(
                outRoot.getRoot().getRelative(barRelative).relativeTo(execRoot), MAIN))
        .isNull();
  }

  @Test
  public void testResolveArtifact_noDerived_simpleSource_other() throws Exception {
    Artifact actual = artifactFactory.resolveSourceArtifact(fooRelative, MAIN);
    assertThat(actual).isSameAs(artifactFactory.getSourceArtifact(fooRelative, clientRoot));
    actual = artifactFactory.resolveSourceArtifact(barRelative, MAIN);
    assertThat(actual).isSameAs(artifactFactory.getSourceArtifact(barRelative, clientRoRoot));
  }

  @Test
  public void testResolveArtifactWithUpLevelFailsCleanly() throws Exception {
    // We need a package in the root directory to make every exec path (even one with up-level
    // references) be in a package.
    Map<PackageIdentifier, Root> packageRoots =
        ImmutableMap.of(PackageIdentifier.createInMainRepo(PathFragment.create("")), clientRoot);
    artifactFactory.setPackageRoots(packageRoots::get);
    PathFragment outsideWorkspace = PathFragment.create("../foo");
    PathFragment insideWorkspace = PathFragment.create("../workspace/foo");
    assertThat(artifactFactory.resolveSourceArtifact(outsideWorkspace, MAIN)).isNull();
    assertWithMessage(
            "Up-level-containing paths that descend into the right workspace aren't allowed")
        .that(artifactFactory.resolveSourceArtifact(insideWorkspace, MAIN))
        .isNull();
    MockPackageRootResolver packageRootResolver = new MockPackageRootResolver();
    packageRootResolver.setPackageRoots(packageRoots);
    Map<PathFragment, Artifact> result = new HashMap<>();
    result.put(insideWorkspace, null);
    result.put(outsideWorkspace, null);
    assertThat(
        artifactFactory.resolveSourceArtifacts(ImmutableList.of(insideWorkspace, outsideWorkspace),
            packageRootResolver).entrySet()).containsExactlyElementsIn(result.entrySet());
  }

  @Test
  public void testClearResetsFactory() {
    Artifact fooArtifact = artifactFactory.getSourceArtifact(fooRelative, clientRoot);
    artifactFactory.clear();
    setupRoots();
    assertThat(artifactFactory.getSourceArtifact(fooRelative, clientRoot)).isNotSameAs(fooArtifact);
  }

  @Test
  public void testFindDerivedRoot() throws Exception {
    assertThat(artifactFactory.isDerivedArtifact(fooRelative)).isFalse();
    assertThat(artifactFactory.isDerivedArtifact(
        PathFragment.create("bazel-out/local-fastbuild/bin/foo"))).isTrue();
  }

  @Test
  public void testAbsoluteArtifact() throws Exception {
    Root absoluteRoot = Root.absoluteRoot(scratch.getFileSystem());

    assertThat(
            artifactFactory.getSourceArtifact(PathFragment.create("foo"), clientRoot).getExecPath())
        .isEqualTo(PathFragment.create("foo"));
    assertThat(
            artifactFactory
                .getSourceArtifact(PathFragment.create("/foo"), absoluteRoot)
                .getExecPath())
        .isEqualTo(PathFragment.create("/foo"));
    MoreAsserts.assertThrows(
        IllegalArgumentException.class,
        () -> artifactFactory.getSourceArtifact(PathFragment.create("/foo"), clientRoot));
    MoreAsserts.assertThrows(
        IllegalArgumentException.class,
        () -> artifactFactory.getSourceArtifact(PathFragment.create("foo"), absoluteRoot));
  }

  @Test
  public void testSetGeneratingActionIdempotenceNewActionGraph() throws Exception {
    Artifact a = artifactFactory.getDerivedArtifact(fooRelative, outRoot, NULL_ARTIFACT_OWNER);
    Artifact b = artifactFactory.getDerivedArtifact(barRelative, outRoot, NULL_ARTIFACT_OWNER);
    MutableActionGraph actionGraph = new MapBasedActionGraph(actionKeyContext);
    Action originalAction = new ActionsTestUtil.NullAction(NULL_ACTION_OWNER, a);
    actionGraph.registerAction(originalAction);

    // Creating a second Action referring to the Artifact should create a conflict.
    try {
      Action action = new ActionsTestUtil.NullAction(NULL_ACTION_OWNER, a, b);
      actionGraph.registerAction(action);
      fail();
    } catch (ActionConflictException e) {
      assertThat(e.getArtifact()).isSameAs(a);
      assertThat(actionGraph.getGeneratingAction(a)).isSameAs(originalAction);
    }
  }

  private static class MockPackageRootResolver implements PackageRootResolver {
    private final Map<PathFragment, Root> packageRoots = Maps.newHashMap();

    public void setPackageRoots(Map<PackageIdentifier, Root> packageRoots) {
      for (Map.Entry<PackageIdentifier, Root> packageRoot : packageRoots.entrySet()) {
        this.packageRoots.put(packageRoot.getKey().getPackageFragment(), packageRoot.getValue());
      }
    }

    @Override
    public Map<PathFragment, Root> findPackageRootsForFiles(Iterable<PathFragment> execPaths) {
      Map<PathFragment, Root> result = new HashMap<>();
      for (PathFragment execPath : execPaths) {
        for (PathFragment dir = execPath.getParentDirectory(); dir != null;
            dir = dir.getParentDirectory()) {
          if (packageRoots.get(dir) != null) {
            result.put(execPath, packageRoots.get(dir));
          }
        }
        if (result.get(execPath) == null) {
          result.put(execPath, null);
        }
      }
      return result;
    }
  }
}
