// Copyright 2018 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.analysis.util;

import static com.google.common.collect.ImmutableMap.toImmutableMap;

import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.actions.ActionLookupKey;
import com.google.devtools.build.lib.actions.ArtifactFactory;
import com.google.devtools.build.lib.actions.BuildFailedException;
import com.google.devtools.build.lib.actions.PackageRoots;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
import com.google.devtools.build.lib.analysis.AnalysisOptions;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.BlazeDirectories;
import com.google.devtools.build.lib.analysis.BuildView;
import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ConfiguredTargetFactory;
import com.google.devtools.build.lib.analysis.ConfiguredTargetValue;
import com.google.devtools.build.lib.analysis.DependencyKind;
import com.google.devtools.build.lib.analysis.DependencyResolutionHelpers;
import com.google.devtools.build.lib.analysis.ExecGroupCollection.InvalidExecGroupException;
import com.google.devtools.build.lib.analysis.InconsistentAspectOrderException;
import com.google.devtools.build.lib.analysis.ResolvedToolchainContext;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
import com.google.devtools.build.lib.analysis.ToolchainCollection;
import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.ConfigConditions;
import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
import com.google.devtools.build.lib.analysis.constraints.IncompatibleTargetChecker.IncompatibleTargetException;
import com.google.devtools.build.lib.analysis.starlark.StarlarkTransition;
import com.google.devtools.build.lib.analysis.test.CoverageReportActionFactory;
import com.google.devtools.build.lib.bugreport.BugReporter;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.NoSuchTargetException;
import com.google.devtools.build.lib.packages.PackageSpecification;
import com.google.devtools.build.lib.packages.PackageSpecification.PackageGroupContents;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.runtime.QuiescingExecutorsImpl;
import com.google.devtools.build.lib.skyframe.AspectKeyCreator.AspectKey;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetEvaluationExceptions.ReportedException;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetEvaluationExceptions.UnreportedException;
import com.google.devtools.build.lib.skyframe.ConfiguredTargetKey;
import com.google.devtools.build.lib.skyframe.DependencyResolver;
import com.google.devtools.build.lib.skyframe.SkyFunctionEnvironmentForTesting;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.SkyframeBuildView;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.skyframe.StarlarkBuiltinsValue;
import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue;
import com.google.devtools.build.lib.skyframe.toolchains.ToolchainException;
import com.google.devtools.build.lib.skyframe.toolchains.UnloadedToolchainContext;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import com.google.devtools.build.skyframe.InMemoryGraph;
import com.google.devtools.build.skyframe.NodeEntry;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.Version;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import net.starlark.java.eval.Mutability;

/**
 * A util class that contains all the helper stuff previously in BuildView that only exists to give
 * tests access to Skyframe internals. The code largely predates the introduction of Skyframe, and
 * mostly exists to avoid having to rewrite our tests to work with Skyframe natively.
 */
public class BuildViewForTesting {
  private final BuildView buildView;
  private final SkyframeExecutor skyframeExecutor;
  private final SkyframeBuildView skyframeBuildView;

  private final ConfiguredRuleClassProvider ruleClassProvider;

  private ImmutableMap<ActionLookupKey, Version> currentActionLookupKeys = ImmutableMap.of();

  /**
   * Tracks keys that mismatched at a previous diff computation.
   *
   * <p>{@link #populateActionLookupKeyMapAndGetDiff} scans the entire graph and computes a diff
   * against the previous {@link #currentActionLookupKeys} value. For this to be consistent with
   * {@link SkyframeExecutor#getEvaluatedCounts} it needs to filter out {@link
   * SkyFunctions#CONFIGURED_TARGET} nodes that do not own the underlying {@link
   * ConfiguredTargetValue}s. The owners have {@link ConfiguredTargetKey#getConfigurationKey} values
   * matching the {@link ConfiguredTarget#getConfigurationKey} values.
   *
   * <p>The problem is that the Skyframe graph may contain entries that are not done at the time of
   * graph inspection. This may occur when there's an incremental evaluation that doesn't require a
   * previously computed value.
   *
   * <p>If the {@link ConfiguredTargetValue} is unavailable and can't be compared, the diff still
   * needs to decide whether to skip it. If it was skipped previously, it needs to be skipped again.
   * Otherwise it'll show up as a newly evaluated node.
   */
  private ImmutableSet<ConfiguredTargetKey> previousProxyNodeKeys = ImmutableSet.of();

  public BuildViewForTesting(
      BlazeDirectories directories,
      ConfiguredRuleClassProvider ruleClassProvider,
      SkyframeExecutor skyframeExecutor,
      CoverageReportActionFactory coverageReportActionFactory) {
    this.buildView =
        new BuildView(
            directories, ruleClassProvider, skyframeExecutor, coverageReportActionFactory);
    this.ruleClassProvider = ruleClassProvider;
    this.skyframeExecutor = Preconditions.checkNotNull(skyframeExecutor);
    this.skyframeBuildView = skyframeExecutor.getSkyframeBuildView();
  }

  Set<ActionLookupKey> getSkyframeEvaluatedActionLookupKeyCountForTesting() {
    Set<ActionLookupKey> actionLookupKeys = populateActionLookupKeyMapAndGetDiff();
    Preconditions.checkState(
        actionLookupKeys.size() == skyframeBuildView.getEvaluatedCounts().total(),
        "Number of newly evaluated action lookup values %s does not agree with number that changed"
            + " in graph: %s. Keys: %s",
        actionLookupKeys.size(),
        skyframeBuildView.getEvaluatedCounts().total(),
        actionLookupKeys);
    return actionLookupKeys;
  }

  private Set<ActionLookupKey> populateActionLookupKeyMapAndGetDiff() {
    InMemoryGraph graph = skyframeExecutor.getEvaluator().getInMemoryGraph();
    var proxyNodeKeys = ImmutableSet.<ConfiguredTargetKey>builder();
    ImmutableMap<ActionLookupKey, Version> newMap =
        graph.getAllNodeEntries().stream()
            .filter(
                entry -> {
                  SkyKey key = entry.getKey();
                  if (!(key instanceof ActionLookupKey)) {
                    return false;
                  }
                  if (!key.functionName().equals(SkyFunctions.CONFIGURED_TARGET)) {
                    return true;
                  }

                  var ctKey = (ConfiguredTargetKey) key;

                  if (!entry.isDone()) {
                    if (previousProxyNodeKeys.contains(ctKey)) {
                      // The node is dirty and was a proxy previously. Filters the entry as long as
                      // it remains not done.
                      proxyNodeKeys.add(ctKey);
                      return false;
                    }
                    return true;
                  }

                  var value = (ConfiguredTargetValue) entry.getValue();
                  if (value == null) {
                    // The node has an error. No filtering is applied in this case.
                    return true;
                  }
                  if (!Objects.equals(
                      ctKey.getConfigurationKey(),
                      value.getConfiguredTarget().getConfigurationKey())) {
                    // The configurations are not equal so the node is only performing delegation
                    // and doesn't own the configured target.
                    proxyNodeKeys.add(ctKey);
                    return false;
                  }
                  return true;
                })
            .collect(toImmutableMap(e -> (ActionLookupKey) e.getKey(), NodeEntry::getVersion));
    previousProxyNodeKeys = proxyNodeKeys.build();
    MapDifference<ActionLookupKey, Version> difference =
        Maps.difference(newMap, currentActionLookupKeys);
    currentActionLookupKeys = newMap;
    return Sets.union(
        difference.entriesDiffering().keySet(), difference.entriesOnlyOnLeft().keySet());
  }

  /** Returns whether the given configured target has errors. */
  public boolean hasErrors(ConfiguredTarget configuredTarget) {
    return configuredTarget == null;
  }

  @ThreadCompatible
  public AnalysisResult update(
      TargetPatternPhaseValue loadingResult,
      BuildOptions targetOptions,
      ImmutableSet<Label> explicitTargetPatterns,
      List<String> aspects,
      ImmutableMap<String, String> aspectsParameters,
      AnalysisOptions viewOptions,
      boolean keepGoing,
      int loadingPhaseThreads,
      TopLevelArtifactContext topLevelOptions,
      ExtendedEventHandler eventHandler,
      EventBus eventBus)
      throws ViewCreationFailedException, InterruptedException, InvalidConfigurationException,
          BuildFailedException, TestExecException, AbruptExitException {
    populateActionLookupKeyMapAndGetDiff();
    return buildView.update(
        loadingResult,
        targetOptions,
        explicitTargetPatterns,
        aspects,
        aspectsParameters,
        viewOptions,
        keepGoing,
        /* skipIncompatibleExplicitTargets= */ false,
        /* checkForActionConflicts= */ true,
        QuiescingExecutorsImpl.forTesting(),
        topLevelOptions,
        /* reportIncompatibleTargets= */ true,
        eventHandler,
        eventBus,
        BugReporter.defaultInstance(),
        /* includeExecutionPhase= */ false,
        /* skymeldAnalysisOverlapPercentage= */ 0,
        /* resourceManager= */ null,
        /* buildResultListener= */ null,
        /* executionSetupCallback= */ null,
        /* buildConfigurationsCreatedCallback= */ null,
        /* buildDriverKeyTestContext= */ null);
  }

  /** Sets the configuration. Not thread-safe. */
  public void setConfigurationForTesting(
      EventHandler eventHandler, BuildConfigurationValue configuration) {
    try {
      skyframeBuildView.setConfiguration(
          eventHandler,
          configuration,
          /* maxDifferencesToShow= */ -1, /* allowAnalysisCacheDiscards */
          true);
    } catch (InvalidConfigurationException e) {
      throw new UnsupportedOperationException(
          "InvalidConfigurationException was thrown and caught during a test, "
              + "this case is not yet handled",
          e);
    }
  }

  public ArtifactFactory getArtifactFactory() {
    return skyframeBuildView.getArtifactFactory();
  }

  /**
   * Sets the possible artifact roots in the artifact factory. This allows the factory to resolve
   * paths with unknown roots to artifacts.
   */
  public void setArtifactRoots(PackageRoots packageRoots) {
    getArtifactFactory().setPackageRoots(packageRoots.getPackageRootLookup());
  }

  public Collection<ConfiguredTarget> getDirectPrerequisitesForTesting(
      ExtendedEventHandler eventHandler, ConfiguredTarget ct)
      throws InterruptedException,
          DependencyResolutionHelpers.Failure,
          InvalidConfigurationException,
          InconsistentAspectOrderException,
          StarlarkTransition.TransitionException {
    return Collections2.transform(
        getConfiguredTargetAndDataDirectPrerequisitesForTesting(eventHandler, ct),
        ConfiguredTargetAndData::getConfiguredTarget);
  }

  protected Collection<ConfiguredTargetAndData>
      getConfiguredTargetAndDataDirectPrerequisitesForTesting(
          ExtendedEventHandler eventHandler, ConfiguredTarget configuredTarget)
          throws InterruptedException,
              DependencyResolutionHelpers.Failure,
              InvalidConfigurationException,
              InconsistentAspectOrderException,
              StarlarkTransition.TransitionException {
    DependencyResolver.State state =
        initializeDependencyResolverState(eventHandler, configuredTarget);
    DependencyResolver producer = runDependencyResolver(eventHandler, configuredTarget, state);
    return producer.getDepValueMap().values();
  }

  /**
   * Returns a configured target for the specified target and configuration. If the target in
   * question has a top-level rule class transition, that transition is applied in the returned
   * ConfiguredTarget.
   *
   * <p>Returns {@code null} if something goes wrong.
   */
  public ConfiguredTarget getConfiguredTargetForTesting(
      ExtendedEventHandler eventHandler, Label label, BuildConfigurationValue config)
      throws InvalidConfigurationException, InterruptedException {
    return skyframeExecutor.getConfiguredTargetForTesting(eventHandler, label, config);
  }

  ConfiguredTargetAndData getConfiguredTargetAndDataForTesting(
      ExtendedEventHandler eventHandler, Label label, BuildConfigurationValue config)
      throws InvalidConfigurationException, InterruptedException {
    return skyframeExecutor.getConfiguredTargetAndDataForTesting(eventHandler, label, config);
  }

  /**
   * Returns a RuleContext which is the same as the original RuleContext of the target parameter.
   */
  public RuleContext getRuleContextForTesting(
      ConfiguredTarget target, StoredEventHandler eventHandler)
      throws DependencyResolutionHelpers.Failure,
          InvalidConfigurationException,
          InterruptedException,
          InconsistentAspectOrderException,
          ToolchainException,
          StarlarkTransition.TransitionException,
          InvalidExecGroupException {
    BuildConfigurationValue targetConfig =
        skyframeExecutor.getConfiguration(eventHandler, target.getConfigurationKey());
    SkyFunction.Environment skyframeEnv =
        new SkyFunctionEnvironmentForTesting(eventHandler, skyframeExecutor);
    StarlarkBuiltinsValue starlarkBuiltinsValue =
        (StarlarkBuiltinsValue)
            Preconditions.checkNotNull(skyframeEnv.getValue(StarlarkBuiltinsValue.key()));
    CachingAnalysisEnvironment analysisEnv =
        new CachingAnalysisEnvironment(
            getArtifactFactory(),
            skyframeExecutor.getActionKeyContext(),
            ConfiguredTargetKey.builder()
                .setLabel(target.getLabel())
                .setConfiguration(targetConfig)
                .build(),
            targetConfig.extendedSanityChecks(),
            targetConfig.allowAnalysisFailures(),
            eventHandler,
            skyframeEnv,
            starlarkBuiltinsValue);
    return getRuleContextForTesting(eventHandler, target, analysisEnv);
  }

  /**
   * Creates and returns a rule context that is equivalent to the one that was used to create the
   * given configured target.
   */
  public RuleContext getRuleContextForTesting(
      ExtendedEventHandler eventHandler, ConfiguredTarget configuredTarget, AnalysisEnvironment env)
      throws DependencyResolutionHelpers.Failure,
          InvalidConfigurationException,
          InterruptedException,
          InconsistentAspectOrderException,
          ToolchainException,
          StarlarkTransition.TransitionException,
          InvalidExecGroupException {
    DependencyResolver.State state =
        initializeDependencyResolverState(eventHandler, configuredTarget);
    DependencyResolver producer = runDependencyResolver(eventHandler, configuredTarget, state);

    OrderedSetMultimap<DependencyKind, ConfiguredTargetAndData> prerequisiteMap =
        producer.getDepValueMap();

    Target target = state.targetAndConfiguration.getTarget();
    String targetDescription = target.toString();

    ToolchainCollection<UnloadedToolchainContext> unloadedToolchainCollection =
        producer.getUnloadedToolchainContexts();

    ToolchainCollection.Builder<ResolvedToolchainContext> resolvedToolchainContext =
        ToolchainCollection.builder();
    for (Map.Entry<String, UnloadedToolchainContext> unloadedToolchainContext :
        unloadedToolchainCollection.getContextMap().entrySet()) {
      ResolvedToolchainContext toolchainContext =
          ResolvedToolchainContext.load(
              unloadedToolchainContext.getValue(),
              targetDescription,
              ImmutableSet.copyOf(
                  prerequisiteMap.get(
                      DependencyKind.forExecGroup(unloadedToolchainContext.getKey()))));
      resolvedToolchainContext.addContext(unloadedToolchainContext.getKey(), toolchainContext);
    }

    return new RuleContext.Builder(
            env,
            target,
            /* aspects= */ ImmutableList.of(),
            state.targetAndConfiguration.getConfiguration())
        .setRuleClassProvider(ruleClassProvider)
        .setConfigurationFragmentPolicy(
            target.getAssociatedRule().getRuleClassObject().getConfigurationFragmentPolicy())
        .setActionOwnerSymbol(ConfiguredTargetKey.fromConfiguredTarget(configuredTarget))
        .setMutability(Mutability.create("configured target"))
        .setVisibility(
            NestedSetBuilder.create(
                Order.STABLE_ORDER,
                PackageGroupContents.create(ImmutableList.of(PackageSpecification.everything()))))
        .setPrerequisites(ConfiguredTargetFactory.transformPrerequisiteMap(prerequisiteMap))
        .setConfigConditions(ConfigConditions.EMPTY)
        .setToolchainContexts(resolvedToolchainContext.build())
        .setExecGroupCollectionBuilder(state.execGroupCollectionBuilder)
        .unsafeBuild();
  }

  private DependencyResolver runDependencyResolver(
      ExtendedEventHandler eventHandler,
      ConfiguredTarget configuredTarget,
      DependencyResolver.State state)
      throws InterruptedException {
    DependencyResolver producer = new DependencyResolver(state.targetAndConfiguration);
    try {
      if (!producer.evaluate(
          state,
          ConfiguredTargetKey.fromConfiguredTarget(configuredTarget),
          ruleClassProvider,
          skyframeBuildView.getStarlarkTransitionCache(),
          /* semaphoreLocker= */ () -> {},
          new SkyFunctionEnvironmentForTesting(eventHandler, skyframeExecutor),
          eventHandler)) {
        throw new IllegalStateException(configuredTarget + " should be already evaluated");
      }
    } catch (ReportedException | UnreportedException | IncompatibleTargetException e) {
      throw new IllegalStateException(e); // Should not be possible for done ConfiguredTarget.
    }
    return producer;
  }

  private DependencyResolver.State initializeDependencyResolverState(
      ExtendedEventHandler eventHandler, ConfiguredTarget configuredTarget)
      throws InterruptedException {
    // In production, the TargetAndConfiguration value is based on final configuration of the
    // ConfiguredTarget after any rule transition is applied.
    BuildConfigurationValue configuration =
        skyframeExecutor.getConfiguration(eventHandler, configuredTarget.getConfigurationKey());
    Target target;
    try {
      target =
          skyframeExecutor.getPackageManager().getTarget(eventHandler, configuredTarget.getLabel());
    } catch (NoSuchPackageException | NoSuchTargetException e) {
      eventHandler.handle(
          Event.error("Failed to get target when trying to get rule context for testing"));
      throw new IllegalStateException(e);
    }
    return DependencyResolver.State.createForTesting(
        new TargetAndConfiguration(target.getAssociatedRule(), configuration));
  }

  /** Clears the analysis cache as in --discard_analysis_cache. */
  void clearAnalysisCache(
      ImmutableSet<ConfiguredTarget> topLevelTargets, ImmutableSet<AspectKey> topLevelAspects) {
    skyframeBuildView.clearAnalysisCache(topLevelTargets, topLevelAspects);
  }
}
