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

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.Type.STRING_LIST;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.analysis.BaseRuleClasses;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.RuleDefinition;
import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.config.ExecutionTransitionFactory;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.packages.AspectDefinition;
import com.google.devtools.build.lib.packages.AspectParameters;
import com.google.devtools.build.lib.packages.NativeAspectClass;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.devtools.build.lib.util.FileTypeSet;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for the actions built by ProguardLibrary. */
@RunWith(JUnit4.class)
public class ProguardLibraryTest extends BuildViewTestCase {

  /** Fake rule for testing {@link ProguardLibrary#collectProguardSpecs()} with all fields. */
  public static final class StandardProguardLibraryRule
      implements RuleDefinition, RuleConfiguredTargetFactory {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
      return builder
          .add(attr("deps", LABEL_LIST).allowedFileTypes(FileTypeSet.NO_FILE))
          .add(attr("exports", LABEL_LIST).allowedFileTypes(FileTypeSet.NO_FILE))
          .add(attr("runtime_deps", LABEL_LIST).allowedFileTypes(FileTypeSet.NO_FILE))
          .add(
              attr("plugins", LABEL_LIST)
                  .cfg(ExecutionTransitionFactory.create())
                  .allowedFileTypes(FileTypeSet.NO_FILE))
          .add(
              attr("exported_plugins", LABEL_LIST)
                  .cfg(ExecutionTransitionFactory.create())
                  .allowedFileTypes(FileTypeSet.NO_FILE))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return RuleDefinition.Metadata.builder()
          .name("standard_proguard_library")
          .ancestors(BaseRuleClasses.NativeBuildRule.class, ProguardLibraryRule.class)
          .factoryClass(StandardProguardLibraryRule.class)
          .build();
    }

    @Override
    public ConfiguredTarget create(RuleContext ruleContext)
        throws InterruptedException, RuleErrorException, ActionConflictException {
      NestedSet<Artifact> proguardSpecs = new ProguardLibrary(ruleContext).collectProguardSpecs();
      return new RuleConfiguredTargetBuilder(ruleContext)
          .setFilesToBuild(proguardSpecs)
          .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
          .addNativeDeclaredProvider(new ProguardSpecProvider(proguardSpecs))
          .build();
    }
  }

  /**
   * Fake rule for testing {@link ProguardLibrary#collectProguardSpecs()} with custom/absent fields.
   */
  public static final class CustomProguardLibraryRule
      implements RuleDefinition, RuleConfiguredTargetFactory {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
      return builder
          .add(attr("target_libs", LABEL_LIST).allowedFileTypes(FileTypeSet.NO_FILE))
          .add(
              attr("host_libs", LABEL_LIST)
                  .cfg(ExecutionTransitionFactory.create())
                  .allowedFileTypes(FileTypeSet.NO_FILE))
          .add(attr("target_attrs", STRING_LIST))
          .add(attr("host_attrs", STRING_LIST))
          .add(
              attr("$implicit_target", LABEL)
                  .value(Label.parseAbsoluteUnchecked("//test/implicit:implicit_target")))
          .add(
              attr("$implicit_host", LABEL)
                  .cfg(ExecutionTransitionFactory.create())
                  .value(Label.parseAbsoluteUnchecked("//test/implicit:implicit_host")))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return RuleDefinition.Metadata.builder()
          .name("custom_proguard_library")
          .ancestors(BaseRuleClasses.NativeBuildRule.class, ProguardLibraryRule.class)
          .factoryClass(CustomProguardLibraryRule.class)
          .build();
    }

    @Override
    public ConfiguredTarget create(RuleContext ruleContext)
        throws ActionConflictException, InterruptedException {
      NestedSet<Artifact> proguardSpecs =
          new ProguardLibrary(ruleContext)
              .collectProguardSpecs(
                  new ImmutableSet.Builder<String>()
                      .addAll(ruleContext.attributes().get("target_attrs", STRING_LIST))
                      .addAll(ruleContext.attributes().get("host_attrs", STRING_LIST))
                      .build());

      return new RuleConfiguredTargetBuilder(ruleContext)
          .setFilesToBuild(proguardSpecs)
          .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
          .addNativeDeclaredProvider(new ProguardSpecProvider(proguardSpecs))
          .build();
    }
  }

  /** Fake rule for testing {@link ProguardLibrary#collectProguardSpecs()} with aspect fields. */
  public static final class AspectProguardLibraryRule
      implements RuleDefinition, RuleConfiguredTargetFactory {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
      return builder
          .add(
              attr("deps", LABEL_LIST)
                  .allowedFileTypes(FileTypeSet.NO_FILE)
                  .aspect(PROGUARD_LIBRARY_ASPECT))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return RuleDefinition.Metadata.builder()
          .name("aspect_proguard_library")
          .ancestors(BaseRuleClasses.NativeBuildRule.class)
          .factoryClass(AspectProguardLibraryRule.class)
          .build();
    }

    @Override
    public ConfiguredTarget create(RuleContext ruleContext)
        throws InterruptedException, RuleErrorException, ActionConflictException {
      NestedSet<Artifact> proguardSpecs = new ProguardLibrary(ruleContext).collectProguardSpecs();

      return new RuleConfiguredTargetBuilder(ruleContext)
          .setFilesToBuild(proguardSpecs)
          .addProvider(RunfilesProvider.EMPTY)
          .addNativeDeclaredProvider(new ProguardSpecProvider(proguardSpecs))
          .build();
    }
  }

  static final class ProguardLibraryAspect extends NativeAspectClass
      implements ConfiguredAspectFactory {
    @Override
    public AspectDefinition getDefinition(AspectParameters params) {
      return new AspectDefinition.Builder(this)
          .add(
              attr("$implicit_target", LABEL)
                  .value(Label.parseAbsoluteUnchecked("//test/implicit:implicit_target")))
          .build();
    }

    @Override
    public ConfiguredAspect create(
        ConfiguredTargetAndData ctadBase,
        RuleContext ruleContext,
        AspectParameters params,
        RepositoryName toolsRepository)
        throws ActionConflictException, InterruptedException {
      NestedSet<Artifact> proguardSpecs =
          new ProguardLibrary(ruleContext)
              .collectProguardSpecs(ImmutableSet.of("$implicit_target"));

      return new ConfiguredAspect.Builder(ruleContext)
          .addNativeDeclaredProvider(new ProguardSpecProvider(proguardSpecs))
          .build();
    }
  }

  public static final ProguardLibraryAspect PROGUARD_LIBRARY_ASPECT = new ProguardLibraryAspect();

  @Before
  public final void createBuildFile() throws Exception {
    scratch.file(
        "test/implicit/BUILD",
        "standard_proguard_library(",
        "    name = 'implicit_target',",
        "    proguard_specs = ['implicit_target.cfg'])",
        "standard_proguard_library(",
        "    name = 'implicit_host',",
        "    proguard_specs = ['implicit_host.cfg'])");
  }

  /** Make the test rule class provider understand our rules in addition to the standard ones. */
  @Override
  protected ConfiguredRuleClassProvider createRuleClassProvider() {
    ConfiguredRuleClassProvider.Builder builder =
        new ConfiguredRuleClassProvider.Builder()
            .addRuleDefinition(new StandardProguardLibraryRule())
            .addRuleDefinition(new CustomProguardLibraryRule())
            .addRuleDefinition(new AspectProguardLibraryRule())
            .addNativeAspectClass(PROGUARD_LIBRARY_ASPECT);
    TestRuleClassProvider.addStandardRules(builder);
    return builder.build();
  }

  @Test
  public void testProguardSpecs_outputsAllowlistedVersions() throws Exception {
    scratch.file(
        "test/BUILD",
        "standard_proguard_library(",
        "    name = 'lib',",
        "    proguard_specs = ['optimizations.cfg', 'configs.cfg'])");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");

    assertThat(getFilesToBuild(target).toList())
        .containsExactly(
            getBinArtifact("validated_proguard/lib/test/optimizations.cfg_valid", target),
            getBinArtifact("validated_proguard/lib/test/configs.cfg_valid", target));
  }

  @Test
  public void testProguardSpecs_originalConfigSentAsInputToAllowlister() throws Exception {
    scratch.file(
        "test/BUILD",
        "standard_proguard_library(",
        "    name = 'lib',",
        "    proguard_specs = ['optimizations.cfg', 'configs.cfg'])");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");
    Artifact output = getBinArtifact("validated_proguard/lib/test/optimizations.cfg_valid", target);
    Artifact source = getFileConfiguredTarget("//test:optimizations.cfg").getArtifact();

    SpawnAction action = getGeneratingSpawnAction(output);

    assertThat(action).isNotNull();
    assertThat(action.getInputs().toList()).contains(source);

    List<String> inputs =
        action.getInputs().toList().stream()
            .map(Artifact::getExecPathString)
            // Strip the bazel-out/CONFIG/ portion of the paths.
            .map(path -> path.replaceFirst(TestConstants.PRODUCT_NAME + "-out/[^/]+/", ""))
            .collect(Collectors.toList());
    List<String> expectedFilesToRun =
        getFilesToRun(getConfiguredTarget(TestConstants.PROGUARD_ALLOWLISTER_TARGET))
            .toList()
            .stream()
            .map(Artifact::getExecPathString)
            // Strip the bazel-out/CONFIG/ portion of the paths.
            .map(path -> path.replaceFirst(TestConstants.PRODUCT_NAME + "-out/[^/]+/", ""))
            .collect(Collectors.toList());
    assertThat(inputs).containsAtLeastElementsIn(expectedFilesToRun);
  }

  @Test
  public void testProguardSpecs_allowlisterPathsPassedAsFlags() throws Exception {
    scratch.file(
        "test/BUILD",
        "standard_proguard_library(",
        "    name = 'lib',",
        "    proguard_specs = ['optimizations.cfg', 'configs.cfg'])");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");
    Artifact output = getBinArtifact("validated_proguard/lib/test/optimizations.cfg_valid", target);
    Artifact source = getFileConfiguredTarget("//test:optimizations.cfg").getArtifact();

    SpawnAction action = getGeneratingSpawnAction(output);

    assertContainsSublist(
        action.getArguments(), ImmutableList.of("--path", source.getExecPathString()));
    assertContainsSublist(
        action.getArguments(), ImmutableList.of("--output", output.getExecPathString()));
  }

  @Test
  public void testProguardSpecs_pickedUpFromDependencyAttributes() throws Exception {
    scratch.file(
        "test/BUILD",
        "standard_proguard_library(",
        "    name = 'lib',",
        "    deps = [':dep'],",
        "    exports = [':export'],",
        "    runtime_deps = [':runtime'],",
        "    plugins = [':plugin'],",
        "    exported_plugins = [':exported_plugin'])",
        "standard_proguard_library(name = 'dep', proguard_specs = ['dep.cfg'])",
        "standard_proguard_library(name = 'export', proguard_specs = ['export.cfg'])",
        "standard_proguard_library(name = 'runtime', proguard_specs = ['runtime.cfg'])",
        "standard_proguard_library(name = 'plugin', proguard_specs = ['plugin.cfg'])",
        "standard_proguard_library(",
        "    name = 'exported_plugin',",
        "    proguard_specs = ['exported_plugin.cfg']",
        ")");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");
    Artifact validatedDep =
        getBinArtifact(
            "validated_proguard/dep/test/dep.cfg_valid", getConfiguredTarget("//test:dep"));
    Artifact validatedExport =
        getBinArtifact(
            "validated_proguard/export/test/export.cfg_valid",
            getConfiguredTarget("//test:export"));
    Artifact validatedRuntimeDep =
        getBinArtifact(
            "validated_proguard/runtime/test/runtime.cfg_valid",
            getConfiguredTarget("//test:runtime"));
    Artifact validatedPlugin =
        getBinArtifact(
            "validated_proguard/plugin/test/plugin.cfg_valid",
            getDirectPrerequisite(target, "//test:plugin"));
    Artifact validatedExportedPlugin =
        getBinArtifact(
            "validated_proguard/exported_plugin/test/exported_plugin.cfg_valid",
            getDirectPrerequisite(target, "//test:exported_plugin"));

    assertThat(getFilesToBuild(target).toList())
        .containsExactly(
            validatedDep,
            validatedExport,
            validatedRuntimeDep,
            validatedPlugin,
            validatedExportedPlugin);
  }

  @Test
  public void testProguardSpecs_customAttributes() throws Exception {
    scratch.file(
        "test/BUILD",
        "custom_proguard_library(",
        "    name = 'lib',",
        "    target_libs = [':target'],",
        "    host_libs = [':host'],",
        "    target_attrs = ['target_libs', '$implicit_target'],",
        "    host_attrs = ['host_libs', '$implicit_host'])",
        "standard_proguard_library(name = 'target', proguard_specs = ['target.cfg'])",
        "standard_proguard_library(name = 'host', proguard_specs = ['host.cfg'])");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");
    Artifact validatedTarget =
        getBinArtifact(
            "validated_proguard/target/test/target.cfg_valid",
            getConfiguredTarget("//test:target"));
    Artifact validatedHost =
        getBinArtifact(
            "validated_proguard/host/test/host.cfg_valid",
            getDirectPrerequisite(target, "//test:host"));
    Artifact validatedImplicitTarget =
        getBinArtifact(
            "validated_proguard/implicit_target/test/implicit/implicit_target.cfg_valid",
            getConfiguredTarget("//test/implicit:implicit_target"));
    Artifact validatedImplicitHost =
        getBinArtifact(
            "validated_proguard/implicit_host/test/implicit/implicit_host.cfg_valid",
            getDirectPrerequisite(target, "//test/implicit:implicit_host"));

    assertThat(getFilesToBuild(target).toList())
        .containsExactly(
            validatedTarget, validatedHost, validatedImplicitTarget, validatedImplicitHost);
  }

  @Test
  public void testProguardSpecs_customAttributes_allowsAttributeMismatch() throws Exception {
    scratch.file(
        "test/BUILD",
        "custom_proguard_library(",
        "    name = 'lib',",
        "    target_libs = [':target'],",
        "    host_libs = [':host'],",
        "    target_attrs = ['deps', 'exports', 'runtime_deps'],",
        "    host_attrs = ['plugins', 'exported_plugins'])",
        "standard_proguard_library(name = 'target', proguard_specs = ['target.cfg'])",
        "standard_proguard_library(name = 'host', proguard_specs = ['host.cfg'])");

    // None of the attributes we have were specified, none of the attributes we specified exist.
    assertThat(getFilesToBuild(getConfiguredTarget("//test:lib")).toList()).isEmpty();
  }

  @Test
  public void testProguardSpecs_aspectAttributes_detectedByProguardLibrary() throws Exception {
    scratch.file(
        "test/BUILD",
        "aspect_proguard_library(",
        "    name = 'lib',",
        "    deps = [':child'])",
        "sh_library(name = 'child')");

    ConfiguredTarget target = getConfiguredTarget("//test:lib");

    Artifact validatedImplicitTarget =
        getBinArtifact(
            "validated_proguard/implicit_target/test/implicit/implicit_target.cfg_valid",
            getConfiguredTarget("//test/implicit:implicit_target"));

    // The only thing that should be picked up is the implicit target from the aspect.
    assertThat(getFilesToBuild(target).toList()).containsExactly(validatedImplicitTarget);
  }
}
