// Copyright 2017 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.android;

import static com.google.common.truth.Truth.assertThat;

import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.actions.FileWriteAction;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Abstract base class for tests of {@link BazelAndroidLocalTest}. These tests are for the java
 * related portions of the rule. Be sure to use writeFile() and overwriteFile() (instead of
 * scratch.writeFile() and scratch.overwriteFile()).
 */
@RunWith(JUnit4.class)
public abstract class AbstractAndroidLocalTestTestBase extends AndroidBuildViewTestCase {

  @Before
  public void setUp() throws Exception {
    writeFile("java/bar/BUILD", "");
    writeFile("java/bar/foo.bzl", "extra_deps = []");
  }

  @Test
  public void testSimpleAndroidRobolectricConfiguredTarget() throws Exception {
    writeFile(
        "java/test/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "android_local_test(name = 'dummyTest',",
        "    srcs = ['test.java'],",
        "    deps = extra_deps)");
    ConfiguredTarget target = getConfiguredTarget("//java/test:dummyTest");
    assertThat(target).isNotNull();
    checkMainClass(target, "dummyTest", false);
  }

  @Test
  public void testDataDependency() throws Exception {
    writeFile(
        "java/test/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "android_local_test(name = 'dummyTest',",
        "    srcs = ['test.java'],",
        "    deps = extra_deps,",
        "    data = ['data.dat'])");

    writeFile("java/test/data.dat", "this is a dummy data file");

    ConfiguredTarget target = getConfiguredTarget("//java/test:dummyTest");
    Artifact data = getFileConfiguredTarget("//java/test:data.dat").getArtifact();
    RunfilesProvider runfiles = target.getProvider(RunfilesProvider.class);

    assertThat(runfiles.getDataRunfiles().getAllArtifacts().toSet()).contains(data);
    assertThat(runfiles.getDefaultRunfiles().getAllArtifacts().toSet()).contains(data);
    assertThat(target).isNotNull();
  }

  @Test
  public void testNeverlinkRuntimeDepsExclusionReportsError() throws Exception {
    useConfiguration("--noexperimental_allow_runtime_deps_on_neverlink");
    checkError(
        "java/test",
        "test",
        "neverlink dep //java/test:neverlink_lib not allowed in runtime deps",
        String.format("%s(name = 'test',", getRuleName()),
        "    srcs = ['test.java'],",
        "    runtime_deps = [':neverlink_lib'])",
        "android_library(name = 'neverlink_lib',",
        "                srcs = ['dummyNeverlink.java'],",
        "                neverlink = 1,)");
  }

  @Test
  public void testFeatureFlagsAttributeSetsSelectInDependency() throws Exception {
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag1@on',",
        "  flag_values = {':flag1': 'on'},",
        "  transitive_configs = [':flag1'],",
        ")",
        "config_feature_flag(",
        "  name = 'flag2',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag2@on',",
        "  flag_values = {':flag2': 'on'},",
        "  transitive_configs = [':flag2'],",
        ")",
        "android_library(",
        "  name = 'lib',",
        "  srcs = select({",
        "    ':flag1@on': ['Flag1On.java'],",
        "    '//conditions:default': ['Flag1Off.java'],",
        "  }) + select({",
        "    ':flag2@on': ['Flag2On.java'],",
        "    '//conditions:default': ['Flag2Off.java'],",
        "  }),",
        "  transitive_configs = [':flag1', ':flag2'],",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java'],",
        "  deps = [':lib'] + extra_deps,",
        "  feature_flags = {",
        "    'flag1': 'on',",
        "  },",
        "  transitive_configs = [':flag1', ':flag2'],",
        ")");
    ConfiguredTarget binary = getConfiguredTarget("//java/com/foo");
    List<String> inputs =
        ActionsTestUtil.prettyArtifactNames(
            actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)));

    assertThat(inputs).containsAtLeast("java/com/foo/Flag1On.java", "java/com/foo/Flag2Off.java");
    assertThat(inputs).containsNoneOf("java/com/foo/Flag1Off.java", "java/com/foo/Flag2On.java");
  }

  @Test
  public void testFeatureFlagsAttributeSetsSelectInTest() throws Exception {
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag1@on',",
        "  flag_values = {':flag1': 'on'},",
        "  transitive_configs = [':flag1'],",
        ")",
        "config_feature_flag(",
        "  name = 'flag2',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag2@on',",
        "  flag_values = {':flag2': 'on'},",
        "  transitive_configs = [':flag2'],",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  deps = extra_deps,",
        "  srcs = ['Test.java'] + select({",
        "    ':flag1@on': ['Flag1On.java'],",
        "    '//conditions:default': ['Flag1Off.java'],",
        "  }) + select({",
        "    ':flag2@on': ['Flag2On.java'],",
        "    '//conditions:default': ['Flag2Off.java'],",
        "  }),",
        "  feature_flags = {",
        "    'flag1': 'on',",
        "  },",
        "  transitive_configs = [':flag1', ':flag2'],",
        ")");
    ConfiguredTarget binary = getConfiguredTarget("//java/com/foo");
    List<String> inputs =
        ActionsTestUtil.prettyArtifactNames(
            actionsTestUtil().artifactClosureOf(getFilesToBuild(binary)));

    assertThat(inputs).containsAtLeast("java/com/foo/Flag1On.java", "java/com/foo/Flag2Off.java");
    assertThat(inputs).containsNoneOf("java/com/foo/Flag1Off.java", "java/com/foo/Flag2On.java");
  }

  @Test
  public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalid() throws Exception {
    reporter.removeHandler(failFastHandler);
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag1@on',",
        "  flag_values = {':flag1': 'on'},",
        "  transitive_configs = [':flag1'],",
        ")",
        "android_library(",
        "  name = 'lib',",
        "  srcs = select({",
        "    ':flag1@on': ['Flag1On.java'],",
        "    '//conditions:default': ['Flag1Off.java'],",
        "  }),",
        "  transitive_configs = [':flag1'],",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java'],",
        "  deps = [':lib',] + extra_deps,",
        "  feature_flags = {",
        "    'flag1': 'invalid',",
        "  },",
        "  transitive_configs = [':flag1'],",
        ")");
    assertThat(getConfiguredTarget("//java/com/foo")).isNull();
    assertContainsEvent(
        "in config_feature_flag rule //java/com/foo:flag1: "
            + "value must be one of [\"off\", \"on\"], but was \"invalid\"");
  }

  @Test
  public void testFeatureFlagsAttributeFailsAnalysisIfFlagValueIsInvalidEvenIfNotUsed()
      throws Exception {
    reporter.removeHandler(failFastHandler);
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_setting(",
        "  name = 'flag1@on',",
        "  flag_values = {':flag1': 'on'},",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java'],",
        "  deps = extra_deps,",
        "  feature_flags = {",
        "    'flag1': 'invalid',",
        "  }",
        ")");
    assertThat(getConfiguredTarget("//java/com/foo")).isNull();
    assertContainsEvent(
        "in config_feature_flag rule //java/com/foo:flag1: "
            + "value must be one of [\"off\", \"on\"], but was \"invalid\"");
  }

  @Test
  public void testFeatureFlagsAttributeSetsFeatureFlagProviderValues() throws Exception {
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/reader.bzl",
        "def _impl(ctx):",
        "  ctx.actions.write(",
        "      ctx.outputs.java,",
        "      '\\n'.join([",
        "          str(target.label) + ': ' + target[config_common.FeatureFlagInfo].value",
        "          for target in ctx.attr.flags]))",
        "  return struct(files=depset([ctx.outputs.java]))",
        "flag_reader = rule(",
        "  implementation=_impl,",
        "  attrs={'flags': attr.label_list(providers=[config_common.FeatureFlagInfo])},",
        "  outputs={'java': '%{name}.java'},",
        ")");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "load('//java/com/foo:reader.bzl', 'flag_reader')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "config_feature_flag(",
        "  name = 'flag2',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "flag_reader(",
        "  name = 'FooFlags',",
        "  flags = [':flag1', ':flag2'],",
        "  transitive_configs = [':flag1', ':flag2'],",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java', ':FooFlags.java'],",
        "  deps = extra_deps,",
        "  feature_flags = {",
        "    'flag1': 'on',",
        "  },",
        "  transitive_configs = [':flag1', ':flag2'],",
        ")");
    Artifact flagList =
        ActionsTestUtil.getFirstArtifactEndingWith(
            actionsTestUtil()
                .artifactClosureOf(getFilesToBuild(getConfiguredTarget("//java/com/foo"))),
            "/FooFlags.java");
    FileWriteAction action = (FileWriteAction) getGeneratingAction(flagList);
    assertThat(action.getFileContents())
        .isAnyOf(
            "//java/com/foo:flag1: on\n//java/com/foo:flag2: off",
            "@//java/com/foo:flag1: on\n@//java/com/foo:flag2: off");
  }

  @Test
  public void testFeatureFlagsAttributeFailsAnalysisIfFlagIsAliased() throws Exception {
    reporter.removeHandler(failFastHandler);
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    writeFile(
        "java/com/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "config_feature_flag(",
        "  name = 'flag1',",
        "  allowed_values = ['on', 'off'],",
        "  default_value = 'off',",
        ")",
        "alias(",
        "  name = 'alias',",
        "  actual = 'flag1',",
        "  transitive_configs = [':flag1'],",
        ")",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java'],",
        "  deps = extra_deps,",
        "  feature_flags = {",
        "    'alias': 'on',",
        "  },",
        "  transitive_configs = [':flag1'],",
        ")");
    assertThat(getConfiguredTarget("//java/com/foo")).isNull();
    assertContainsEvent(
        String.format(
            "in feature_flags attribute of %s rule //java/com/foo:foo: "
                + "Feature flags must be named directly, not through aliases; "
                + "use '//java/com/foo:flag1', not '//java/com/foo:alias'",
            getRuleName()));
  }

  @Test
  public void testFeatureFlagPolicyMustBeVisibleToRuleToUseFeatureFlags() throws Exception {
    reporter.removeHandler(failFastHandler); // expecting an error
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    overwriteFile(
        "tools/allowlists/config_feature_flag/BUILD",
        "package_group(",
        "    name = 'config_feature_flag',",
        "    packages = ['//flag'])");
    writeFile(
        "flag/BUILD",
        "config_feature_flag(",
        "    name = 'flag',",
        "    allowed_values = ['right', 'wrong'],",
        "    default_value = 'right',",
        "    visibility = ['//java/com/google/android/foo:__pkg__'],",
        ")");
    writeFile(
        "java/com/google/android/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java', ':FooFlags.java'],",
        "  deps = extra_deps,",
        "  feature_flags = {",
        "    '//flag:flag': 'right',",
        "  },",
        "  transitive_configs = ['//flag:flag'],",
        ")");
    assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNull();
    assertContainsEvent(
        String.format(
            "in %s rule //java/com/google/android/foo:foo:"
                + " the attribute feature_flags is not available in this package",
            getRuleName()));
  }

  @Test
  public void testFeatureFlagPolicyDoesNotBlockRuleIfInPolicy() throws Exception {
    useConfiguration("--enforce_transitive_configs_for_config_feature_flag");
    overwriteFile(
        "tools/allowlists/config_feature_flag/BUILD",
        "package_group(",
        "    name = 'config_feature_flag',",
        "    packages = ['//flag', '//java/com/google/android/foo'])");
    writeFile(
        "flag/BUILD",
        "config_feature_flag(",
        "    name = 'flag',",
        "    allowed_values = ['right', 'wrong'],",
        "    default_value = 'right',",
        "    visibility = ['//java/com/google/android/foo:__pkg__'],",
        ")");
    writeFile(
        "java/com/google/android/foo/BUILD",
        "load('//java/bar:foo.bzl', 'extra_deps')",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java', ':FooFlags.java'],",
        "  deps = extra_deps,",
        "  feature_flags = {",
        "    '//flag:flag': 'right',",
        "  },",
        "  transitive_configs = ['//flag:flag'],",
        ")");
    assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNotNull();
    assertNoEvents();
  }

  @Test
  public void testFeatureFlagPolicyIsNotUsedIfFlagValuesNotUsed() throws Exception {
    overwriteFile(
        "tools/allowlists/config_feature_flag/BUILD",
        "package_group(",
        "    name = 'config_feature_flag',",
        "    packages = ['*super* busted package group'])");
    writeFile(
        "java/com/google/android/foo/BUILD",
        "android_local_test(",
        "  name = 'foo',",
        "  srcs = ['Test.java', ':FooFlags.java'],",
        ")");
    assertThat(getConfiguredTarget("//java/com/google/android/foo:foo")).isNotNull();
    // the package_group is busted, so we would have failed to get this far if we depended on it
    assertNoEvents();
    // NOTE: someone should verify that this test actually does what we think it does.
    reporter.removeHandler(failFastHandler);
    assertThat(getConfiguredTarget("//tools/allowlists/config_feature_flag:config_feature_flag"))
        .isNull();
    assertContainsEvent("*super* busted package group");
  }

  @Test
  public void androidManifestMergerOrderAlphabeticalByConfiguration_MergeesSortedByPathInBinOrGen()
      throws Exception {
    useConfiguration(
        "--fat_apk_cpu=k8", "--android_manifest_merger_order=alphabetical_by_configuration");
    scratch.overwriteFile(
        "java/android/BUILD",
        "android_library(",
        "    name = 'core',",
        "    manifest = 'core/AndroidManifest.xml',",
        "    exports_manifest = 1,",
        "    resource_files = ['core/res/values/strings.xml'],",
        ")",
        "android_library(",
        "    name = 'utility',",
        "    manifest = 'utility/AndroidManifest.xml',",
        "    exports_manifest = 1,",
        "    resource_files = ['utility/res/values/values.xml'],",
        "    deps = ['//java/common:common'],",
        ")");
    scratch.file(
        "java/binary/BUILD",
        "android_binary(",
        "    name = 'application',",
        "    srcs = ['App.java'],",
        "    manifest = 'app/AndroidManifest.xml',",
        "    deps = [':library'],",
        ")",
        "android_library(",
        "    name = 'library',",
        "    manifest = 'library/AndroidManifest.xml',",
        "    exports_manifest = 1,",
        "    deps = ['//java/common:theme', '//java/android:utility'],",
        ")");
    scratch.file(
        "java/test/BUILD",
        "android_local_test(",
        "    name = 'test',",
        "    srcs = ['Test.java'],",
        "    manifest = 'testing/AndroidManifest.xml',",
        "    deps = ['//java/binary:application', '//java/binary:library'],",
        ")");
    scratch.file(
        "java/common/BUILD",
        "android_library(",
        "    name = 'common',",
        "    manifest = 'common/AndroidManifest.xml',",
        "    exports_manifest = 1,",
        "    resource_files = ['common/res/values/common.xml'],",
        "    deps = ['//java/android:core'],",
        ")",
        "android_library(",
        "    name = 'theme',",
        "    manifest = 'theme/AndroidManifest.xml',",
        "    exports_manifest = 1,",
        "    resource_files = ['theme/res/values/values.xml'],",
        ")");

    ConfiguredTarget test = getConfiguredTarget("//java/test:test");
    assertNoEvents();
    assertThat(test).isNotNull();

    // each entry appears twice because the application duplicates them all into another config;
    // this is partially testing that doing so does not cause a crash because of matching
    // root-relative paths.
    assertThat(getLocalTestMergeeManifests(test).values())
        .containsExactly(
            "//java/android:core",
            "//java/android:core",
            "//java/android:utility",
            "//java/android:utility",
            "//java/binary:library",
            "//java/binary:library",
            "//java/common:common",
            "//java/common:common",
            "//java/common:theme",
            "//java/common:theme")
        .inOrder();
  }

  @Test
  public void testDeployJar() throws Exception {
    writeFile(
        "java/com/google/android/foo/BUILD",
        "android_local_test(name = 'test',",
        "                srcs =['test.java'],",
        "                )",
        "android_library(name = 'lib',",
        "                data = [':test_deploy.jar'],",
        "                srcs =['lib.java'],",
        "                )");

    Action deployJarAction =
        getGeneratingAction(
            getFileConfiguredTarget("//java/com/google/android/foo:test_deploy.jar").getArtifact());
    List<String> inputs = ActionsTestUtil.baseArtifactNames(deployJarAction.getInputs());

    assertThat(inputs).containsAtLeast("test_resources.jar", "test.jar");
  }

  public abstract void checkMainClass(
      ConfiguredTarget target, String targetName, boolean coverageEnabled) throws Exception;

  protected abstract String getRuleName();

  protected abstract void writeFile(String path, String... lines) throws Exception;

  protected abstract void overwriteFile(String path, String... lines) throws Exception;
}
