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

import static com.google.devtools.build.lib.packages.Attribute.attr;
import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST;
import static com.google.devtools.build.lib.packages.BuildType.OUTPUT_LIST;
import static com.google.devtools.build.lib.packages.Type.INTEGER;
import static com.google.devtools.build.lib.packages.Type.STRING_LIST;

import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.analysis.BaseRuleClasses;
import com.google.devtools.build.lib.analysis.CommonPrerequisiteValidator;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.PlatformConfiguration;
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.TemplateVariableInfo;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.ToolchainTypeRequirement;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.rules.config.ConfigRules;
import com.google.devtools.build.lib.rules.core.CoreRules;
import com.google.devtools.build.lib.rules.platform.PlatformRules;
import com.google.devtools.build.lib.util.FileTypeSet;
import java.lang.reflect.Method;
import java.util.Map;
import javax.annotation.Nullable;
import net.starlark.java.syntax.Location;

/** Helper class to provide a RuleClassProvider for tests. */
public class TestRuleClassProvider {

  private static ConfiguredRuleClassProvider ruleClassProvider = null;
  private static ConfiguredRuleClassProvider ruleClassProviderWithClearedSuffix = null;

  private TestRuleClassProvider() {}

  /** Adds all the rule classes supported internally within the build tool to the given builder. */
  public static void addStandardRules(ConfiguredRuleClassProvider.Builder builder) {
    try {
      Class<?> providerClass = Class.forName(TestConstants.TEST_RULE_CLASS_PROVIDER);
      Method setupMethod =
          providerClass.getMethod("setup", ConfiguredRuleClassProvider.Builder.class);
      setupMethod.invoke(null, builder);
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }
  }

  private static ConfiguredRuleClassProvider createRuleClassProvider(boolean clearSuffix) {
    ConfiguredRuleClassProvider.Builder builder = new ConfiguredRuleClassProvider.Builder();
    addStandardRules(builder);
    // TODO(b/174773026): Eliminate TestingDummyRule/MockToolchainRule from this class, push them
    // down into the tests that use them. It's better for tests to avoid spooky mocks at a distance.
    // The same might also be said for the cleared-workspace variant of getRuleClassProvider(). If
    // we eliminate both, TestRuleClassProvider probably doesn't need to exist anymore.
    builder.addRuleDefinition(new TestingDummyRule());
    builder.addRuleDefinition(new MockToolchainRule());
    if (clearSuffix) {
      builder.clearWorkspaceFileSuffixForTesting();
    }
    return builder.build();
  }

  /** Returns a rule class provider. */
  public static ConfiguredRuleClassProvider getRuleClassProvider() {
    if (ruleClassProvider == null) {
      ruleClassProvider = createRuleClassProvider(false);
    }
    return ruleClassProvider;
  }

  /** Returns a rule class provider with the workspace suffix cleared. */
  public static ConfiguredRuleClassProvider getRuleClassProviderWithClearedSuffix() {
    if (ruleClassProviderWithClearedSuffix == null) {
      ruleClassProviderWithClearedSuffix = createRuleClassProvider(true);
    }
    return ruleClassProviderWithClearedSuffix;
  }

  // TODO(bazel-team): The logic for the "minimal" rule class provider is currently split between
  // TestRuleClassProvider and BuiltinsInjectionTest's overrides of BuildViewTestCase setup helpers.
  // Consider refactoring this together into one place as a new MinimalAnalysisMock.
  /**
   * Adds a few essential rules to a builder, such that it is usable but does not contain all the
   * rule classes known to the production environment.
   */
  public static void addMinimalRules(ConfiguredRuleClassProvider.Builder builder) {
    // TODO(bazel-team): See also TrimmableTestConfigurationFragments#installFragmentsAndNativeRules
    // for alternative/additional setup. Consider factoring that one to use this method.
    builder
        .setToolsRepository(RepositoryName.MAIN)
        .setRunfilesPrefix("test")
        .setPrerequisiteValidator(new MinimalPrerequisiteValidator());
    CoreRules.INSTANCE.init(builder);
    builder.addConfigurationOptions(CoreOptions.class);
    PlatformRules.INSTANCE.init(builder);
    ConfigRules.INSTANCE.init(builder);
  }

  private static class MinimalPrerequisiteValidator extends CommonPrerequisiteValidator {
    @Override
    public boolean isSameLogicalPackage(
        PackageIdentifier thisPackage, PackageIdentifier prerequisitePackage) {
      return thisPackage.equals(prerequisitePackage);
    }

    @Override
    protected boolean packageUnderExperimental(PackageIdentifier packageIdentifier) {
      return false;
    }

    @Override
    protected boolean checkVisibilityForExperimental(RuleContext.Builder context) {
      // It does not matter whether we return true or false here if packageUnderExperimental always
      // returns false.
      return true;
    }

    @Override
    protected boolean allowExperimentalDeps(RuleContext.Builder context) {
      // It does not matter whether we return true or false here if packageUnderExperimental always
      // returns false.
      return false;
    }
  }

  /** A dummy rule with some dummy attributes. */
  public static final class TestingDummyRule implements RuleDefinition {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
      return builder
          .setUndocumented()
          .add(attr("srcs", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))
          .add(attr("outs", OUTPUT_LIST))
          .add(attr("dummystrings", STRING_LIST))
          .add(attr("dummyinteger", INTEGER))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return RuleDefinition.Metadata.builder()
          .name("testing_dummy_rule")
          .ancestors(BaseRuleClasses.NativeActionCreatingRule.class)
          .factoryClass(UnknownRuleConfiguredTarget.class)
          .build();
    }
  }

  /** Stub rule to test Make variable expansion. */
  public static final class MakeVariableTester implements RuleConfiguredTargetFactory {

    @Override
    @Nullable
    public ConfiguredTarget create(RuleContext ruleContext)
        throws InterruptedException, RuleErrorException, ActionConflictException {
      Map<String, String> variables = ruleContext.attributes().get("variables", Type.STRING_DICT);
      return new RuleConfiguredTargetBuilder(ruleContext)
          .setFilesToBuild(NestedSetBuilder.emptySet(Order.STABLE_ORDER))
          .addProvider(RunfilesProvider.EMPTY)
          .addNativeDeclaredProvider(
              new TemplateVariableInfo(ImmutableMap.copyOf(variables), Location.BUILTIN))
          .build();
    }
  }

  /** Definition of a stub rule to test Make variable expansion. */
  public static final class MakeVariableTesterRule implements RuleDefinition {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment environment) {
      return builder
          .advertiseProvider(TemplateVariableInfo.class)
          .add(attr("variables", Type.STRING_DICT))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return Metadata.builder()
          .name("make_variable_tester")
          .ancestors(
              BaseRuleClasses.NativeBuildRule.class,
              BaseRuleClasses.MakeVariableExpandingRule.class)
          .factoryClass(MakeVariableTester.class)
          .build();
    }
  }

  /** A mock rule that requires a toolchain. */
  public static class MockToolchainRule implements RuleDefinition {
    @Override
    public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env) {
      return builder
          .requiresConfigurationFragments(PlatformConfiguration.class)
          .addToolchainTypes(
              ToolchainTypeRequirement.create(
                  Label.parseAbsoluteUnchecked("//toolchain:test_toolchain")))
          .build();
    }

    @Override
    public Metadata getMetadata() {
      return RuleDefinition.Metadata.builder()
          .name("mock_toolchain_rule")
          .factoryClass(UnknownRuleConfiguredTarget.class)
          .ancestors(BaseRuleClasses.NativeActionCreatingRule.class)
          .build();
    }
  }
}
