// Copyright 2020 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.packages;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.analysis.util.MockRule;
import com.google.devtools.build.lib.packages.BazelStarlarkEnvironment.InjectionException;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import net.starlark.java.eval.Structure;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for PackageFactory's management of the predeclared Starlark symbols. */
@RunWith(JUnit4.class)
public final class BazelStarlarkEnvironmentTest extends BuildViewTestCase {

  private static final MockRule OVERRIDABLE_RULE = () -> MockRule.define("overridable_rule");

  private BazelStarlarkEnvironment starlarkEnv;

  @Before
  public void setUp() {
    this.starlarkEnv = ruleClassProvider.getBazelStarlarkEnvironment();
  }

  @Override
  protected ConfiguredRuleClassProvider createRuleClassProvider() {
    // Add a fake rule and top-level symbol to override.
    ConfiguredRuleClassProvider.Builder builder =
        new ConfiguredRuleClassProvider.Builder()
            // While reading, feel free to mentally substitute overridable_rule -> cc_library and
            // overridable_symbol -> CcInfo.
            .addRuleDefinition(OVERRIDABLE_RULE)
            .addBzlToplevel("overridable_symbol", "original_value")
            .addBzlToplevel("another_overridable_symbol", "another_original_value");
    TestRuleClassProvider.addStandardRules(builder);
    return builder.build();
  }

  // TODO(#11954): We want BUILD- and WORKSPACE-loaded bzl files to have the exact same environment.
  // In the meantime these two tests help avoid regressions.

  // This property is important for BzlCompileFunction, which relies on the symbol names in the env
  // matching even if the symbols themselves differ.
  @Test
  public void buildAndWorkspaceBzlEnvsDeclareSameNames() throws Exception {
    Set<String> buildBzlNames = starlarkEnv.getUninjectedBuildBzlEnv().keySet();
    Set<String> workspaceBzlNames = starlarkEnv.getUninjectedWorkspaceBzlEnv().keySet();
    assertThat(buildBzlNames).isEqualTo(workspaceBzlNames);
  }

  @Test
  public void buildAndWorkspaceBzlEnvsAreSameExceptForNative() throws Exception {
    Map<String, Object> buildBzlEnv = new HashMap<>();
    buildBzlEnv.putAll(starlarkEnv.getUninjectedBuildBzlEnv());
    buildBzlEnv.remove("native");
    Map<String, Object> workspaceBzlEnv = new HashMap<>();
    workspaceBzlEnv.putAll(starlarkEnv.getUninjectedWorkspaceBzlEnv());
    workspaceBzlEnv.remove("native");
    assertThat(buildBzlEnv).isEqualTo(workspaceBzlEnv);
  }

  @Test
  public void builtinsBzlEnv() throws Exception {
    ImmutableMap<String, Object> env = starlarkEnv.getBuiltinsBzlEnv();
    // Can see general toplevel symbols.
    assertThat(env).containsKey("rule");
    // Cannot see rule-specific toplevel symbols.
    assertThat(env).doesNotContainKey("overridable_symbol");
    // Has the special builtins-internal module.
    assertThat(env).containsKey("_builtins");
  }

  /**
   * Asserts that injection for a BUILD-loaded .bzl file fails, using the given maps and expecting
   * the given error substring. The overrides list is empty.
   */
  private void assertBuildBzlInjectionFailure(
      Map<String, Object> exportedToplevels, Map<String, Object> exportedRules, String message) {
    InjectionException ex =
        assertThrows(
            InjectionException.class,
            () ->
                starlarkEnv.createBuildBzlEnvUsingInjection(
                    exportedToplevels, exportedRules, /*overridesList=*/ ImmutableList.of()));
    assertThat(ex).hasMessageThat().contains(message);
  }

  /**
   * Asserts that injection for a BUILD file fails, using the given map and expecting the given
   * error substring. The overrides list is empty.
   */
  private void assertBuildInjectionFailure(Map<String, Object> exportedRules, String message) {
    InjectionException ex =
        assertThrows(
            InjectionException.class,
            () ->
                starlarkEnv.createBuildEnvUsingInjection(
                    exportedRules, /*overridesList=*/ ImmutableList.of()));
    assertThat(ex).hasMessageThat().contains(message);
  }

  @Test
  public void buildBzlInjection() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of("overridable_symbol", "new_value"),
            ImmutableMap.of("overridable_rule", "new_rule"),
            /*overridesList=*/ ImmutableList.of());
    assertThat(env).containsEntry("overridable_symbol", "new_value");
    assertThat(((Structure) env.get("native")).getValue("overridable_rule")).isEqualTo("new_rule");
  }

  @Test
  public void buildInjection() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildEnvUsingInjection(
            ImmutableMap.of("overridable_rule", "new_rule"), /*overridesList=*/ ImmutableList.of());
    assertThat(env).containsEntry("overridable_rule", "new_rule");
  }

  @Test
  public void injectedNameMustOverrideExistingName_toplevel() throws Exception {
    assertBuildBzlInjectionFailure(
        ImmutableMap.of("brand_new_toplevel", "foo"),
        ImmutableMap.of(),
        "Injected top-level symbol 'brand_new_toplevel' must override an existing one by that"
            + " name");
  }

  @Test
  public void injectedNameMustOverrideExistingName_rule() throws Exception {
    assertBuildBzlInjectionFailure(
        ImmutableMap.of(),
        ImmutableMap.of("brand_new_rule", "foo"),
        "Injected rule 'brand_new_rule' must override an existing one by that name");
    assertBuildInjectionFailure(
        ImmutableMap.of("brand_new_rule", "foo"),
        "Injected rule 'brand_new_rule' must override an existing one by that name");
  }

  @Test
  public void cannotInjectGeneralSymbol_toplevel() {
    assertBuildBzlInjectionFailure(
        ImmutableMap.of("provider", "new_builtin"),
        ImmutableMap.of(),
        "Cannot override 'provider' with an injected top-level symbol");
  }

  @Test
  public void cannotInjectGeneralSymbol_nativeField() {
    // (Native field for bzl files, toplevel for BUILD files.)
    assertBuildBzlInjectionFailure(
        ImmutableMap.of(),
        ImmutableMap.of("glob", "new_builtin"),
        "Cannot override 'glob' with an injected rule");
    assertBuildInjectionFailure(
        ImmutableMap.of("glob", "new_builtin"), "Cannot override 'glob' with an injected rule");
  }

  @Test
  public void cannotInjectGeneralSymbol_nativeModuleItself() {
    assertBuildBzlInjectionFailure(
        ImmutableMap.of("native", "new_builtin"),
        ImmutableMap.of(),
        "Cannot override 'native' with an injected top-level symbol");
  }

  @Test
  public void cannotInjectGeneralSymbol_universe() {
    assertBuildBzlInjectionFailure(
        ImmutableMap.of("len", "new_builtin"),
        ImmutableMap.of(),
        "Cannot override 'len' with an injected top-level symbol");
    assertBuildInjectionFailure(
        ImmutableMap.of("len", "new_builtin"), "Cannot override 'len' with an injected rule");
  }

  @Test
  public void injectionStatus_respectsDefault() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of(
                "+overridable_symbol",
                "new_value",
                "-another_overridable_symbol",
                "another_new_value"),
            ImmutableMap.of("-overridable_rule", "new_rule"),
            /*overridesList=*/ ImmutableList.of());
    assertThat(env).containsEntry("overridable_symbol", "new_value");
    assertThat(env).containsEntry("another_overridable_symbol", "another_original_value");
    // Match the original rule's toString since the actual specific object is not easily accessible.
    Object overridableRuleValue = ((Structure) env.get("native")).getValue("overridable_rule");
    assertThat(overridableRuleValue.toString()).contains("overridable_rule");
  }

  @Test
  public void injectionStatus_canBeOverridden() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of(
                "+overridable_symbol",
                "new_value",
                "-another_overridable_symbol",
                "another_new_value"),
            ImmutableMap.of("-overridable_rule", "new_rule"),
            ImmutableList.of(
                "-overridable_symbol", "+another_overridable_symbol", "+overridable_rule"));
    assertThat(env).containsEntry("overridable_symbol", "original_value");
    assertThat(env).containsEntry("another_overridable_symbol", "another_new_value");
    assertThat(((Structure) env.get("native")).getValue("overridable_rule")).isEqualTo("new_rule");
  }

  @Test
  public void injectionStatus_cannotBeOverriddenForUnprefixedKeys() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of(
                "overridable_symbol",
                "new_value",
                "another_overridable_symbol",
                "another_new_value"),
            ImmutableMap.of(),
            ImmutableList.of("+overridable_symbol", "-another_overridable_symbol"));
    // Both the + and - are no-ops since the keys aren't prefixed.
    assertThat(env).containsEntry("overridable_symbol", "new_value");
    assertThat(env).containsEntry("another_overridable_symbol", "another_new_value");
  }

  @Test
  public void injectionStatus_overridingUnknownKeysIsNoop() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of("-overridable_symbol", "new_value"),
            ImmutableMap.of(),
            ImmutableList.of("+overridable_symbol", "+unknown_symbol", "-another_unknown_symbol"));
    // Both the + and - are no-ops since the keys aren't prefixed.
    assertThat(env).containsEntry("overridable_symbol", "new_value");
  }

  @Test
  public void injectionStatus_lastOverrideTakesPrecedence() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildBzlEnvUsingInjection(
            ImmutableMap.of("-overridable_symbol", "new_value"),
            ImmutableMap.of(),
            ImmutableList.of("+overridable_symbol", "-overridable_symbol"));
    // Both the + and - are no-ops since the keys aren't prefixed.
    assertThat(env).containsEntry("overridable_symbol", "original_value");
  }

  @Test
  public void injectionStatus_invalidOverrideItem_empty() throws Exception {
    InjectionException ex =
        assertThrows(
            InjectionException.class,
            () ->
                starlarkEnv.createBuildBzlEnvUsingInjection(
                    ImmutableMap.of(), ImmutableMap.of(), ImmutableList.of("")));
    assertThat(ex).hasMessageThat().contains("Invalid injection override item: ''");
  }

  @Test
  public void injectionStatus_invalidOverrideItem_unprefixed() throws Exception {
    InjectionException ex =
        assertThrows(
            InjectionException.class,
            () ->
                starlarkEnv.createBuildBzlEnvUsingInjection(
                    ImmutableMap.of(), ImmutableMap.of(), ImmutableList.of("foo")));
    assertThat(ex).hasMessageThat().contains("Invalid injection override item: 'foo'");
  }

  @Test
  public void injectionStatus_appliesToBuildFiles() throws Exception {
    Map<String, Object> env =
        starlarkEnv.createBuildEnvUsingInjection(
            ImmutableMap.of("+overridable_rule", "new_rule"),
            ImmutableList.of("-overridable_rule"));
    // Match the original rule's toString since the actual specific object is not easily accessible.
    assertThat(env.get("overridable_rule").toString()).contains("overridable_rule");
  }
}
