// 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.buildtool;

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

import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Tests use of the --target_environment flag. */
@RunWith(TestParameterInjector.class)
public class EnvironmentRestrictedBuildTest extends BuildIntegrationTestCase {
  @TestParameter boolean mergedSkyframeAnalysisExecution;

  @Before
  public final void addNoBuildOption() throws Exception  {
    if (mergedSkyframeAnalysisExecution) {
      // TODO(b/223761810): Add --nobuild after Skymeld supports it.
      addOptions("--experimental_merged_skyframe_analysis_execution");
    } else {
      addOptions("--nobuild"); // Target enforcement happens before the execution phase.
    }
  }

  private void writeEnvironmentRules(String... defaults) throws Exception {
    StringBuilder defaultsBuilder = new StringBuilder();
    for (String defaultEnv : defaults) {
      defaultsBuilder.append("'" + defaultEnv + "', ");
    }

    write("buildenv/BUILD",
        "environment_group(",
        "    name = 'group',",
        "    environments = [':one', ':two'],",
        "    defaults = [" + defaultsBuilder + "])",
        "environment(name = 'one')",
        "environment(name = 'two')");
  }

  @Test
  public void testTargetEnvironmentError() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");
    addOptions("--target_environment=//buildenv:one");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:bar declares compatibility with:\n"
                + "  []\n"
                + "but does not support:\n"
                + "  //buildenv:one");
  }

  @Test
  public void testTargetEnvironmentSuccess() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], compatible_with = ['//buildenv:one'])");
    write("foo/bar.sh");
    addOptions("--target_environment=//buildenv:one");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void testMultipleTargetEnvironments() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], compatible_with = ['//buildenv:one'])");

    addOptions("--target_environment=//buildenv:one", "--target_environment=//buildenv:two");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:bar declares compatibility with:\n"
                + "  [//buildenv:one]\n"
                + "but does not support:\n"
                + "  //buildenv:two");
  }

  @Test
  public void testTargetEnvironmentIsDefault() throws Exception {
    writeEnvironmentRules(":one");
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");
    write("foo/bar.sh");
    addOptions("--target_environment=//buildenv:one");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void testEmptyTargetEnvironment() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");
    write("foo/bar.sh");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void testOnlySomeTargetsQualify() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'good_bar', srcs = ['bar.sh'], compatible_with = ['//buildenv:one'])",
        "sh_library(name = 'bad_bar', srcs = ['bar.sh'], compatible_with = ['//buildenv:two'])");
    write("foo/bar.sh");
    addOptions("--target_environment=//buildenv:one");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:all")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:bad_bar declares compatibility with:\n"
                + "  [//buildenv:two]\n"
                + "but does not support:\n"
                + "  //buildenv:one");
  }

  @Test
  public void testNoConstraintEnforcement() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");
    write("foo/bar.sh");
    addOptions("--target_environment=//buildenv:one", "--noenforce_constraints");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void testFlagUsesNonexistentTarget() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");

    addOptions("--target_environment=//buildenv:nada");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar")))
        .hasMessageThat()
        .contains("invalid target environment: no such target '//buildenv:nada'");
  }

  @Test
  public void testFlagUsesWrongTargetType() throws Exception {
    write("foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'])");

    addOptions("--target_environment=//foo:bar");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar")))
        .hasMessageThat()
        .contains("//foo:bar is not a valid environment definition");
  }

  @Test
  public void testRefinedEnvironmentCheckValidTarget() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "config_setting(name = 'config_one', values = {'define': 'mode=one'})",
        "config_setting(name = 'config_two', values = {'define': 'mode=two'})",
        "sh_library(name = 'lib_one', srcs = [], compatible_with = ['//buildenv:one'])",
        "sh_library(name = 'lib_two', srcs = [], compatible_with = ['//buildenv:two'])",
        "sh_library(name = 'toplevel',",
        "    srcs = ['toplevel.sh'],",
        "    deps = select({",
        "        ':config_one': [':lib_one'],",
        "        ':config_two': [':lib_two'],",
        "    }),",
        "    compatible_with = ['//buildenv:one', '//buildenv:two'])");
    // "--define mode=one" refines :toplevel to (matching) ["//buildenv:one"]:
    addOptions("--target_environment=//buildenv:one", "--define", "mode=one");
    write("foo/toplevel.sh");
    buildTarget("//foo:toplevel");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void testRefinedEnvironmentCheckBadTarget() throws Exception {
    writeEnvironmentRules();
    write("foo/BUILD",
        "config_setting(name = 'config_one', values = {'define': 'mode=one'})",
        "config_setting(name = 'config_two', values = {'define': 'mode=two'})",
        "sh_library(name = 'lib_one', srcs = [], compatible_with = ['//buildenv:one'])",
        "sh_library(name = 'lib_two', srcs = [], compatible_with = ['//buildenv:two'])",
        "sh_library(name = 'toplevel',",
        "    srcs = ['toplevel.sh'],",
        "    deps = select({",
        "        ':config_one': [':lib_one'],",
        "        ':config_two': [':lib_two'],",
        "    }),",
        "    compatible_with = ['//buildenv:one', '//buildenv:two'])");
    // "--define mode=two" refines :toplevel to (non-matching) ["//buildenv:two"]:
    addOptions("--target_environment=//buildenv:one", "--define", "mode=two");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:toplevel")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:toplevel declares compatibility with:\n"
                + "  [//buildenv:one, //buildenv:two]\n"
                + "but does not support:\n"
                + "  environment: //buildenv:one\n"
                + "    removed by: //foo:toplevel");
  }

  @Test
  public void topLevelOutputFile() throws Exception {
    writeEnvironmentRules();
    write(
        "foo/rule.bzl",
        "def _impl(ctx):",
        "  file = ctx.actions.declare_file('libbar.a')",
        "  ctx.actions.write(file, 'hello')",
        "  return [DefaultInfo(files = depset([file]))]",
        "crule = rule(",
        "  _impl,",
        "  outputs = {",
        "    'archive': 'lib%{name}.a'",
        "  },",
        ");");
    write(
        "foo/BUILD",
        "load(':rule.bzl', 'crule')",
        "crule(name = 'bar', compatible_with = ['//buildenv:one'])");
    addOptions("--target_environment=//buildenv:one");
    buildTarget("//foo:libbar.a");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void topLevelAliasToCompatibleOutputFile() throws Exception {
    writeEnvironmentRules();
    write(
        "foo/BUILD",
        "genrule(",
        "    name = 'goodgen',",
        "    srcs = [],",
        "    cmd = 'touch $@',",
        "    outs = ['goodgen.out'],",
        "    compatible_with = ['//buildenv:one'])",
        "alias(",
        "    name = 'goodalias',",
        "    actual = 'goodgen.out')");
    addOptions("--target_environment=//buildenv:one");
    buildTarget("//foo:goodalias");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void topLevelAliasToBadOutputFile() throws Exception {
    writeEnvironmentRules();
    write(
        "foo/BUILD",
        "genrule(",
        "    name = 'badgen',",
        "    srcs = [],",
        "    cmd = '',",
        "    outs = ['badgen.out'])",
        "alias(",
        "    name = 'badalias',",
        "    actual = 'badgen.out')");
    addOptions("--target_environment=//buildenv:one");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:badalias")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:badgen.out declares compatibility with:\n"
                + "  []\n"
                + "but does not support:\n"
                + "  //buildenv:one");
  }

  @Test
  public void doesNotCheckDefaultEnvironments() throws Exception {
    write(
        "buildenv/a/BUILD",
        "environment_group(",
        "    name = 'a',",
        "    defaults = [':a1'],",
        "    environments = [':a1', ':a2'])",
        "environment(name = 'a1')",
        "environment(name = 'a2')");
    write(
        "buildenv/b/BUILD",
        "environment_group(",
        "    name = 'b',",
        "    defaults = [':b1'],",
        "    environments = [':b1', ':b2'])",
        "environment(name = 'b1')",
        "environment(name = 'b2')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/b:b2'])");

    addOptions("--target_environment=//buildenv/a:a1");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccess()).isTrue();
  }

  @Test
  public void autoTargetEnvironment_success() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/auto_cpu:k8'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus", "--cpu=k8");
    buildTarget("//foo:bar");
    ConfiguredTarget successful = Iterables.getOnlyElement(getResult().getSuccessfulTargets());
    assertThat(successful.getLabel().toString()).isEqualTo("//foo:bar");
  }

  @Test
  public void autoTargetEnvironment_cpuNotSet() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/auto_cpu:k8'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus");
    buildTarget("//foo:bar");
    ConfiguredTarget successful = Iterables.getOnlyElement(getResult().getSuccessfulTargets());
    assertThat(successful.getLabel().toString()).isEqualTo("//foo:bar");
  }

  @Test
  public void autoTargetEnvironment_disabled() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/auto_cpu:k8'])");

    addOptions("--build", "--auto_cpu_environment_group=", "--cpu=k8");
    buildTarget("//foo:bar");
    ConfiguredTarget successful = Iterables.getOnlyElement(getResult().getSuccessfulTargets());
    assertThat(successful.getLabel().toString()).isEqualTo("//foo:bar");
  }

  @Test
  public void autoTargetEnvironment_error() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");

    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/auto_cpu:k8'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus", "--cpu=ppc");
    buildTarget("//foo:bar");
    assertThat(getResult().getSuccessfulTargets()).isEmpty();
  }

  @Test
  public void autoTargetEnvironment_does_not_check_default_environments() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");
    write(
        "buildenv/b/BUILD",
        "environment_group(",
        "    name = 'b',",
        "    defaults = [':b1'],",
        "    environments = [':b1', ':b2'])",
        "environment(name = 'b1')",
        "environment(name = 'b2')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/b:b2'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus", "--cpu=k8");
    // Even though //foo:bar doesn't support the expected //buildenv/b:b1 (which is the default
    // for //buildenv/b), --auto_cpu_environment_group is only concerned about //buildenv/auto_cpu.
    buildTarget("//foo:bar");
    ConfiguredTarget successful = Iterables.getOnlyElement(getResult().getSuccessfulTargets());
    assertThat(successful.getLabel().toString()).isEqualTo("//foo:bar");
  }

  @Test
  public void autoTargetEnvironment_good_explicit_checking_bad() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");
    write(
        "buildenv/b/BUILD",
        "environment_group(",
        "    name = 'b',",
        "    defaults = [':b1'],",
        "    environments = [':b1', ':b2'])",
        "environment(name = 'b1')",
        "environment(name = 'b2')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/b:b2'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus", "--cpu=k8",
        "--target_environment=//buildenv/b:b1");
    assertThat(assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:bar")))
        .hasMessageThat()
        .contains(
            ""
                + "//foo:bar declares compatibility with:\n"
                + "  [//buildenv/b:b2]\n"
                + "but does not support:\n"
                + "  //buildenv/b:b1");
  }

  @Test
  public void autoTargetEnvironment_bad_explicit_checking_good() throws Exception {
    write(
        "buildenv/auto_cpu/BUILD",
        "environment_group(",
        "    name = 'cpus',",
        "    defaults = [':k8'],",
        "    environments = [':k8', ':ppc'])",
        "environment(name = 'k8')",
        "environment(name = 'ppc')");
    write(
        "buildenv/b/BUILD",
        "environment_group(",
        "    name = 'b',",
        "    defaults = [':b1'],",
        "    environments = [':b1', ':b2'])",
        "environment(name = 'b1')",
        "environment(name = 'b2')");

    write(
        "foo/bar.sh",
        "echo Bar!");
    write(
        "foo/BUILD",
        "sh_library(name = 'bar', srcs = ['bar.sh'], restricted_to = ['//buildenv/b:b2'])");

    addOptions("--build", "--auto_cpu_environment_group=//buildenv/auto_cpu:cpus", "--cpu=ppc",
        "--target_environment=//buildenv/b:b2");

    buildTarget("//foo:bar");
    assertThat(getResult().getSuccessfulTargets()).isEmpty();
    assertThat(Iterables.getOnlyElement(getResult().getSkippedTargets()).getLabel().toString())
        .isEqualTo("//foo:bar");
  }
}
