// 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.view.java;

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.prettyArtifactNames;
import static com.google.devtools.build.lib.rules.java.JavaCompileActionTestHelper.getDirectJars;

import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.configuredtargets.OutputFileConfiguredTarget;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.rules.java.JavaCompilationArgsProvider;
import com.google.devtools.build.lib.rules.java.JavaCompilationInfoProvider;
import com.google.devtools.build.lib.rules.java.JavaCompileAction;
import com.google.devtools.build.lib.rules.java.JavaImport;
import com.google.devtools.build.lib.rules.java.JavaInfo;
import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider;
import com.google.devtools.build.lib.rules.java.JavaSourceJarsProvider;
import com.google.devtools.build.lib.rules.java.ProguardSpecProvider;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link JavaImport}. */
@RunWith(JUnit4.class)
public class JavaImportConfiguredTargetTest extends BuildViewTestCase {

  @Before
  public final void writeBuildFile() throws Exception {
    scratch.file(
        "java/jarlib/BUILD",
        "java_import(name = 'libraryjar',",
        "            jars = ['library.jar'])",
        "java_import(name = 'libraryjar_with_srcjar',",
        "            jars = ['library.jar'],",
        "            srcjar = 'library.srcjar')");

    scratch.overwriteFile(
        "tools/allowlists/java_import_exports/BUILD",
        "package_group(",
        "    name = 'java_import_exports',",
        "    packages = ['//...'],",
        ")");
  }

  @Test
  public void testSimple() throws Exception {
    ConfiguredTarget jarLib = getConfiguredTarget("//java/jarlib:libraryjar");
    assertThat(prettyArtifactNames(getFilesToBuild(jarLib)))
        .containsExactly("java/jarlib/library.jar");
  }

  // Regression test for b/5868388.
  @Test
  public void testJavaLibraryAllowsImportInDeps() throws Exception {
    scratchConfiguredTarget(
        "java",
        "javalib",
        "java_library(name = 'javalib',",
        "             srcs = ['Other.java'],",
        "             exports = ['//java/jarlib:libraryjar'])");
    assertNoEvents(); // Make sure that no warnings were emitted.
  }

  private static void validateRuntimeClassPath(
      ConfiguredTarget binary, String... expectedRuntimeClasspath) throws Exception {
    assertThat(
            prettyArtifactNames(
                JavaInfo.getProvider(JavaCompilationInfoProvider.class, binary)
                    .getRuntimeClasspath()
                    .getSet(Artifact.class)))
        .containsExactlyElementsIn(expectedRuntimeClasspath)
        .inOrder();
  }

  private static void validateCompilationClassPath(
      ConfiguredTarget binary, String... expectedCompilationClasspath) throws Exception {
    assertThat(
            prettyArtifactNames(
                JavaInfo.getProvider(JavaCompilationInfoProvider.class, binary)
                    .getCompilationClasspath()
                    .getSet(Artifact.class)))
        .containsExactlyElementsIn(expectedCompilationClasspath)
        .inOrder();
  }

  @Test
  public void testWithJavaLibrary() throws Exception {
    scratch.file(
        "java/somelib/BUILD",
        "java_library(name  = 'javalib',",
        "             srcs = ['Other.java'],",
        "             deps = ['//java/jarlib:libraryjar'])");

    ConfiguredTarget javaLib = getConfiguredTarget("//java/somelib:javalib");

    validateCompilationClassPath(
        javaLib, "java/jarlib/_ijar/libraryjar/java/jarlib/library-ijar.jar");

    validateRuntimeClassPath(javaLib, "java/somelib/libjavalib.jar", "java/jarlib/library.jar");
  }


  @Test
  public void testDeps() throws Exception {
    scratch.file(
        "java/jarlib2/BUILD",
        "java_library(name  = 'lib',",
        "             srcs = ['Main.java'],",
        "             deps = [':import-jar'])",
        "java_import(name  = 'import-jar',",
        "            jars = ['import.jar'],",
        "            deps = ['//java/jarlib2:depjar'],",
        "            exports = ['//java/jarlib2:exportjar'],",
        ")",
        "java_import(name  = 'depjar',",
        "            jars = ['depjar.jar'])",
        "java_import(name  = 'exportjar',",
        "            jars = ['exportjar.jar'])");

    ConfiguredTarget importJar = getConfiguredTarget("//java/jarlib2:import-jar");

    assertThat(prettyArtifactNames(getFilesToBuild(importJar)))
        .containsExactly("java/jarlib2/import.jar");

    // JavaCompilationArgs should hold classpaths of the transitive closure.
    JavaCompilationArgsProvider recursiveCompilationArgs =
        JavaInfo.getProvider(JavaCompilationArgsProvider.class, importJar);
    assertThat(prettyArtifactNames(recursiveCompilationArgs.getTransitiveCompileTimeJars()))
        .containsExactly(
            "java/jarlib2/_ijar/import-jar/java/jarlib2/import-ijar.jar",
            "java/jarlib2/_ijar/exportjar/java/jarlib2/exportjar-ijar.jar",
            "java/jarlib2/_ijar/depjar/java/jarlib2/depjar-ijar.jar")
        .inOrder();
    assertThat(prettyArtifactNames(recursiveCompilationArgs.getRuntimeJars()))
        .containsExactly(
            "java/jarlib2/import.jar", "java/jarlib2/exportjar.jar", "java/jarlib2/depjar.jar")
        .inOrder();

    // Recursive deps work the same as with java_library.
    JavaCompilationArgsProvider compilationArgsProvider =
        JavaInfo.getProvider(JavaCompilationArgsProvider.class, importJar);
    assertThat(prettyArtifactNames(compilationArgsProvider.getDirectCompileTimeJars()))
        .containsExactly(
            "java/jarlib2/_ijar/import-jar/java/jarlib2/import-ijar.jar",
            "java/jarlib2/_ijar/exportjar/java/jarlib2/exportjar-ijar.jar")
        .inOrder();
    assertThat(prettyArtifactNames(compilationArgsProvider.getRuntimeJars()))
        .containsExactly(
            "java/jarlib2/import.jar", "java/jarlib2/exportjar.jar", "java/jarlib2/depjar.jar")
        .inOrder();

    // Check that parameters propagate to Java libraries properly.
    ConfiguredTarget lib = getConfiguredTarget("//java/jarlib2:lib");
    validateCompilationClassPath(
        lib,
        "java/jarlib2/_ijar/import-jar/java/jarlib2/import-ijar.jar",
        "java/jarlib2/_ijar/exportjar/java/jarlib2/exportjar-ijar.jar",
        "java/jarlib2/_ijar/depjar/java/jarlib2/depjar-ijar.jar");

    validateRuntimeClassPath(
        lib,
        "java/jarlib2/liblib.jar",
        "java/jarlib2/import.jar",
        "java/jarlib2/exportjar.jar",
        "java/jarlib2/depjar.jar");
  }

  @Test
  public void testSrcJars() throws Exception {
    ConfiguredTarget jarLibWithSources =
        getConfiguredTarget("//java/jarlib:libraryjar_with_srcjar");

    assertThat(
            Iterables.getOnlyElement(
                    JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, jarLibWithSources)
                        .getAllSrcOutputJars())
                .prettyPrint())
        .isEqualTo("java/jarlib/library.srcjar");
  }

  @Test
  public void testFromGenrule() throws Exception {
    scratch.file(
        "java/genrules/BUILD",
        "genrule(name  = 'generated_jar',",
        "        outs = ['generated.jar'],",
        "        cmd = '')",
        "genrule(name  = 'generated_src_jar',",
        "        outs = ['generated.srcjar'],",
        "        cmd = '')",
        "java_import(name  = 'library-jar',",
        "            jars = [':generated_jar'],",
        "            srcjar = ':generated_src_jar',",
        "            exports = ['//java/jarlib:libraryjar'])");
    ConfiguredTarget jarLib = getConfiguredTarget("//java/genrules:library-jar");

    JavaCompilationArgsProvider compilationArgs =
        JavaInfo.getProvider(JavaCompilationArgsProvider.class, jarLib);
    assertThat(prettyArtifactNames(compilationArgs.getTransitiveCompileTimeJars()))
        .containsExactly(
            "java/genrules/_ijar/library-jar/java/genrules/generated-ijar.jar",
            "java/jarlib/_ijar/libraryjar/java/jarlib/library-ijar.jar")
        .inOrder();
    assertThat(prettyArtifactNames(compilationArgs.getRuntimeJars()))
        .containsExactly("java/genrules/generated.jar", "java/jarlib/library.jar")
        .inOrder();

    Artifact jar = compilationArgs.getRuntimeJars().toList().get(0);
    assertThat(getGeneratingAction(jar).prettyPrint())
        .isEqualTo("action 'Executing genrule //java/genrules:generated_jar'");
  }

  @Test
  public void testAllowsJarInSrcjars() throws Exception {
    scratch.file(
        "java/srcjarlib/BUILD",
        "java_import(name  = 'library-jar',",
        "            jars = ['somelib.jar'],",
        "            srcjar = 'somelib-src.jar')");
    ConfiguredTarget jarLib = getConfiguredTarget("//java/srcjarlib:library-jar");
    assertThat(
            Iterables.getOnlyElement(
                    JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, jarLib)
                        .getAllSrcOutputJars())
                .prettyPrint())
        .isEqualTo("java/srcjarlib/somelib-src.jar");
  }

  @Test
  public void testRequiresJars() throws Exception {
    checkError("pkg", "rule", "mandatory attribute 'jars'", "java_import(name = 'rule')");
  }

  @Test
  public void testPermitsEmptyJars() throws Exception {
    scratchConfiguredTarget("pkg", "rule", "java_import(name = 'rule', jars = [])");
  }

  @Test
  public void testDisallowsFilesInExports() throws Exception {
    scratch.file("pkg/bad.jar", "");
    checkError(
        "pkg",
        "rule",
        "expected no files",
        "java_import(name = 'rule', jars = ['good.jar'], exports = ['bad.jar'])");
  }

  @Test
  public void testDisallowsArbitraryFiles() throws Exception {
    scratch.file("badlib/not-a-jar.txt", "foo");
    checkError(
        "badlib",
        "library-jar",
        getErrorMsgMisplacedFiles(
            "jars", "java_import", "//badlib:library-jar", "//badlib:not-a-jar.txt"),
        "java_import(name = 'library-jar',",
        "            jars = ['not-a-jar.txt'])");
  }

  @Test
  public void testDisallowsArbitraryFilesFromGenrule() throws Exception {
    checkError(
        "badlib",
        "library-jar",
        getErrorMsgNoGoodFiles("jars", "java_import", "//badlib:library-jar", "//badlib:gen"),
        "genrule(name = 'gen', outs = ['not-a-jar.txt'], cmd = '')",
        "java_import(name  = 'library-jar',",
        "            jars = [':gen'])");
  }

  @Test
  public void testDisallowsJavaRulesInSrcs() throws Exception {
    checkError(
        "badlib",
        "library-jar",
        "//badlib:library-jar: should not refer to Java rules",
        "java_library(name = 'javalib',",
        "             srcs = ['Javalib.java'])",
        "java_import(name  = 'library-jar',",
        "            jars = [':javalib'])");
  }

  @Test
  public void testJavaImportExportsTransitiveProguardSpecs() throws Exception {
    scratch.file(
        "java/com/google/android/hello/BUILD",
        "java_import(name = 'export',",
        "            jars = ['Export.jar'],",
        "            proguard_specs = ['export.pro'],",
        "            constraints = ['android'])",
        "java_import(name = 'runtime_dep',",
        "            jars = ['RuntimeDep.jar'],",
        "            proguard_specs = ['runtime_dep.pro'],",
        "            constraints = ['android'])",
        "java_import(name = 'lib',",
        "            jars = ['Lib.jar'],",
        "            proguard_specs = ['lib.pro'],",
        "            constraints = ['android'],",
        "            exports = [':export'],",
        "            runtime_deps = [':runtime_dep'])");
    NestedSet<Artifact> providedSpecs =
        getConfiguredTarget("//java/com/google/android/hello:lib")
            .get(ProguardSpecProvider.PROVIDER)
            .getTransitiveProguardSpecs();
    assertThat(ActionsTestUtil.baseArtifactNames(providedSpecs))
        .containsAtLeast("lib.pro_valid", "export.pro_valid", "runtime_dep.pro_valid");
  }

  @Test
  public void testJavaImportValidatesProguardSpecs() throws Exception {
    scratch.file(
        "java/com/google/android/hello/BUILD",
        "java_import(name = 'lib',",
        "            jars = ['Lib.jar'],",
        "            proguard_specs = ['lib.pro'],",
        "            constraints = ['android'])");
    SpawnAction action =
        (SpawnAction)
            actionsTestUtil()
                .getActionForArtifactEndingWith(
                    getOutputGroup(
                        getConfiguredTarget("//java/com/google/android/hello:lib"),
                        OutputGroupInfo.HIDDEN_TOP_LEVEL),
                    "lib.pro_valid");
    assertWithMessage("Proguard validate action").that(action).isNotNull();
    assertWithMessage("Proguard validate action input")
        .that(prettyArtifactNames(action.getInputs()))
        .contains("java/com/google/android/hello/lib.pro");
  }

  @Test
  public void testJavaImportValidatesTransitiveProguardSpecs() throws Exception {
    scratch.file(
        "java/com/google/android/hello/BUILD",
        "java_import(name = 'transitive',",
        "            jars = ['Transitive.jar'],",
        "            proguard_specs = ['transitive.pro'],",
        "            constraints = ['android'])",
        "java_import(name = 'lib',",
        "            jars = ['Lib.jar'],",
        "            constraints = ['android'],",
        "            exports = [':transitive'])");
    SpawnAction action =
        (SpawnAction)
            actionsTestUtil()
                .getActionForArtifactEndingWith(
                    getOutputGroup(
                        getConfiguredTarget("//java/com/google/android/hello:lib"),
                        OutputGroupInfo.HIDDEN_TOP_LEVEL),
                    "transitive.pro_valid");
    assertWithMessage("Proguard validate action").that(action).isNotNull();
    assertWithMessage("Proguard validate action input")
        .that(prettyArtifactNames(action.getInputs()))
        .contains("java/com/google/android/hello/transitive.pro");
  }

  @Test
  public void testNeverlinkIsPopulated() throws Exception {
    scratch.file(
        "java/com/google/test/BUILD",
        "java_library(name = 'lib')",
        "java_import(name = 'jar',",
        "    neverlink = 1,",
        "    jars = ['dummy.jar'],",
        "    exports = [':lib'])");
    ConfiguredTarget processorTarget = getConfiguredTarget("//java/com/google/test:jar");
    JavaInfo javaInfo = (JavaInfo) processorTarget.get(JavaInfo.PROVIDER.getKey());
    assertThat(javaInfo.isNeverlink()).isTrue();
  }

  @Test
  public void testTransitiveSourceJars() throws Exception {
    ConfiguredTarget aTarget =
        scratchConfiguredTarget(
            "java/my",
            "a",
            "java_import(name = 'a',",
            "    jars = ['dummy.jar'],",
            "    srcjar = 'dummy-src.jar',",
            "    exports = [':b'])",
            "java_library(name = 'b',",
            "    srcs = ['B.java'])");
    getConfiguredTarget("//java/my:a");
    Set<String> inputs =
        artifactsToStrings(
            JavaInfo.getProvider(JavaSourceJarsProvider.class, aTarget).getTransitiveSourceJars());
    assertThat(inputs)
        .isEqualTo(Sets.newHashSet("src java/my/dummy-src.jar", "bin java/my/libb-src.jar"));
  }

  @Test
  public void testExportsRunfilesCollection() throws Exception {
    scratch.file(
        "java/com/google/exports/BUILD",
        "java_import(name = 'other_lib',",
        "  data = ['foo.txt'],",
        "  jars = ['other.jar'])",
        "java_import(name = 'lib',",
        "  jars = ['lib.jar'],",
        "  exports = [':other_lib'])",
        "java_binary(name = 'tool',",
        "  data = [':lib'],",
        "  main_class = 'com.google.exports.Launcher')");

    ConfiguredTarget testTarget = getConfiguredTarget("//java/com/google/exports:tool");
    Runfiles runfiles = getDefaultRunfiles(testTarget);
    assertThat(prettyArtifactNames(runfiles.getArtifacts()))
        .containsAtLeast(
            "java/com/google/exports/lib.jar",
            "java/com/google/exports/other.jar",
            "java/com/google/exports/foo.txt");
  }

  // Regression test for b/13936397: don't flatten transitive dependencies into direct deps.
  @Test
  public void testTransitiveDependencies() throws Exception {
    scratch.file(
        "java/jarlib2/BUILD",
        "java_library(name = 'lib',",
        "             srcs = ['Lib.java'],",
        "             deps = ['//java/jarlib:libraryjar'])",
        "java_import(name  = 'library2-jar',",
        "            jars = ['library2.jar'],",
        "            exports = [':lib'])",
        "java_library(name  = 'javalib2',",
        "             srcs = ['Other.java'],",
        "             deps = [':library2-jar'])");

    JavaCompileAction javacAction =
        (JavaCompileAction) getGeneratingActionForLabel("//java/jarlib2:libjavalib2.jar");
    // Direct jars should NOT include java/jarlib/libraryjar-ijar.jar
    assertThat(prettyArtifactNames(getInputs(javacAction, getDirectJars(javacAction))))
        .isEqualTo(
            Arrays.asList(
                "java/jarlib2/_ijar/library2-jar/java/jarlib2/library2-ijar.jar",
                "java/jarlib2/liblib-hjar.jar"));
  }

  @Test
  public void testRuntimeDepsAreNotOnClasspath() throws Exception {
    scratch.file(
        "java/com/google/runtimetest/BUILD",
        "java_import(",
        "    name = 'import_dep',",
        "    jars = ['import_compile.jar'],",
        "    runtime_deps = ['import_runtime.jar'],",
        ")",
        "java_library(",
        "    name = 'library_dep',",
        "    srcs = ['library_compile.java'],",
        ")",
        "java_library(",
        "    name = 'depends_on_runtimedep',",
        "    srcs = ['dummy.java'],",
        "    deps = [",
        "        ':import_dep',",
        "        ':library_dep',",
        "    ],",
        ")");

    OutputFileConfiguredTarget dependsOnRuntimeDep =
        (OutputFileConfiguredTarget)
            getFileConfiguredTarget("//java/com/google/runtimetest:libdepends_on_runtimedep.jar");

    JavaCompileAction javacAction =
        (JavaCompileAction) getGeneratingAction(dependsOnRuntimeDep.getArtifact());
    // Direct jars should NOT include import_runtime.jar
    assertThat(prettyArtifactNames(getInputs(javacAction, getDirectJars(javacAction))))
        .containsExactly(
            "java/com/google/runtimetest/_ijar/import_dep/java/com/google/runtimetest/import_compile-ijar.jar",
            "java/com/google/runtimetest/liblibrary_dep-hjar.jar");
  }

  @Test
  public void testDuplicateJars() throws Exception {
    checkError(
        "ji",
        "ji-with-dupe",
        // error:
        "Label '//ji:a.jar' is duplicated in the 'jars' attribute of rule 'ji-with-dupe'",
        // build file
        "filegroup(name='jars', srcs=['a.jar'])",
        "java_import(name = 'ji-with-dupe', jars = ['a.jar', 'a.jar'])");
  }

  @Test
  public void testDuplicateJarsThroughFilegroup() throws Exception {
    checkError(
        "ji",
        "ji-with-dupe-through-fg",
        // error:
        "in jars attribute of java_import rule //ji:ji-with-dupe-through-fg: a.jar is a duplicate",
        // build file
        "filegroup(name='jars', srcs=['a.jar'])",
        "java_import(name = 'ji-with-dupe-through-fg', jars = ['a.jar', ':jars'])");
  }

  @Test
  public void testExposesJavaProvider() throws Exception {
    ConfiguredTarget jarLib = getConfiguredTarget("//java/jarlib:libraryjar");
    JavaCompilationArgsProvider compilationArgsProvider =
        JavaInfo.getProvider(JavaCompilationArgsProvider.class, jarLib);
    assertThat(prettyArtifactNames(compilationArgsProvider.getRuntimeJars()))
        .containsExactly("java/jarlib/library.jar");
  }

  @Test
  public void testIjarCanBeDisabled() throws Exception {
    useConfiguration("--nouse_ijars");
    ConfiguredTarget lib =
        scratchConfiguredTarget(
            "java/a",
            "a",
            "java_library(name='a', srcs=['A.java'], deps=[':b'])",
            "java_import(name='b', jars=['b.jar'])");
    List<String> jars =
        ActionsTestUtil.baseArtifactNames(
            JavaInfo.getProvider(JavaCompilationArgsProvider.class, lib)
                .getTransitiveCompileTimeJars());
    assertThat(jars).doesNotContain("b-ijar.jar");
    assertThat(jars).contains("b.jar");
  }

  @Test
  public void testExports() throws Exception {
    useConfiguration("--incompatible_disallow_java_import_exports");
    checkError(
        "ugly",
        "jar",
        "java_import.exports is no longer supported; use java_import.deps instead",
        "java_library(name = 'dep', srcs = ['dep.java'])",
        "java_import(name = 'jar',",
        "    jars = ['dummy.jar'],",
        "    exports = [':dep'])");
  }
}
