// Copyright 2018 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.analysis;

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.actions.CompletionContext.FAILED_COMPLETION_CTX;
import static com.google.devtools.build.lib.analysis.TargetCompleteEvent.newFileFromArtifact;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ActionInputMap;
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.TreeFileArtifact;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.CompletionContext;
import com.google.devtools.build.lib.actions.EventReportingArtifacts.ReportedArtifacts;
import com.google.devtools.build.lib.actions.FileArtifactValue;
import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper.ArtifactsToBuild;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.util.AnalysisTestCase;
import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile;
import com.google.devtools.build.lib.buildeventstream.BuildEvent.LocalFile.LocalFileType;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.File;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link TargetCompleteEvent}. */
@RunWith(JUnit4.class)
public class TargetCompleteEventTest extends AnalysisTestCase {

  @Test
  public void testReferencedSourceFile() throws Exception {
    scratch.file("BUILD", "filegroup(name = 'files', srcs = ['file'])");
    scratch.file("file", "content does not matter");
    ConfiguredTargetAndData ctAndData = getCtAndData("//:files");
    ArtifactsToBuild artifactsToBuild = getArtifactsToBuild(ctAndData);
    Artifact artifact = Iterables.getOnlyElement(artifactsToBuild.getAllArtifacts().toList());
    FileArtifactValue metadata =
        FileArtifactValue.createForNormalFile(new byte[] {1, 2, 3}, null, 10);
    CompletionContext completionContext =
        getCompletionContext(ImmutableMap.of(artifact, metadata), ImmutableMap.of());

    TargetCompleteEvent event =
        TargetCompleteEvent.successfulBuild(
            ctAndData,
            completionContext,
            artifactsToBuild.getAllArtifactsByOutputGroup(),
            /* announceTargetSummary= */ false);

    assertThat(event.referencedLocalFiles())
        .containsExactly(
            new LocalFile(artifact.getPath(), LocalFileType.OUTPUT_FILE, artifact, metadata));
  }

  @Test
  public void testReferencedSourceDirectory() throws Exception {
    scratch.file("BUILD", "filegroup(name = 'files', srcs = ['dir'])");
    scratch.file("dir/file", "content does not matter");
    ConfiguredTargetAndData ctAndData = getCtAndData("//:files");
    ArtifactsToBuild artifactsToBuild = getArtifactsToBuild(ctAndData);
    Artifact artifact = Iterables.getOnlyElement(artifactsToBuild.getAllArtifacts().toList());
    FileArtifactValue metadata = FileArtifactValue.createForDirectoryWithMtime(0);
    CompletionContext completionContext =
        getCompletionContext(ImmutableMap.of(artifact, metadata), ImmutableMap.of());

    TargetCompleteEvent event =
        TargetCompleteEvent.successfulBuild(
            ctAndData,
            completionContext,
            artifactsToBuild.getAllArtifactsByOutputGroup(),
            /* announceTargetSummary= */ false);

    assertThat(event.referencedLocalFiles())
        .containsExactly(
            new LocalFile(artifact.getPath(), LocalFileType.OUTPUT_DIRECTORY, artifact, metadata));
  }

  @Test
  public void testReferencedTreeArtifact() throws Exception {
    scratch.file(
        "defs.bzl",
        "def _impl(ctx):",
        "  d = ctx.actions.declare_directory(ctx.label.name)",
        "  ctx.actions.run_shell(outputs = [d], command = 'does not matter')",
        "  return DefaultInfo(files = depset([d]))",
        "dir = rule(_impl)");
    scratch.file(
        "BUILD",
        "load(':defs.bzl', 'dir')",
        "dir(name = 'dir')",
        "filegroup(name = 'files', srcs = ['dir'])");
    ConfiguredTargetAndData ctAndData = getCtAndData("//:files");
    ArtifactsToBuild artifactsToBuild = getArtifactsToBuild(ctAndData);
    SpecialArtifact tree =
        (SpecialArtifact) Iterables.getOnlyElement(artifactsToBuild.getAllArtifacts().toList());
    TreeFileArtifact fileChild =
        TreeFileArtifact.createTreeOutput(tree, PathFragment.create("dir/file.txt"));
    FileArtifactValue fileMetadata =
        FileArtifactValue.createForNormalFile(new byte[] {1, 2, 3}, null, 10);
    // A TreeFileArtifact can be a directory, when materialized by a symlink.
    // See https://github.com/bazelbuild/bazel/issues/20418.
    TreeFileArtifact dirChild = TreeFileArtifact.createTreeOutput(tree, PathFragment.create("sym"));
    FileArtifactValue dirMetadata = FileArtifactValue.createForDirectoryWithMtime(123456789);
    TreeArtifactValue metadata =
        TreeArtifactValue.newBuilder(tree)
            .putChild(fileChild, fileMetadata)
            .putChild(dirChild, dirMetadata)
            .build();
    CompletionContext completionContext =
        getCompletionContext(ImmutableMap.of(), ImmutableMap.of(tree, metadata));

    TargetCompleteEvent event =
        TargetCompleteEvent.successfulBuild(
            ctAndData,
            completionContext,
            artifactsToBuild.getAllArtifactsByOutputGroup(),
            /* announceTargetSummary= */ false);

    assertThat(event.referencedLocalFiles())
        .containsExactly(
            new LocalFile(fileChild.getPath(), LocalFileType.OUTPUT_FILE, fileChild, fileMetadata),
            new LocalFile(
                dirChild.getPath(), LocalFileType.OUTPUT_DIRECTORY, dirChild, dirMetadata));
  }

  @Test
  public void testReferencedUnresolvedSymlink() throws Exception {
    scratch.file(
        "defs.bzl",
        "def _impl(ctx):",
        "  s = ctx.actions.declare_symlink(ctx.label.name)",
        "  ctx.actions.symlink(output = s, target_path = 'does not matter')",
        "  return DefaultInfo(files = depset([s]))",
        "sym = rule(_impl)");
    scratch.file(
        "BUILD",
        "load(':defs.bzl', 'sym')",
        "sym(name = 'sym')",
        "filegroup(name = 'files', srcs = ['sym'])");
    ConfiguredTargetAndData ctAndData = getCtAndData("//:files");
    ArtifactsToBuild artifactsToBuild = getArtifactsToBuild(ctAndData);
    Artifact artifact = Iterables.getOnlyElement(artifactsToBuild.getAllArtifacts().toList());
    artifact.getPath().getParentDirectory().createDirectoryAndParents();
    artifact.getPath().createSymbolicLink(fileSystem.getPath("/some/path"));
    FileArtifactValue metadata = FileArtifactValue.createForUnresolvedSymlink(artifact.getPath());
    CompletionContext completionContext =
        getCompletionContext(ImmutableMap.of(artifact, metadata), ImmutableMap.of());

    TargetCompleteEvent event =
        TargetCompleteEvent.successfulBuild(
            ctAndData,
            completionContext,
            artifactsToBuild.getAllArtifactsByOutputGroup(),
            /* announceTargetSummary= */ false);

    assertThat(event.referencedLocalFiles())
        .containsExactly(
            new LocalFile(artifact.getPath(), LocalFileType.OUTPUT_SYMLINK, artifact, metadata));
  }

  /** Regression test for b/165671166. */
  @Test
  public void testFileProtoFromArtifactReencodesAsUtf8() throws Exception {
    if (OS.getCurrent() == OS.WINDOWS) {
      // Windows filesystems return paths with wide characters and don't suffer from the current
      // workaround where arbitrary bytes are represented to Java as Latin-1.
      return;
    }
    scratch.file("sh/BUILD", "filegroup(name = 'globby', srcs = glob(['dir/*']))");
    // Bytes are UTF-8 encoding of: sh/dir/圖片
    byte[] filenameBytes = {
      0x73, 0x68, 0x2f, 0x64, 0x69, 0x72, 0x2f, -27, -100, -106, -25, -119, -121
    };
    String utf8InLatin1FileName = new String(filenameBytes, ISO_8859_1);
    scratch.file(utf8InLatin1FileName, "content does not matter");
    ConfiguredTargetAndData ctAndData = getCtAndData("//sh:globby");
    ArtifactsToBuild artifactsToBuild = getArtifactsToBuild(ctAndData);

    TargetCompleteEvent event =
        TargetCompleteEvent.successfulBuild(
            ctAndData,
            FAILED_COMPLETION_CTX,
            artifactsToBuild.getAllArtifactsByOutputGroup(),
            /*announceTargetSummary=*/ false);

    ArrayList<File> fileProtos = new ArrayList<>();
    ReportedArtifacts reportedArtifacts = event.reportedArtifacts();
    for (NestedSet<Artifact> artifactSet : reportedArtifacts.artifacts) {
      for (Artifact a : artifactSet.toListInterruptibly()) {
        fileProtos.add(
            newFileFromArtifact(
                /* name= */ null,
                a,
                PathFragment.EMPTY_FRAGMENT,
                FAILED_COMPLETION_CTX,
                /* uri= */ null));
      }
    }
    // Bytes are the same but the encoding is actually UTF-8 as required of a protobuf string.
    String utf8FileName = new String(filenameBytes, UTF_8);
    assertThat(fileProtos).hasSize(1);
    assertThat(fileProtos.get(0).getName()).isEqualTo(utf8FileName);
  }

  private ConfiguredTargetAndData getCtAndData(String target) throws Exception {
    AnalysisResult result = update(target);
    ConfiguredTarget ct = Iterables.getOnlyElement(result.getTargetsToBuild());
    TargetAndConfiguration tac = Iterables.getOnlyElement(result.getTopLevelTargetsWithConfigs());
    var configuredTargetConfiguration =
        (BuildConfigurationValue)
            skyframeExecutor.getEvaluator().getExistingValue(ct.getConfigurationKey());
    return new ConfiguredTargetAndData(ct, tac.getTarget(), configuredTargetConfiguration, null);
  }

  private ArtifactsToBuild getArtifactsToBuild(ConfiguredTargetAndData ctAndData) {
    TopLevelArtifactContext context =
        new TopLevelArtifactContext(false, false, false, OutputGroupInfo.DEFAULT_GROUPS);
    return TopLevelArtifactHelper.getAllArtifactsToBuild(ctAndData.getConfiguredTarget(), context);
  }

  private CompletionContext getCompletionContext(
      Map<Artifact, FileArtifactValue> metadata,
      Map<SpecialArtifact, TreeArtifactValue> treeMetadata) {
    ImmutableMap.Builder<Artifact, ImmutableCollection<? extends Artifact>> expandedArtifacts =
        ImmutableMap.builder();
    ActionInputMap inputMap = new ActionInputMap(0);

    for (Map.Entry<Artifact, FileArtifactValue> entry : metadata.entrySet()) {
      expandedArtifacts.put(entry.getKey(), ImmutableList.of(entry.getKey()));
      inputMap.put(entry.getKey(), entry.getValue(), /* depOwner= */ null);
    }

    for (Map.Entry<SpecialArtifact, TreeArtifactValue> entry : treeMetadata.entrySet()) {
      expandedArtifacts.put(entry.getKey(), entry.getValue().getChildren());
      inputMap.putTreeArtifact(entry.getKey(), entry.getValue(), /* depOwner= */ null);
    }

    return new CompletionContext(
        directories.getExecRoot(TestConstants.WORKSPACE_NAME),
        expandedArtifacts.buildOrThrow(),
        /* expandedFilesets= */ ImmutableMap.of(),
        ArtifactPathResolver.IDENTITY,
        inputMap,
        /* expandFilesets= */ false,
        /* fullyResolveFilesetLinks= */ false);
  }
}
