// Copyright 2023 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.query2.testutil;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.util.MockToolsConfig;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Setting;
import java.io.IOException;
import java.util.Set;
import org.junit.Test;

/** Tests for the blaze query implementation, --nokeep_going. */
public abstract class QueryTest extends AbstractQueryTest<Target> {

  @Override
  protected QueryHelper<Target> createQueryHelper() {
    return new SkyframeQueryHelper() {
      @Override
      protected String getRootDirectoryNameForSetup() {
        return "/workspace";
      }

      @Override
      protected void performAdditionalClientSetup(MockToolsConfig mockToolsConfig)
          throws IOException {}

      @Override
      protected Iterable<QueryFunction> getExtraQueryFunctions() {
        return ImmutableList.of();
      }
    };
  }

  @Override
  protected boolean includeCppToolchainDependencies() {
    return false;
  }

  @Test
  public void testFindsAllTargets_nativeRuleMacro() throws Exception {
    writeFile(
        "test/starlark/extension.bzl",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + '.txt'], cmd = 'echo hi >$@')");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'macro')",
        "",
        "macro(name = 'rule1')",
        "macro(name = 'rule2')");

    assertThat(targetLabels(eval("//test/starlark:*")))
        .containsExactly(
            "//test/starlark:rule1",
            "//test/starlark:rule2",
            "//test/starlark:BUILD",
            "//test/starlark:rule1.txt",
            "//test/starlark:rule2.txt");
  }

  @Test
  public void testFindsAllTargets_starlarkRuleMacro() throws Exception {
    writeFile(
        "test//starlark/extension.bzl",
        "def impl(ctx):",
        "  return None",
        "",
        "starlark_rule = rule(implementation = impl)",
        "",
        "def macro(name):",
        "  starlark_rule(name = name)");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'macro')",
        "",
        "macro(name = 'rule1')",
        "macro(name = 'rule2')");

    assertThat(targetLabels(eval("//test/starlark:*")))
        .containsExactly("//test/starlark:rule1", "//test/starlark:rule2", "//test/starlark:BUILD");
  }

  @Test
  public void testBuildfiles_starlarkDep() throws Exception {
    writeFile(
        "test//starlark/extension.bzl",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + '.txt'], cmd = 'echo hi >$@')");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'macro')",
        "",
        "macro(name = 'rule1')");

    assertThat(targetLabels(eval("buildfiles(//test/starlark:BUILD)")))
        .containsExactly("//test/starlark:extension.bzl", "//test/starlark:BUILD");
  }

  @Test
  public void testLoadfiles_starlarkDep() throws Exception {
    writeFile(
        "test//starlark/extension.bzl",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + '.txt'], cmd = 'echo hi >$@')");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'macro')",
        "",
        "macro(name = 'rule1')");

    assertThat(targetLabels(eval("loadfiles(//test/starlark:BUILD)")))
        .containsExactly("//test/starlark:extension.bzl");
  }

  @Test
  public void testLoadfiles_sclDep() throws Exception {
    writeBzlAndSclFiles();

    assertThat(targetLabels(eval("loadfiles(//foo:BUILD)")))
        .containsExactly("//bar:direct.scl", "//bar:indirect.scl", "//bar:intermediate.bzl");
  }

  @Test
  public void testDeps_labelKeyedStringDictDeps() throws Exception {
    writeFile(
        "test//starlark/rule.bzl",
        "def _impl(ctx):",
        "  return",
        "my_rule = rule(",
        "  implementation = _impl,",
        "  attrs = {",
        "    'value_dict': attr.label_keyed_string_dict(allow_files=True),",
        "  }",
        ")");
    writeFile("test//starlark/dep.cc");
    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:rule.bzl', 'my_rule')",
        "filegroup(name='group', srcs=['dep.cc'])",
        "my_rule(name='rule', value_dict={':group': 'queried'})");

    assertThat(targetLabels(eval("deps(//test/starlark:rule)")))
        .containsExactly("//test/starlark:rule", "//test/starlark:group", "//test/starlark:dep.cc");
  }

  @Test
  public void testBuildfiles_transitiveStarlarkDeps() throws Exception {
    writeFile(
        "test//starlark/extension1.bzl",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + '.txt'], cmd = 'echo hi >$@')");

    writeFile(
        "test//starlark/extension2.bzl",
        "load('//test/starlark:extension1.bzl', 'macro')",
        "def func(name):",
        "  macro(name)");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension2.bzl', 'func')",
        "",
        "func(name = 'rule1')");

    assertThat(targetLabels(eval("buildfiles(//test/starlark:BUILD)")))
        .containsExactly(
            "//test/starlark:extension1.bzl",
            "//test/starlark:extension2.bzl",
            "//test/starlark:BUILD");
  }

  @Test
  public void testBuildfiles_diamondStarlarkDeps() throws Exception {
    writeFile(
        "test//starlark/extension1.bzl",
        "my_constant = 'rule1'",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + '.txt'], cmd = 'echo hi >$@')");

    writeFile(
        "test//starlark/extension2.bzl",
        "load('//test/starlark:extension1.bzl', 'macro')",
        "def func(name):",
        "  macro(name)");

    writeFile(
        "test//starlark/extension3.bzl",
        "load('//test/starlark:extension1.bzl', 'my_constant')",
        "my_rule_name = my_constant");

    writeFile(
        "test//starlark/extension4.bzl",
        "load('//test/starlark:extension2.bzl', 'func')",
        "load('//test/starlark:extension3.bzl', 'my_rule_name')",
        "",
        "my_dummy_name = my_rule_name");

    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension2.bzl', 'func')",
        "load('//test/starlark:extension3.bzl', 'my_rule_name')",
        "load('//test/starlark:extension4.bzl', 'my_dummy_name')",
        "",
        "func(name = my_rule_name + '-' + my_dummy_name)");

    assertThat(targetLabels(eval("buildfiles(//test/starlark:BUILD)")))
        .containsExactly(
            "//test/starlark:extension1.bzl",
            "//test/starlark:extension2.bzl",
            "//test/starlark:extension3.bzl",
            "//test/starlark:extension4.bzl",
            "//test/starlark:BUILD");
  }

  @Test
  public void testBuildfiles_starlarkDepPackageBuildfileIncluded() throws Exception {
    writeFile("test//starlark2/BUILD");
    writeFile("test//starlark2/extension.bzl", "file_ext = '.txt'");

    writeFile("test//starlark1/BUILD");
    writeFile(
        "test//starlark1/extension.bzl",
        "load('//test/starlark2:extension.bzl', 'file_ext')",
        "def macro(name):",
        "  native.genrule(name = name, outs = [name + file_ext], cmd = 'echo hi >$@')");

    writeFile(
        "test/pkg/BUILD",
        "load('//test/starlark1:extension.bzl', 'macro')",
        "",
        "macro(name = 'rule1')");

    assertThat(targetLabels(eval("buildfiles(//test/pkg:BUILD)")))
        .containsExactly(
            "//test/pkg:BUILD",
            "//test/starlark1:extension.bzl",
            "//test/starlark1:BUILD",
            "//test/starlark2:extension.bzl",
            "//test/starlark2:BUILD");
  }

  @Test
  public void testQueryTimeLoadingWhenPackageDoesNotExist() throws Exception {
    // Given a workspace containing a package "//a",
    writeFile("a/BUILD", "sh_library(name = 'a')");

    // When the query environment is queried for "//a/b:b" which doesn't exist,
    String nonExistentPackage = "a/b";
    String s = evalThrows("//" + nonExistentPackage + ":itsNotThere", false).getMessage();

    // Then an exception is thrown that says that the specified package does not exist.
    assertThat(s).containsMatch("no such package '" + nonExistentPackage + "'");
  }

  @Test
  public void testQueryTimeLoadingWhenPackageIsMalformed() throws Exception {
    // Given a workspace containing a malformed package "//a",
    writeFile("a/BUILD", "sh_library(name = 'a') BUT WAIT THERE'S MORE");

    // When the query environment is queried for "//a:a" which belongs to a malformed package,
    String s = evalThrows("//a:a", false).getMessage();

    // Then an exception is thrown,
    assertThat(s).isNotNull();

    // And then the query output contains a description of the malformed package error.
    assertContainsEvent("unclosed string literal");
  }

  @Test
  public void testQueryTimeLoadingOfSymlinkCyclePackage() throws Exception {
    // Given a workspace containing a symlink cycle that looks like a BUILD file at "//a/BUILD",
    ensureSymbolicLink("a/BUILD", "a/BUILD");

    // When the query environment is queried for "//a:*",
    String s = evalThrows("//a:*", false).getMessage();

    // Then an exception is thrown,
    assertThat(s).isNotNull();

    // And then the query output contains a description of the circular symlink problem.
    assertContainsEvent("circular symlinks detected");
  }

  @Test
  public void boundedDepsQueryWithError() throws Exception {
    writeFile("foo/BUILD", "sh_library(name ='foo', deps = ['//bar'])");
    writeFile("bar/BUILD", "sh_library(name ='bar')");
    writeFile("errorparent", "sh_library(name = 'errorparent', deps = ['//error'])");
    writeFile("error", "has errors");

    evalThrows("deps(//foo:all + //errorparent:all, 25)", false);
  }

  @Test
  public void testIgnoredPackagePrefixes() throws Exception {
    writeFile(helper.getIgnoredPackagePrefixesFile().getPathString(), "a/b", "a/c");
    writeFile("a/BUILD", "filegroup(name = 'a')");
    writeFile("b/BUILD", "filegroup(name = 'b')");
    writeFile("a/b/BUILD", "filegroup(name = 'a_b')");
    writeFile("a/c/BUILD", "filegroup(name = 'a_c')");
    writeFile("a/d/BUILD", "filegroup(name = 'a_d')");
    writeFile("a/e/BUILD", "filegroup(name = 'a_e')");
    // Ensure that modified files are invalidated in the skyframe. If a file has
    // already been read prior to the test's writes, this forces the query to
    // pick up the modified versions.
    helper.maybeHandleDiffs();
    Iterable<String> result = targetLabels(eval("//..."));
    assertThat(result).containsAtLeast("//a:a", "//b:b", "//a/d:a_d", "//a/e:a_e");
    assertThat(result).containsNoneOf("//a/b:a_b", "//a/c:a_c");
    result = targetLabels(eval("//a/..."));
    assertThat(result).containsExactly("//a:a", "//a/d:a_d", "//a/e:a_e");
  }

  private void writeStarlarkDefinedRuleClassBzlFile() throws java.io.IOException {
    writeFile(
        "test//starlark/extension.bzl",
        "def custom_rule_impl(ctx):",
        "  ftb = depset(ctx.attr._secret_labels)",
        "  return struct(runfiles = ctx.runfiles(), files = ftb)",
        "",
        "def secret_labels_func(prefix, suffix):",
        "  return [Label('//test/starlark:' + prefix + '01' + suffix),",
        "          Label('//test/starlark:' + prefix + '02' + suffix)]",
        "",
        "custom_rule = rule(implementation = custom_rule_impl,",
        "  attrs = {'prefix': attr.string(default='default_prefix'),",
        "           'suffix': attr.string(default='default_suffix'),",
        "          '_secret_labels': attr.label_list(default=secret_labels_func)})");
  }

  @Test
  public void testQueryStarlarkComputedDefault() throws Exception {
    writeStarlarkDefinedRuleClassBzlFile();
    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'custom_rule')",
        "",
        "custom_rule(",
        "    name = 'custom',",
        "    prefix = 'a',",
        "    suffix = 'b')");

    Set<Target> targets = eval("//test/starlark:*");
    assertThat(targetLabels(targets))
        .containsExactly(
            "//test/starlark:BUILD",
            "//test/starlark:custom",
            "//test/starlark:a01b",
            "//test/starlark:a02b");
  }

  @Test
  public void testQueryStarlarkComputedDefaultWithConfigurableDeps() throws Exception {
    writeStarlarkDefinedRuleClassBzlFile();
    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'custom_rule')",
        "",
        "config_setting(",
        "    name = 'cfg_a',",
        "    values = {'test_arg': 'something'})",
        "config_setting(",
        "    name = 'cfg_b',",
        "    values = {'test_arg': 'something_else'})",
        "",
        "custom_rule(",
        "    name = 'custom',",
        "    prefix = select({",
        "        ':cfg_a':'a',",
        "        ':cfg_b':'b',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def'}),",
        "    suffix = select({",
        "        ':cfg_a':'a',",
        "        ':cfg_b':'b',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def'}))");

    ImmutableList.Builder<String> computedLabelsBuilder = ImmutableList.builder();
    for (String prefix : ImmutableList.of("a", "b", "def")) {
      for (String middle : ImmutableList.of("01", "02")) {
        for (String suffix : ImmutableList.of("a", "b", "def")) {
          computedLabelsBuilder.add("//test/starlark:" + prefix + middle + suffix);
        }
      }
    }
    ImmutableList<String> computedLabels = computedLabelsBuilder.build();

    Set<Target> targets = eval("//test/starlark:*");
    assertThat(targetLabels(targets))
        .containsAtLeastElementsIn(
            Iterables.concat(
                ImmutableList.of(
                    "//test/starlark:BUILD",
                    "//test/starlark:cfg_a",
                    "//test/starlark:cfg_b",
                    "//test/starlark:custom"),
                computedLabels));
  }

  @Test
  public void testQueryStarlarkComputedDefaultWithConfigurableDepsUsedTwice() throws Exception {
    writeStarlarkDefinedRuleClassBzlFile();
    writeFile(
        "test//starlark/BUILD",
        "load('//test/starlark:extension.bzl', 'custom_rule')",
        "",
        "config_setting(",
        "    name = 'cfg_a',",
        "    values = {'test_arg': 'something'})",
        "config_setting(",
        "    name = 'cfg_b',",
        "    values = {'test_arg': 'something_else'})",
        "",
        "custom_rule(",
        "    name = 'custom_one',",
        "    prefix = select({",
        "        ':cfg_a':'a_one',",
        "        ':cfg_b':'b_one',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def_one'}),",
        "    suffix = select({",
        "        ':cfg_a':'a_one',",
        "        ':cfg_b':'b_one',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def_one'}))",
        "custom_rule(",
        "    name = 'custom_two',",
        "    prefix = select({",
        "        ':cfg_a':'a_two',",
        "        ':cfg_b':'b_two',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def_two'}),",
        "    suffix = select({",
        "        ':cfg_a':'a_two',",
        "        ':cfg_b':'b_two',",
        "        '" + BuildType.Selector.DEFAULT_CONDITION_KEY + "':'def_two'}))");

    ImmutableList.Builder<String> computedLabelsBuilder = ImmutableList.builder();
    for (String prefix : ImmutableList.of("a_one", "b_one", "def_one")) {
      for (String middle : ImmutableList.of("01", "02")) {
        for (String suffix : ImmutableList.of("a_one", "b_one", "def_one")) {
          computedLabelsBuilder.add("//test/starlark:" + prefix + middle + suffix);
        }
      }
    }
    for (String prefix : ImmutableList.of("a_two", "b_two", "def_two")) {
      for (String middle : ImmutableList.of("01", "02")) {
        for (String suffix : ImmutableList.of("a_two", "b_two", "def_two")) {
          computedLabelsBuilder.add("//test/starlark:" + prefix + middle + suffix);
        }
      }
    }
    ImmutableList<String> computedLabels = computedLabelsBuilder.build();

    Set<Target> targets = eval("//test/starlark:*");
    assertThat(targetLabels(targets))
        .containsExactlyElementsIn(
            Iterables.concat(
                ImmutableList.of(
                    "//test/starlark:BUILD",
                    "//test/starlark:cfg_a",
                    "//test/starlark:cfg_b",
                    "//test/starlark:custom_one",
                    "//test/starlark:custom_two"),
                computedLabels));
  }

  @Test
  public void testFileTargetLiteralInSubdirectory() throws Exception {
    writeFile(
        "foo/BUILD",
        "exports_files(glob(['**/*.txt']))");
    writeFile("foo/bar/file1.txt");
    writeFile("foo/bar/file2.txt");
    Set<Target> targets = eval("foo/bar/file1.txt + foo/bar/file2.txt");
    assertThat(targetLabels(targets))
        .containsExactly(
            "//foo:bar/file1.txt",
            "//foo:bar/file2.txt");
  }

  @Test
  public void testShorthandTargetLiteralUnion() throws Exception {
    writeFile(
        "foo/bar/BUILD",
        "sh_library(name = 'bar')");
    writeFile(
        "foo/baz/BUILD",
        "sh_library(name = 'baz')");
    Set<Target> targets = eval("foo/bar + foo/baz");
    assertThat(targetLabels(targets))
        .containsExactly(
            "//foo/bar:bar",
            "//foo/baz:baz");
  }

  @Test
  public void testShorthandAbsoluteTargetLiteralUnion() throws Exception {
    writeFile(
        "foo/bar/BUILD",
        "sh_library(name = 'bar')");
    writeFile(
        "foo/baz/BUILD",
        "sh_library(name = 'baz')");
    Set<Target> targets = eval("//foo/bar + //foo/baz");
    assertThat(targetLabels(targets))
        .containsExactly(
            "//foo/bar:bar",
            "//foo/baz:baz");
  }

  @Test
  public void testLoadfilesWithDuplicates() throws Exception {
    writeFile(
        "foo/BUILD",
        "load('//bar:bar.bzl', 'B')",
        "sh_library(name = 'foo', deps = ['//bar'])");
    writeFile(
        "bar/BUILD",
        "load('//bar:bar.bzl', 'B')",
        "sh_library(name = 'bar')");
    writeFile(
        "bar/bar.bzl",
        "B = []");
    assertThat(evalToString("loadfiles(deps(//foo))")).isEqualTo("//bar:bar.bzl");
  }

  protected void runTestRdepsWithNonDefaultDependencyFilter(String query, String expected)
      throws Exception {
    writeFile(
        "foo/BUILD",
        "genrule(",
        "  name = 'gen',",
        "  tools = [':a'],",
        "  srcs = ['doesntmatter.txt'],",
        "  outs = ['out.txt'],",
        "  cmd = 'blah')",
        "sh_binary(name = 'a', srcs = ['doesntmatter.sh'])",
        "sh_binary(name = 'b', srcs = ['doesntmatter.sh'], data = [':a'])",
        "sh_binary(name = 'c', srcs = ['doesntmatter.sh'], data = [':out.txt'])");
    helper.setQuerySettings(Setting.ONLY_TARGET_DEPS);
    assertThat(evalToString(query)).isEqualTo(expected);
  }

  @Test
  public void testRdepsUnboundedWithNonDefaultDependencyFilter() throws Exception {
    runTestRdepsWithNonDefaultDependencyFilter("rdeps(//foo:all, //foo:a)", "//foo:a //foo:b");
  }

  @Test
  public void testRdepsBoundedWithNonDefaultDependencyFilter() throws Exception {
    runTestRdepsWithNonDefaultDependencyFilter("rdeps(//foo:all, //foo:a, 1)", "//foo:a //foo:b");
  }

  protected Iterable<String> targetLabels(Set<Target> targets) {
    return Iterables.transform(targets, new Function<Target, String>() {
      @Override
      public String apply(Target input) {
        return input.getLabel().toString();
      }
    });
  }
}
