// Copyright 2021 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.packages;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@code native.subpackages} function. */
@RunWith(JUnit4.class)
public class NativeSubpackagesTest extends BuildViewTestCase {

  private static final String ALL_SUBDIRS = "**";

  @Test
  public void subpackages_simple_subDir() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);
    makeFilesSubPackage("test/starlark/sub");

    assertAttrLabelList(
        "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));
  }

  @Test
  public void subpackages_simple_include() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", "sub1/**", null, null);

    makeFilesSubPackage("test/starlark/sub");
    makeFilesSubPackage("test/starlark/sub1");
    makeFilesSubPackage("test/starlark/sub2");

    assertAttrLabelList(
        "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub1:files"));
  }

  @Test
  public void subpackages_simple_exclude() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, "['sub2/**']", null);

    makeFilesSubPackage("test/starlark/sub");
    makeFilesSubPackage("test/starlark/sub1");
    makeFilesSubPackage("test/starlark/sub2");
    makeFilesSubPackage("test/starlark/sub3");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of(
            "//test/starlark/sub:files",
            "//test/starlark/sub1:files",
            "//test/starlark/sub3:files"));
  }

  @Test
  public void subpackages_simple_empty_allow() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, true);
    assertAttrLabelList("//test/starlark:files", "srcs", ImmutableList.of());
  }

  @Test
  public void subpackages_simple_empty_disallow() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);

    // force evaluation
    AssertionError e =
        assertThrows(AssertionError.class, () -> getConfiguredTarget("//test/starlark:files"));
    assertThat(e).hasMessageThat().contains("subpackages pattern '**' didn't match anything");
  }

  @Test
  public void subpackages_deeplyNested_withSubdirs() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, true);

    // Setup a dir with 2 subdirs, 1 a package one not
    makeFilesSubPackage("test/starlark/sub");
    // Should be blocked by 'sub'
    makeFilesSubPackage("test/starlark/sub/sub2");

    makeFilesSubPackage("test/starlark/sub3");
    makeFilesSubPackage("test/starlark/not_sub/sub_is_pkg/eventually");

    scratch.file("test/starlark/not_sub/file1.txt");
    scratch.file("test/starlark/not_sub/double_not_sub/file.txt");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of(
            "//test/starlark/sub:files",
            "//test/starlark/sub3:files",
            "//test/starlark/not_sub/sub_is_pkg/eventually:files"));
  }

  @Test
  public void subpackages_incremental_addSubPkg() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);

    // Setup a two subdirs one shallow and one deep
    makeFilesSubPackage("test/starlark/sub");
    makeFilesSubPackage("test/starlark/deep/1/2/3");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of("//test/starlark/sub:files", "//test/starlark/deep/1/2/3:files"));

    // Add a 2nd shallow and 2nd deep mid
    makeFilesSubPackage("test/starlark/sub2");

    // Poke Skyframe by invalidating the dirent and files that changed.
    invalidateSkyFrameFiles(
        "test/starlark/sub2", "test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");

    // We should now be aware of the new one via Skyframe invalidation.
    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of(
            "//test/starlark/sub:files",
            "//test/starlark/sub2:files",
            "//test/starlark/deep/1/2/3:files"));
  }

  @Test
  public void subpackages_incremental_delSubPkg() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);

    // Setup a single subdir
    makeFilesSubPackage("test/starlark/sub");
    makeFilesSubPackage("test/starlark/sub2");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2:files"));

    scratch.deleteFile("test/starlark/sub2/BUILD");
    scratch.deleteFile("test/starlark/sub2/file.txt");

    invalidateSkyFrameFiles("test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");

    // We should now be aware of the new one.
    assertAttrLabelList(
        "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));
  }

  @Test
  public void subpackages_incremental_convertSubDirToPkg() throws Exception {
    makeSubpackageFileGroup("test/starlark/BUILD", ALL_SUBDIRS, null, null);

    // Setup both immediate and deeply nested sub-dirs with BUILD files.
    makeFilesSubPackage("test/starlark/sub");
    scratch.file("test/starlark/sub2/file2.txt");

    // Initially we have a subdir with 'sub/BUILD' and sub2/file2.txt"
    assertAttrLabelList(
        "//test/starlark:files", "srcs", ImmutableList.of("//test/starlark/sub:files"));

    // Then we add a BUILD file to sub2 making it a package Skyframe should pick
    // that up once invalidated.
    makeFilesSubPackage("test/starlark/sub2");

    // Poke Skyframe by invalidating the dirent and files that changed.
    invalidateSkyFrameFiles("test/starlark/sub2/BUILD", "test/starlark/sub2/file.txt");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2:files"));
  }

  @Test
  public void invalidPositionalParams() throws Exception {
    scratch.file("foo/subdir/BUILD");
    scratch.file("foo/BUILD", "[sh_library(name = p) for p in subpackages(['subdir'])]");

    AssertionError e =
        assertThrows(AssertionError.class, () -> getConfiguredTargetAndData("//foo:subdir"));
    assertThat(e).hasMessageThat().contains("got unexpected positional argument");
  }

  @Test
  public void invalidMissingInclude() throws Exception {
    scratch.file("foo/subdir/BUILD");
    scratch.file("foo/BUILD", "[sh_library(name = p) for p in subpackages()]");

    AssertionError e =
        assertThrows(AssertionError.class, () -> getConfiguredTargetAndData("//foo:subdir"));
    assertThat(e).hasMessageThat().contains("missing 1 required named argument: include");
  }

  @Test
  public void validNoWildCardInclude() throws Exception {
    makeSubpackageFileGroup(
        "test/starlark/BUILD", /*include=*/ ImmutableList.of("sub", "sub2/deep"), null, null);
    makeFilesSubPackage("test/starlark/sub");
    makeFilesSubPackage("test/starlark/sub2/deep");

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of("//test/starlark/sub:files", "//test/starlark/sub2/deep:files"));
  }

  @Test
  public void includeValidMatchSubdir() throws Exception {
    scratch.file("foo/subdir/BUILD");
    scratch.file(
        "foo/BUILD", "[sh_library(name = p) for p in subpackages(include = ['subdir/*'])]");

    getConfiguredTargetAndData("//foo:subdir");
  }

  @Test
  public void includeValidSubMatchSubdir() throws Exception {
    makeFilesSubPackage("test/starlark/subdir/sub/deeper");
    makeFilesSubPackage("test/starlark/subdir/sub2/deeper");
    makeFilesSubPackage("test/starlark/subdir/sub3/deeper");

    makeSubpackageFileGroup("test/starlark/BUILD", "subdir/*/deeper", null, null);

    assertAttrLabelList(
        "//test/starlark:files",
        "srcs",
        ImmutableList.of(
            "//test/starlark/subdir/sub/deeper:files",
            "//test/starlark/subdir/sub2/deeper:files",
            "//test/starlark/subdir/sub3/deeper:files"));
  }

  /**
   * Constructs a BUILD file with a single filegroup target whose srcs attribute is the list of all
   * //p:files, where //p is a subpackage returned by a call to native.subpackages.
   */
  private void makeSubpackageFileGroup(
      String buildPath, ImmutableList<String> include, String exclude, Boolean allowEmpty)
      throws IOException {
    StringBuilder subpackages = new StringBuilder();
    subpackages.append("subpackages(include = [");
    subpackages.append(include.stream().map(i -> "'" + i + "'").collect(Collectors.joining(", ")));
    subpackages.append("]");

    if (exclude != null) {
      subpackages.append(", exclude = ");
      subpackages.append(exclude);
    }

    if (allowEmpty != null) {
      subpackages.append(", allow_empty = ");
      subpackages.append(allowEmpty ? "True" : "False");
    }
    subpackages.append(")");

    scratch.file(
        buildPath,
        "filegroup(",
        "   name = 'files',",
        "   srcs = [",
        "     '//%s/%s:files' % (package_name(), s) for s in " + subpackages,
        "   ],",
        ")");
  }

  private void makeSubpackageFileGroup(
      String buildPath, String include, String exclude, Boolean allowEmpty) throws IOException {
    makeSubpackageFileGroup(buildPath, ImmutableList.of(include), exclude, allowEmpty);
  }

  /**
   * Creates a BUILD file and single file at the given packagePath, the BUILD file will contain a
   * single filegroup called 'files' which contains the created file.
   */
  private void makeFilesSubPackage(String packagePath) throws IOException {
    scratch.file(packagePath + "/file.txt");
    scratch.file(
        packagePath + "/BUILD", "filegroup(", "   name = 'files',", "   srcs = glob(['*']),", ")");
  }

  private void assertAttrLabelList(String target, String attrName, List<String> expectedLabels)
      throws Exception {
    ConfiguredTargetAndData cfgTarget = getConfiguredTargetAndData(target);
    assertThat(cfgTarget).isNotNull();

    ImmutableList<Label> labels =
        expectedLabels.stream().map(this::makeLabel).collect(toImmutableList());

    ConfiguredAttributeMapper configuredAttributeMapper =
        getMapperFromConfiguredTargetAndTarget(cfgTarget);
    assertThat(configuredAttributeMapper.get(attrName, BuildType.LABEL_LIST))
        .containsExactlyElementsIn(labels);
  }

  private Label makeLabel(String label) {
    try {
      return Label.parseCanonical(label);
    } catch (Exception e) {
      fail("Unable to construct Label from " + label);
      return null;
    }
  }

  private void invalidateSkyFrameFiles(String... files) throws Exception {
    ModifiedFileSet.Builder builder = ModifiedFileSet.builder();

    for (String f : files) {
      builder.modify(PathFragment.create(f));
    }

    getSkyframeExecutor()
        .invalidateFilesUnderPathForTesting(
            reporter, builder.build(), Root.fromPath(rootDirectory));
  }
}
