#!/usr/bin/env bash
#
# Copyright 2024 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.
#
# Tests the behaviour of --incompatible_autoload_externally flag.

# Load the test setup defined in the parent directory
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${CURRENT_DIR}/../integration_test_setup.sh" \
  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }

#### SETUP #############################################################

set -e
add_to_bazelrc "common --noincompatible_disable_autoloads_in_main_repo"

#### TESTS #############################################################

function mock_rules_android() {
  rules_android_workspace="${TEST_TMPDIR}/rules_android_workspace"
  mkdir -p "${rules_android_workspace}/rules"
  touch "${rules_android_workspace}/rules/BUILD"
  touch "${rules_android_workspace}/WORKSPACE"
  cat > "${rules_android_workspace}/MODULE.bazel" << EOF
module(name = "rules_android")
EOF
  cat > "${rules_android_workspace}/rules/rules.bzl" << EOF
def _impl(ctx):
  pass

aar_import = rule(
  implementation = _impl,
  attrs = {
    "aar": attr.label(allow_files = True),
    "deps": attr.label_list(),
  }
)
EOF

  cat >> MODULE.bazel << EOF
bazel_dep(
    name = "rules_android",
)
local_path_override(
    module_name = "rules_android",
    path = "${rules_android_workspace}",
)
EOF

  cat > WORKSPACE << EOF
workspace(name = "test")
local_repository(
    name = "rules_android",
    path = "${rules_android_workspace}",
)
EOF
}


function mock_rules_java() {
  rules_java_workspace="${TEST_TMPDIR}/rules_java_workspace"
  mkdir -p "${rules_java_workspace}/java"
  touch "${rules_java_workspace}/java/BUILD"
  cat > "${rules_java_workspace}/java/rules_java_deps.bzl" <<EOF
def rules_java_dependencies():
  pass
EOF
  cat > "${rules_java_workspace}/java/repositories.bzl" <<EOF
def rules_java_toolchains():
  pass
EOF
  touch "${rules_java_workspace}/WORKSPACE"
  cat > "${rules_java_workspace}/MODULE.bazel" << EOF
module(name = "rules_java")
EOF
  cat > MODULE.bazel << EOF
bazel_dep(
    name = "rules_java",
)
local_path_override(
    module_name = "rules_java",
    path = "${rules_java_workspace}",
)
EOF

  cat > WORKSPACE << EOF
workspace(name = "test")
local_repository(
    name = "rules_java",
    path = "${rules_java_workspace}",
)
EOF
}

function mock_apple_support() {
  apple_support_workspace="${TEST_TMPDIR}/apple_support_workspace"
  mkdir -p "${apple_support_workspace}/xcode"
  touch "${apple_support_workspace}/xcode/BUILD"
  touch "${apple_support_workspace}/WORKSPACE"
  cat > "${apple_support_workspace}/MODULE.bazel" << EOF
module(name = "apple_support", repo_name = "build_bazel_apple_support")
EOF
  cat > "${apple_support_workspace}/xcode/xcode_version.bzl" << EOF
def _impl(ctx):
  pass

xcode_version = rule(
  implementation = _impl,
  attrs = {
    "version": attr.string(),
  }
)
EOF

  cat >> MODULE.bazel << EOF
bazel_dep(
    name = "apple_support",
    repo_name = "build_bazel_apple_support",
)
local_path_override(
    module_name = "apple_support",
    path = "${apple_support_workspace}",
)
EOF

  cat > WORKSPACE << EOF
workspace(name = "test")
local_repository(
    name = "build_bazel_apple_support",
    path = "${apple_support_workspace}",
)
EOF
}

function mock_protobuf() {
  protobuf_workspace="${TEST_TMPDIR}/protobuf_workspace"
  mkdir -p "${protobuf_workspace}/protobuf"
  cat > "${protobuf_workspace}/MODULE.bazel" << EOF
module(name = "protobuf", repo_name = "com_google_protobuf")
EOF

  cat >> MODULE.bazel << EOF
bazel_dep(
    name = "protobuf",
    repo_name = "com_google_protobuf",
)
local_path_override(
    module_name = "protobuf",
    path = "${protobuf_workspace}",
)
EOF
}

function test_missing_necessary_repo_fails() {
  # Mock protobuf to prevent apple_support being added from MODULE.tools via protobuf
  mock_protobuf
  cat > BUILD << EOF
xcode_version(
    name = 'xcode_version',
    version = "5.1.2",
)
EOF
  bazel build --incompatible_autoload_externally=xcode_version :xcode_version >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Couldn't auto load 'xcode_version' from '@build_bazel_apple_support//xcode:xcode_version.bzl'. Ensure that you have a 'bazel_dep(name = \"apple_support\", ...)' in your MODULE.bazel file or add an explicit load statement to your BUILD file."
}

function test_missing_unnecessary_repo_doesnt_fail() {
  # Intentionally not adding rules_android to MODULE.bazel (and it's not in MODULE.tools)
  cat > WORKSPACE << EOF
workspace(name = "test")
EOF
  cat > BUILD << EOF
filegroup(
    name = 'filegroup',
    srcs = [],
)
EOF
  bazel build --incompatible_autoload_externally=+@rules_android :filegroup >&$TEST_log 2>&1 || fail "build failed"
}

function test_removed_rule_loaded() {
  setup_module_dot_bazel
  mock_rules_android

  cat > BUILD << EOF
aar_import(
    name = 'aar',
    aar = 'aar.file',
    deps = [],
)
EOF

  bazel build --incompatible_autoload_externally=aar_import :aar >&$TEST_log 2>&1 || fail "build failed"
}

function test_removed_rule_loaded_from_legacy_repo_name() {
  setup_module_dot_bazel
  mock_apple_support

  cat > BUILD << EOF
xcode_version(
    name = 'xcode_version',
    version = "5.1.2",
)
EOF
  bazel build --incompatible_autoload_externally=xcode_version :xcode_version >&$TEST_log 2>&1 || fail "build failed"
}

function test_removed_rule_loaded_from_bzl() {
  setup_module_dot_bazel
  mock_rules_android

  cat > macro.bzl << EOF
def macro():
    native.aar_import(
        name = 'aar',
        aar = 'aar.file',
        deps = [],
    )
EOF

  cat > BUILD << EOF
load(":macro.bzl", "macro")
macro()
EOF

  bazel build --incompatible_autoload_externally=aar_import :aar >&$TEST_log 2>&1 || fail "build failed"

}

function test_removed_symbol_loaded() {
  cat > symbol.bzl << EOF
def symbol():
  a = ProtoInfo
EOF

  cat > BUILD << EOF
load(":symbol.bzl", "symbol")
symbol()
EOF

  bazel build --incompatible_autoload_externally=ProtoInfo,proto_common_do_not_use,java_binary :all >&$TEST_log 2>&1 || fail "build failed"
}

function test_proto_common_do_not_use() {
  cat > symbol.bzl << EOF
def symbol():
  print("\n".join(dir(proto_common_do_not_use)))
EOF

  cat > BUILD << EOF
load(":symbol.bzl", "symbol")
symbol()
EOF

  bazel build --incompatible_autoload_externally=proto_common_do_not_use :all >&$TEST_log 2>&1 || fail "build failed"
  expect_log INCOMPATIBLE_ENABLE_PROTO_TOOLCHAIN_RESOLUTION
  expect_not_log "compile"
}

function test_existing_rule_is_redirected() {
  cat > BUILD << EOF
sh_library(
    name = 'sh_library',
)
EOF
  bazel query --incompatible_autoload_externally=+sh_library ':sh_library' --output=build >&$TEST_log 2>&1 || fail "build failed"
  expect_log "rules_shell.\\?/shell/private/sh_library.bzl"
}

function test_existing_rule_is_redirected_in_bzl() {
  cat > macro.bzl << EOF
def macro():
    native.sh_library(
        name = 'sh_library',
    )
EOF

  cat > BUILD << EOF
load(":macro.bzl", "macro")
macro()
EOF

  bazel query --incompatible_autoload_externally=+sh_library ':sh_library' --output=build >&$TEST_log 2>&1 || fail "build failed"
  expect_log "rules_shell.\\?/shell/private/sh_library.bzl"
}

function test_removed_rule_not_loaded() {
  cat > BUILD << EOF
aar_import(
    name = 'aar',
    aar = 'aar.file',
    deps = [],
    visibility = ['//visibility:public'],
)
EOF

  bazel build --incompatible_autoload_externally= :aar >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "name 'aar_import' is not defined"
}

function test_removed_rule_not_loaded_in_bzl() {
  cat > macro.bzl << EOF
def macro():
    native.aar_import(
      name = 'aar',
      aar = 'aar.file',
      deps = [],
      visibility = ['//visibility:public'],
    )
EOF

  cat > BUILD << EOF
load(":macro.bzl", "macro")
macro()
EOF

  bazel build --incompatible_autoload_externally= :aar >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "no native function or rule 'aar_import'"
}

function test_removed_symbol_not_loaded_in_bzl() {
  setup_module_dot_bazel

  cat > symbol.bzl << EOF
def symbol():
    a = ProtoInfo
EOF

  cat > BUILD << EOF
load(":symbol.bzl", "symbol")
symbol()
EOF

  bazel build --incompatible_autoload_externally= :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "name 'ProtoInfo' is not defined"
}


function test_removing_existing_rule() {
  cat > BUILD << EOF
cc_binary(
    name = "bin",
    srcs = ["a.cc"]
)
EOF

  bazel build --incompatible_autoload_externally=-cc_binary :bin >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "name 'cc_binary' is not defined"
}

function test_removing_existing_rule_in_bzl() {
  cat > macro.bzl << EOF
def macro():
    native.cc_binary(
        name = "bin",
        srcs = ["a.cc"],
    )
EOF

  cat > BUILD << EOF
load(":macro.bzl", "macro")
macro()
EOF

  bazel build --incompatible_autoload_externally=-cc_binary :bin >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "no native function or rule 'cc_binary'"
}

function test_removing_symbol_incompletely() {
  cat > symbol.bzl << EOF
def symbol():
   a = CcInfo
EOF

  cat > BUILD << EOF
load(":symbol.bzl", "symbol")
symbol()
EOF

  bazel build --incompatible_autoload_externally=-CcInfo :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Symbol 'CcInfo' can't be removed, because it's still used by:"
}

function test_removing_existing_symbol() {
  cat > symbol.bzl << EOF
def symbol():
   a = DebugPackageInfo
EOF

  cat > BUILD << EOF
load(":symbol.bzl", "symbol")
symbol()
EOF

  bazel build --incompatible_autoload_externally=-DebugPackageInfo,-cc_binary,-cc_test :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "name 'DebugPackageInfo' is not defined"
}

function test_removing_symbol_typo() {
  cat > bzl_file.bzl << EOF
def bzl_file():
    pass
EOF

  cat > BUILD << EOF
load(":bzl_file.bzl", "bzl_file")
EOF

  bazel build --incompatible_autoload_externally=-ProtozzzInfo :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Undefined symbol in --incompatible_autoload_externally"
}

function test_removing_rule_typo() {
  touch BUILD

  bazel build --incompatible_autoload_externally=-androidzzz_binary :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Undefined symbol in --incompatible_autoload_externally"
}

function test_redirecting_rule_with_bzl_typo() {
  # Bzl file is evaluated first, so this should cover bzl file support
  cat > bzl_file.bzl << EOF
def bzl_file():
    pass
EOF

  cat > BUILD << EOF
load(":bzl_file.bzl", "bzl_file")
EOF

  bazel build --incompatible_autoload_externally=pyzzz_library :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Undefined symbol in --incompatible_autoload_externally"
}

function test_redirecting_rule_typo() {
  cat > BUILD << EOF
EOF


  bazel build --incompatible_autoload_externally=pyzzz_library :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Undefined symbol in --incompatible_autoload_externally"
}

function test_redirecting_symbols_typo() {
  # Bzl file is evaluated first, so this should cover bzl file support
  cat > bzl_file.bzl << EOF
def bzl_file():
    pass
EOF

  cat > BUILD << EOF
load(":bzl_file.bzl", "bzl_file")
EOF

  bazel build --incompatible_autoload_externally=ProotoInfo :all >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
    expect_log "Undefined symbol in --incompatible_autoload_externally"
}

function test_bad_flag_value() {
  cat > BUILD << EOF
py_library(
    name = 'py_library',
)
EOF
  bazel query --incompatible_autoload_externally=py_library,-py_library ':py_library' --output=build >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Duplicated symbol 'py_library' in --incompatible_autoload_externally"
}

function test_missing_symbol_error() {
  mock_rules_android
  rules_android_workspace="${TEST_TMPDIR}/rules_android_workspace"
  # emptying the file simulates a missing symbol
  cat > "${rules_android_workspace}/rules/rules.bzl" << EOF
EOF

  cat > BUILD << EOF
aar_import(
    name = 'aar',
    aar = 'aar.file',
    deps = [],
)
EOF
  bazel build --incompatible_autoload_externally=aar_import :aar >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Failed to apply symbols loaded externally: The toplevel symbol 'aar_import' set by --incompatible_load_symbols_externally couldn't be loaded. 'aar_import' not found in auto loaded '@rules_android//rules:rules.bzl'."
}

function test_missing_bzlfile_error() {
  mock_rules_android
  rules_android_workspace="${TEST_TMPDIR}/rules_android_workspace"
  rm "${rules_android_workspace}/rules/rules.bzl"

  cat > BUILD << EOF
aar_import(
    name = 'aar',
    aar = 'aar.file',
    deps = [],
)
EOF
  bazel build --incompatible_autoload_externally=aar_import :aar >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Failed to autoload external symbols: cannot load '@@\?rules_android+\?//rules:rules.bzl': no such file"
}


function test_whole_repo_flag() {
  cat > BUILD << EOF
sh_library(
    name = 'sh_library',
)
EOF
  bazel query --incompatible_autoload_externally=+@rules_shell ':sh_library' --output=build >&$TEST_log 2>&1 || fail "build failed"
}

function test_legacy_globals() {
  mock_rules_java

  rules_java_workspace="${TEST_TMPDIR}/rules_java_workspace"

  mkdir -p "${rules_java_workspace}/java/common"
  touch "${rules_java_workspace}/java/common/BUILD"
  cat > "${rules_java_workspace}/java/common/print_legacy_globals.bzl" << EOF
def print_legacy_globals():
  print(dir(native.legacy_globals))
EOF

  cat > BUILD << EOF
load("@rules_java//java/common:print_legacy_globals.bzl", "print_legacy_globals")

print_legacy_globals()
EOF

  bazel build --incompatible_autoload_externally= :all >&$TEST_log 2>&1 || fail "build unexpectedly failed"
  expect_log '"CcInfo", "CcSharedLibraryHintInfo", "CcSharedLibraryInfo", "CcToolchainConfigInfo", "DebugPackageInfo", "apple_common", "cc_common", "java_common", "proto_common_do_not_use"'
}

function test_incompatible_disable_autoloads_in_main_repo() {
  setup_module_dot_bazel

  mkdir foo
  cat > foo/BUILD << EOF
java_library(
  name = "foo",
  srcs = ["A.java"]
)
EOF

  mkdir bar
  cat > bar/a.bzl << EOF
def my_java_library(**kwargs):
  native.java_library(**kwargs)
EOF
  cat > bar/BUILD << EOF
load(":a.bzl", "my_java_library")
my_java_library(
  name = "bar",
  srcs = ["A.java"]
)
EOF

  bazel query --incompatible_autoload_externally=java_library --noincompatible_disable_autoloads_in_main_repo //foo >&$TEST_log 2>&1 || fail "build failed"
  bazel query --incompatible_autoload_externally=java_library --incompatible_disable_autoloads_in_main_repo //foo >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "name 'java_library' is not defined"
  bazel query --incompatible_autoload_externally=java_library --noincompatible_disable_autoloads_in_main_repo //bar >&$TEST_log 2>&1 || fail "build failed"
  bazel query --incompatible_autoload_externally=java_library --incompatible_disable_autoloads_in_main_repo //bar >&$TEST_log 2>&1 && fail "build unexpectedly succeeded"
  expect_log "Error: no native function or rule 'java_library'"
}


run_suite "Tests for incompatible_autoload_externally flag"
