// Copyright 2022 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.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.createModuleKey;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.CommandLineExpansionException;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryDirtinessChecker;
import com.google.devtools.build.lib.skyframe.SkyframeExecutorRepositoryHelpersHolder;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Map.Entry;
import net.starlark.java.eval.EvalException;
import net.starlark.java.syntax.Location;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests that the repo mapping manifest file is properly generated for runfiles. */
@RunWith(JUnit4.class)
public class RunfilesRepoMappingManifestTest extends BuildViewTestCase {

  @Override
  protected SkyframeExecutorRepositoryHelpersHolder getRepositoryHelpersHolder() {
    // Transitive packages are needed for RepoMappingManifestAction and are only stored when
    // external repositories are enabled.
    return SkyframeExecutorRepositoryHelpersHolder.create(
        new RepositoryDirectoryDirtinessChecker());
  }

  /**
   * Sets up a Bazel module bare_rule@1.0, which provides a bare_binary rule that passes along
   * runfiles in the data attribute, and does nothing else.
   */
  @Before
  public void setupBareBinaryRule() throws Exception {
    registry.addModule(
        createModuleKey("bare_rule", "1.0"), "module(name='bare_rule',version='1.0')");
    scratch.overwriteFile(moduleRoot.getRelative("bare_rule~v1.0/WORKSPACE").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("bare_rule~v1.0/defs.bzl").getPathString(),
        "def _bare_binary_impl(ctx):",
        "  exe = ctx.actions.declare_file(ctx.label.name)",
        "  ctx.actions.write(exe, 'i got nothing', True)",
        "  runfiles = ctx.runfiles(files=ctx.files.data)",
        "  for data in ctx.attr.data:",
        "    runfiles = runfiles.merge(data[DefaultInfo].default_runfiles)",
        "  return DefaultInfo(files=depset(direct=[exe]), executable=exe, runfiles=runfiles)",
        "bare_binary=rule(",
        "  implementation=_bare_binary_impl,",
        "  attrs={'data':attr.label_list(allow_files=True)},",
        "  executable=True,",
        ")");
    scratch.overwriteFile(
        moduleRoot.getRelative("bare_rule~v1.0/BUILD").getPathString(),
        "load('//:defs.bzl', 'bare_binary')",
        "bare_binary(name='bare_binary')");
  }

  private RepoMappingManifestAction getRepoMappingManifestActionForTarget(String label)
      throws Exception {
    Action action = getGeneratingAction(getRunfilesSupport(label).getRepoMappingManifest());
    assertThat(action).isInstanceOf(RepoMappingManifestAction.class);
    return (RepoMappingManifestAction) action;
  }

  private String computeKey(RepoMappingManifestAction action)
      throws CommandLineExpansionException, EvalException, InterruptedException {
    Fingerprint fp = new Fingerprint();
    action.computeKey(actionKeyContext, /* artifactExpander= */ null, fp);
    return fp.hexDigestAndReset();
  }

  private ImmutableList<String> getRepoMappingManifestForTarget(String label) throws Exception {
    return getRepoMappingManifestActionForTarget(label)
        .newDeterministicWriter(null)
        .getBytes()
        .toStringUtf8()
        .lines()
        .collect(toImmutableList());
  }

  @Test
  public void diamond() throws Exception {
    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='bbb',version='1.0')",
        "bazel_dep(name='ccc',version='2.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    registry.addModule(
        createModuleKey("bbb", "1.0"),
        "module(name='bbb',version='1.0')",
        "bazel_dep(name='ddd',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    registry.addModule(
        createModuleKey("ccc", "2.0"),
        "module(name='ccc',version='2.0')",
        "bazel_dep(name='ddd',version='2.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    registry.addModule(
        createModuleKey("ddd", "1.0"),
        "module(name='ddd',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    registry.addModule(
        createModuleKey("ddd", "2.0"),
        "module(name='ddd',version='2.0')",
        "bazel_dep(name='bare_rule',version='1.0')");

    scratch.overwriteFile(
        "BUILD",
        "load('@bare_rule//:defs.bzl', 'bare_binary')",
        "bare_binary(name='aaa',data=['@bbb'])");
    ImmutableMap<String, String> buildFiles =
        ImmutableMap.of(
            "bbb~v1.0", "bare_binary(name='bbb',data=['@ddd'])",
            "ccc~v2.0", "bare_binary(name='ccc',data=['@ddd'])",
            "ddd~v1.0", "bare_binary(name='ddd')",
            "ddd~v2.0", "bare_binary(name='ddd')");
    for (Entry<String, String> entry : buildFiles.entrySet()) {
      scratch.overwriteFile(
          moduleRoot.getRelative(entry.getKey()).getRelative("WORKSPACE").getPathString());
      scratch.overwriteFile(
          moduleRoot.getRelative(entry.getKey()).getRelative("BUILD").getPathString(),
          "load('@bare_rule//:defs.bzl', 'bare_binary')",
          entry.getValue());
    }

    // Called last as it triggers package invalidation, which requires a valid MODULE.bazel setup.
    rewriteWorkspace("workspace(name='aaa_ws')");

    assertThat(getRepoMappingManifestForTarget("//:aaa"))
        .containsExactly(
            ",aaa," + getRuleClassProvider().getRunfilesPrefix(),
            ",aaa_ws," + getRuleClassProvider().getRunfilesPrefix(),
            ",bbb,bbb~",
            "bbb~,bbb,bbb~",
            "bbb~,ddd,ddd~",
            "ddd~,ddd,ddd~")
        .inOrder();
    assertThat(getRepoMappingManifestForTarget("@@ccc~//:ccc"))
        .containsExactly("ccc~,ccc,ccc~", "ccc~,ddd,ddd~", "ddd~,ddd,ddd~")
        .inOrder();
  }

  @Test
  public void runfilesFromToolchain() throws Exception {
    scratch.overwriteFile("MODULE.bazel", "bazel_dep(name='tooled_rule',version='1.0')");
    // tooled_rule offers a tooled_binary rule, which uses a toolchain backed by a binary from
    // bare_rule. tooled_binary explicitly requests that runfiles from this binary are included in
    // its runfiles tree, which would mean that bare_rule should be included in the repo mapping
    // manifest.
    registry.addModule(
        createModuleKey("tooled_rule", "1.0"),
        "module(name='tooled_rule',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')",
        "register_toolchains('//:all')");
    scratch.overwriteFile(moduleRoot.getRelative("tooled_rule~v1.0/WORKSPACE").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("tooled_rule~v1.0/defs.bzl").getPathString(),
        "def _tooled_binary_impl(ctx):",
        "  exe = ctx.actions.declare_file(ctx.label.name)",
        "  ctx.actions.write(exe, 'i got something', True)",
        "  runfiles = ctx.runfiles(files=ctx.files.data)",
        "  for data in ctx.attr.data:",
        "    runfiles = runfiles.merge(data[DefaultInfo].default_runfiles)",
        "  runfiles = runfiles.merge(",
        "      ctx.toolchains['//:toolchain_type'].tooled_info[DefaultInfo].default_runfiles)",
        "  return DefaultInfo(files=depset(direct=[exe]), executable=exe, runfiles=runfiles)",
        "tooled_binary = rule(",
        "  implementation=_tooled_binary_impl,",
        "  attrs={'data':attr.label_list(allow_files=True)},",
        "  executable=True,",
        "  toolchains=['//:toolchain_type'],",
        ")",
        "",
        "def _tooled_toolchain_rule_impl(ctx):",
        "  return [platform_common.ToolchainInfo(tooled_info = ctx.attr.backing_binary)]",
        "tooled_toolchain_rule=rule(_tooled_toolchain_rule_impl,",
        "  attrs={'backing_binary':attr.label()})",
        "def tooled_toolchain(name, backing_binary):",
        "  tooled_toolchain_rule(name=name+'_impl',backing_binary=backing_binary)",
        "  native.toolchain(",
        "    name=name,",
        "    toolchain=':'+name+'_impl',",
        "    toolchain_type=Label('//:toolchain_type'),",
        "  )");
    scratch.overwriteFile(
        moduleRoot.getRelative("tooled_rule~v1.0/BUILD").getPathString(),
        "load('//:defs.bzl', 'tooled_toolchain')",
        "toolchain_type(name='toolchain_type')",
        "tooled_toolchain(name='tooled_toolchain', backing_binary='@bare_rule//:bare_binary')");

    scratch.overwriteFile(
        "BUILD",
        "load('@tooled_rule//:defs.bzl', 'tooled_binary')",
        "tooled_binary(name='tooled')");

    // Called last as it triggers package invalidation, which requires a valid MODULE.bazel setup.
    rewriteWorkspace("workspace(name='main')");

    assertThat(getRepoMappingManifestForTarget("//:tooled"))
        .containsExactly(
            ",main," + getRuleClassProvider().getRunfilesPrefix(),
            "bare_rule~,bare_rule,bare_rule~",
            "tooled_rule~,bare_rule,bare_rule~")
        .inOrder();
  }

  @Test
  public void actionRerunsOnRepoMappingChange_workspaceName() throws Exception {
    overwriteWorkspaceFile("workspace(name='aaa_ws')");
    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        "BUILD", "load('@bare_rule//:defs.bzl', 'bare_binary')", "bare_binary(name='aaa')");
    invalidatePackages();

    RepoMappingManifestAction actionBeforeChange = getRepoMappingManifestActionForTarget("//:aaa");

    overwriteWorkspaceFile("workspace(name='not_aaa_ws')");
    invalidatePackages();

    RepoMappingManifestAction actionAfterChange = getRepoMappingManifestActionForTarget("//:aaa");
    assertThat(computeKey(actionBeforeChange)).isNotEqualTo(computeKey(actionAfterChange));
  }

  @Test
  public void actionRerunsOnRepoMappingChange_repoName() throws Exception {
    overwriteWorkspaceFile("workspace(name='aaa_ws')");
    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        "BUILD", "load('@bare_rule//:defs.bzl', 'bare_binary')", "bare_binary(name='aaa')");
    invalidatePackages();

    RepoMappingManifestAction actionBeforeChange = getRepoMappingManifestActionForTarget("//:aaa");

    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0',repo_name='not_aaa')",
        "bazel_dep(name='bare_rule',version='1.0')");
    invalidatePackages();

    RepoMappingManifestAction actionAfterChange = getRepoMappingManifestActionForTarget("//:aaa");
    assertThat(computeKey(actionBeforeChange)).isNotEqualTo(computeKey(actionAfterChange));
  }

  @Test
  public void actionRerunsOnRepoMappingChange_newEntry() throws Exception {
    overwriteWorkspaceFile("workspace(name='aaa_ws')");
    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        "BUILD", "load('@bare_rule//:defs.bzl', 'bare_binary')", "bare_binary(name='aaa')");

    registry.addModule(
        createModuleKey("bbb", "1.0"),
        "module(name='bbb',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        moduleRoot.getRelative("bbb~v1.0").getRelative("WORKSPACE").getPathString());
    scratch.overwriteFile(moduleRoot.getRelative("bbb~v1.0").getRelative("BUILD").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("bbb~v1.0").getRelative("def.bzl").getPathString(), "BBB = '1'");
    invalidatePackages();

    RepoMappingManifestAction actionBeforeChange = getRepoMappingManifestActionForTarget("//:aaa");

    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='bbb',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        "BUILD",
        "load('@bare_rule//:defs.bzl', 'bare_binary')",
        "load('@bbb//:def.bzl', 'BBB')",
        "bare_binary(name='aaa')");
    invalidatePackages();

    RepoMappingManifestAction actionAfterChange = getRepoMappingManifestActionForTarget("//:aaa");
    assertThat(computeKey(actionBeforeChange)).isNotEqualTo(computeKey(actionAfterChange));
  }

  @Test
  public void hasMappingForSymlinks() throws Exception {
    overwriteWorkspaceFile("workspace(name='my_workspace')");
    scratch.overwriteFile(
        "MODULE.bazel",
        "module(name='my_module',version='1.0')",
        "bazel_dep(name='aaa',version='1.0')");

    registry.addModule(
        createModuleKey("aaa", "1.0"),
        "module(name='aaa',version='1.0')",
        "bazel_dep(name='my_module',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')",
        "bazel_dep(name='symlinks',version='1.0')");
    scratch.overwriteFile(moduleRoot.getRelative("aaa~v1.0/WORKSPACE").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("aaa~v1.0/BUILD").getPathString(),
        "load('@bare_rule//:defs.bzl', 'bare_binary')",
        "bare_binary(name='aaa',data=['@symlinks'])");

    registry.addModule(
        createModuleKey("symlinks", "1.0"),
        "module(name='symlinks',version='1.0')",
        "bazel_dep(name='ddd',version='1.0')");
    scratch.overwriteFile(moduleRoot.getRelative("symlinks~v1.0/WORKSPACE").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("symlinks~v1.0/defs.bzl").getPathString(),
        "def _symlinks_impl(ctx):",
        "  runfiles = ctx.runfiles(",
        "    symlinks = {'path/to/pkg/symlink': ctx.file.data},",
        "    root_symlinks = {ctx.label.workspace_name + '/path/to/pkg/root_symlink':"
            + " ctx.file.data},",
        "  )",
        "  return DefaultInfo(runfiles=runfiles)",
        "symlinks = rule(",
        "  implementation=_symlinks_impl,",
        "  attrs={'data':attr.label(allow_single_file=True)},",
        ")");
    scratch.overwriteFile(
        moduleRoot.getRelative("symlinks~v1.0/BUILD").getPathString(),
        "load('//:defs.bzl', 'symlinks')",
        "symlinks(name='symlinks',data='@ddd')");

    registry.addModule(
        createModuleKey("ddd", "1.0"),
        "module(name='ddd',version='1.0')",
        "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(moduleRoot.getRelative("ddd~v1.0/WORKSPACE").getPathString());
    scratch.overwriteFile(
        moduleRoot.getRelative("ddd~v1.0/BUILD").getPathString(),
        "load('@bare_rule//:defs.bzl', 'bare_binary')",
        "bare_binary(name='ddd')");
    invalidatePackages();

    RunfilesSupport runfilesSupport = getRunfilesSupport("@aaa~//:aaa");
    ImmutableList<String> runfilesPaths =
        runfilesSupport
            .getRunfiles()
            .getRunfilesInputs(reporter, Location.BUILTIN, runfilesSupport.getRepoMappingManifest())
            .keySet()
            .stream()
            .map(PathFragment::getPathString)
            .collect(toImmutableList());
    assertThat(runfilesPaths)
        .containsAtLeast(
            "aaa~/aaa",
            getRuleClassProvider().getRunfilesPrefix() + "/path/to/pkg/symlink",
            "symlinks~/path/to/pkg/root_symlink",
            "_repo_mapping");

    assertThat(getRepoMappingManifestForTarget("@aaa~//:aaa"))
        .containsExactly(
            // @aaa~ contributes the top-level executable to runfiles.
            "aaa~,aaa,aaa~",
            // The symlink is staged under the main repository's runfiles directory and aaa has a
            // repo mapping entry for it.
            "aaa~,my_module," + getRuleClassProvider().getRunfilesPrefix(),
            // @symlinks~ appears as the first segment of a root symlink.
            "aaa~,symlinks,symlinks~",
            "symlinks~,symlinks,symlinks~")
        .inOrder();
  }

  @Test
  public void repoMappingOnFilesToRunProvider() throws Exception {
    scratch.overwriteFile("MODULE.bazel", "bazel_dep(name='bare_rule',version='1.0')");
    scratch.overwriteFile(
        "defs.bzl",
        "def _get_repo_mapping_impl(ctx):",
        "    files_to_run = ctx.attr.bin[DefaultInfo].files_to_run",
        "    return [",
        "        DefaultInfo(files = depset([files_to_run.repo_mapping_manifest])),",
        "    ]",
        "get_repo_mapping = rule(",
        "    implementation = _get_repo_mapping_impl,",
        "    attrs = {'bin':attr.label(cfg='target',executable=True)}",
        ")");
    scratch.overwriteFile(
        "BUILD",
        "load('@bare_rule//:defs.bzl', 'bare_binary')",
        "load('//:defs.bzl', 'get_repo_mapping')",
        "bare_binary(name='aaa')",
        "get_repo_mapping(name='get_repo_mapping', bin=':aaa')");
    invalidatePackages();

    assertThat(getFilesToBuild(getConfiguredTarget("//:get_repo_mapping")).toList())
        .containsExactly(getRunfilesSupport("//:aaa").getRepoMappingManifest());
  }

  /**
   * Similar to {@link BuildViewTestCase#rewriteWorkspace(String...)}, but does not call {@link
   * BuildViewTestCase#invalidatePackages()}.
   */
  public void overwriteWorkspaceFile(String... lines) throws Exception {
    scratch.overwriteFile(
        "WORKSPACE",
        new ImmutableList.Builder<String>()
            .addAll(analysisMock.getWorkspaceContents(mockToolsConfig))
            .addAll(ImmutableList.copyOf(lines))
            .build());
  }
}
