#!/bin/bash

# --- begin runfiles.bash initialization ---
# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
set -euo pipefail
if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
  if [[ -f "$0.runfiles_manifest" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
    export RUNFILES_DIR="$0.runfiles"
  fi
fi
if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
else
  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
  exit 1
fi
# --- end runfiles.bash initialization ---

die () {
  echo "$1" 1>&2
  exit 1
}

[[ "$1" =~ external/* ]] && buildifier="${{1#external/}}" || buildifier="$TEST_WORKSPACE/$1"
buildifier="$(rlocation "$buildifier")"

touch WORKSPACE.bazel
[[ -d test_dir ]] && rm -r test_dir
mkdir -p test_dir/subdir
mkdir -p golden
INPUT="load(':foo.bzl', 'foo'); foo(tags=['b', 'a'],srcs=['d', 'c'])"  # formatted differently in build and bzl modes
echo -e "$INPUT" > test_dir/BUILD
echo -e "$INPUT" > test_dir/test.bzl
echo -e "$INPUT" > test_dir/subdir/test.bzl
echo -e "$INPUT" > test_dir/subdir/build  # lowercase, should be ignored by -r
echo -e "$INPUT" > test.bzl  # outside the test_dir directory
echo -e "$INPUT" > test2.bzl  # outside the test_dir directory
echo -e "not valid +" > test_dir/foo.bar
echo -e '{ "type": "build" }' > test_dir/.buildifier.test.json # demonstrate config file works by overriding input type to format a bzl file as a BUILD file.
mkdir test_dir/workspace  # name of a starlark file, but a directory
mkdir test_dir/.git  # contents should be ignored
echo -e "a+b" > test_dir/.git/git.bzl
cat > test_dir/MODULE.bazel <<'EOF'
module(name='my-module',version='1.0',compatibility_level=1)
include("cpp.MODULE.bazel")

include("go.MODULE.bazel")
include("web.MODULE.bazel")
bazel_dep(name='rules_cc',version='0.0.1')
bazel_dep(name='protobuf',repo_name='com_google_protobuf',version='3.19.0')
bazel_dep(
    name='rules_go',
    version='0.37.0',
    repo_name='io_bazel_rules_go',
)
go_sdk=use_extension("@io_bazel_rules_go//go:extensions.bzl","go_sdk")
# Known to exist since it is instantiated by rules_go itself.
use_repo(go_sdk,"go_default_sdk")
non_module_deps = use_extension("//internal/bzlmod:non_module_deps.bzl","non_module_deps")
use_repo(
    non_module_deps,
    "bazel_gazelle_go_repository_tools",
    "bazel_gazelle_go_repository_config",
    "bazel_gazelle_go_repository_cache",
)
rules_go_non_module_deps = use_extension("@io_bazel_rules_go//go/private:extensions.bzl","non_module_dependencies",dev_dependency=True)
use_repo(rules_go_non_module_deps,"go_googleapis")
go_deps  =  use_extension("//:extensions.bzl",  "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(
    go_deps,
    "com_github_fsnotify_fsnotify",
    "com_github_fsnotify_fsnotify",
    "com_github_bmatcuk_doublestar_v4",
    "com_github_bazelbuild_buildtools",
    "com_github_google_go_cmp",
    "com_github_pelletier_go_toml",
    "org_golang_x_mod",
    "com_github_pmezard_go_difflib",
    # Separated by comment.
    "org_golang_x_sync",
    "org_golang_x_tools",
    # Used internally by the go_deps module extension.
    "bazel_gazelle_go_repository_directives",
    c = "a",
    b = "b",
    a = "c",
)
go_deps.module(name = "foo")
use_repo(go_deps, "foo")
bazel_dep(name="foo",version="1.0")
git_override(module_name="foo",remote="foo.git",commit="1234567890")
bazel_dep(name="bar",version="1.0")
archive_override(module_name="not_bar",integrity="sha256-1234567890")
# do not sort
use_repo(go_deps, "b", "b", "a")
use_repo(
    # do not sort
    go_deps,
    "b",
    "b",
    "a",
)
use_repo(
    go_deps,
    # do not sort
    "b",
    "b",
    "a",
)

bazel_dep(name='prod_dep',version='3.19.0')
bazel_dep(name='other_prod_dep',version='3.19.0',dev_dependency=False)
bazel_dep(name='dev_dep',version='3.19.0',dev_dependency=True)
bazel_dep(name = "weird_dep", version = "3.19.0", dev_dependency = "True" == "True")
bazel_dep(name='yet_another_prod_dep',version='3.19.0')
EOF

cp test_dir/foo.bar golden/foo.bar
cp test_dir/subdir/build golden/build
cp test_dir/.git/git.bzl golden/git.bzl

"$buildifier" < test_dir/BUILD > stdout
"$buildifier" -r test_dir
"$buildifier" test.bzl
"$buildifier" --path=foo.bzl test2.bzl
"$buildifier" --config=test_dir/.buildifier.test.json < test_dir/test.bzl > test_dir/test.bzl.BUILD.out
"$buildifier" --config=example > test_dir/.buildifier.example.json

cat > golden/BUILD.golden <<EOF
load(":foo.bzl", "foo")

foo(
    srcs = [
        "c",
        "d",
    ],
    tags = [
        "a",
        "b",
    ],
)
EOF

cat > golden/test.bzl.golden <<EOF
load(":foo.bzl", "foo")

foo(tags = ["b", "a"], srcs = ["d", "c"])
EOF

cat > golden/MODULE.bazel.golden <<EOF
module(
    name = "my-module",
    version = "1.0",
    compatibility_level = 1,
)

include("cpp.MODULE.bazel")
include("go.MODULE.bazel")
include("web.MODULE.bazel")

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0", repo_name = "com_google_protobuf")
bazel_dep(
    name = "rules_go",
    version = "0.37.0",
    repo_name = "io_bazel_rules_go",
)

go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")

# Known to exist since it is instantiated by rules_go itself.
use_repo(go_sdk, "go_default_sdk")

non_module_deps = use_extension("//internal/bzlmod:non_module_deps.bzl", "non_module_deps")
use_repo(
    non_module_deps,
    "bazel_gazelle_go_repository_cache",
    "bazel_gazelle_go_repository_config",
    "bazel_gazelle_go_repository_tools",
)

rules_go_non_module_deps = use_extension("@io_bazel_rules_go//go/private:extensions.bzl", "non_module_dependencies", dev_dependency = True)
use_repo(rules_go_non_module_deps, "go_googleapis")

go_deps = use_extension("//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(
    go_deps,
    "com_github_bazelbuild_buildtools",
    "com_github_bmatcuk_doublestar_v4",
    "com_github_fsnotify_fsnotify",
    "com_github_google_go_cmp",
    "com_github_pelletier_go_toml",
    "com_github_pmezard_go_difflib",
    "org_golang_x_mod",
    # Separated by comment.
    "org_golang_x_sync",
    "org_golang_x_tools",
    # Used internally by the go_deps module extension.
    "bazel_gazelle_go_repository_directives",
    a = "c",
    b = "b",
    c = "a",
)

go_deps.module(name = "foo")
use_repo(go_deps, "foo")

bazel_dep(name = "foo", version = "1.0")
git_override(
    module_name = "foo",
    commit = "1234567890",
    remote = "foo.git",
)

bazel_dep(name = "bar", version = "1.0")

archive_override(
    module_name = "not_bar",
    integrity = "sha256-1234567890",
)

# do not sort
use_repo(go_deps, "b", "a")
use_repo(
    # do not sort
    go_deps,
    "b",
    "a",
)
use_repo(
    go_deps,
    # do not sort
    "b",
    "a",
)

bazel_dep(name = "prod_dep", version = "3.19.0")
bazel_dep(name = "other_prod_dep", version = "3.19.0", dev_dependency = False)

bazel_dep(name = "dev_dep", version = "3.19.0", dev_dependency = True)
bazel_dep(name = "weird_dep", version = "3.19.0", dev_dependency = "True" == "True")

bazel_dep(name = "yet_another_prod_dep", version = "3.19.0")
EOF

cat > golden/.buildifier.example.json <<EOF
{
  "type": "auto",
  "mode": "fix",
  "lint": "fix",
  "warningsList": [
    "attr-applicable_licenses",
    "attr-cfg",
    "attr-license",
    "attr-licenses",
    "attr-non-empty",
    "attr-output-default",
    "attr-single-file",
    "build-args-kwargs",
    "bzl-visibility",
    "canonical-repository",
    "confusing-name",
    "constant-glob",
    "ctx-actions",
    "ctx-args",
    "deprecated-function",
    "depset-items",
    "depset-iteration",
    "depset-union",
    "dict-concatenation",
    "dict-method-named-arg",
    "duplicated-name",
    "external-path",
    "filetype",
    "function-docstring",
    "function-docstring-args",
    "function-docstring-header",
    "function-docstring-return",
    "git-repository",
    "http-archive",
    "integer-division",
    "keyword-positional-params",
    "list-append",
    "load",
    "module-docstring",
    "name-conventions",
    "native-android",
    "native-build",
    "native-cc-binary",
    "native-cc-common",
    "native-cc-debug-package-info",
    "native-cc-fdo-prefetch-hints",
    "native-cc-fdo-profile",
    "native-cc-import",
    "native-cc-info",
    "native-cc-library",
    "native-cc-memprof-profile",
    "native-cc-objc-import",
    "native-cc-objc-library",
    "native-cc-propeller-optimize",
    "native-cc-proto",
    "native-cc-shared-library",
    "native-cc-shared-library-hint-info",
    "native-cc-shared-library-info",
    "native-cc-test",
    "native-cc-toolchain",
    "native-cc-toolchain-suite",
    "native-java-binary",
    "native-java-common",
    "native-java-import",
    "native-java-info",
    "native-java-library",
    "native-java-lite-proto",
    "native-java-package-config",
    "native-java-plugin",
    "native-java-plugin-info",
    "native-java-proto",
    "native-java-runtime",
    "native-java-test",
    "native-java-toolchain",
    "native-package",
    "native-proto",
    "native-proto-common",
    "native-proto-info",
    "native-proto-lang-toolchain",
    "native-proto-lang-toolchain-info",
    "native-py",
    "native-sh-binary",
    "native-sh-library",
    "native-sh-test",
    "no-effect",
    "output-group",
    "overly-nested-depset",
    "package-name",
    "package-on-top",
    "positional-args",
    "print",
    "provider-params",
    "redefined-variable",
    "repository-name",
    "return-value",
    "rule-impl-return",
    "skylark-comment",
    "skylark-docstring",
    "string-iteration",
    "uninitialized",
    "unnamed-macro",
    "unreachable",
    "unsorted-dict-items",
    "unused-variable"
  ]
}
EOF

diff -u test_dir/BUILD golden/BUILD.golden
diff -u test_dir/test.bzl golden/test.bzl.golden
diff -u test_dir/subdir/test.bzl golden/test.bzl.golden
diff -u test_dir/test.bzl.BUILD.out golden/BUILD.golden
diff -u test_dir/subdir/build golden/build
diff -u test_dir/foo.bar golden/foo.bar
diff -u test.bzl golden/test.bzl.golden
diff -u test2.bzl golden/test.bzl.golden
diff -u stdout golden/test.bzl.golden
diff -u test_dir/.git/git.bzl golden/git.bzl
diff -u test_dir/MODULE.bazel golden/MODULE.bazel.golden
diff -u test_dir/.buildifier.example.json golden/.buildifier.example.json

# Test run on a directory without -r
"$buildifier" test_dir || ret=$?
if [[ $ret -ne 3 ]]; then
  die "Directory without -r: expected buildifier to exit with 3, actual: $ret"
fi

# Test the linter

cat > test_dir/to_fix.bzl <<EOF
load("//foo/bar/internal/baz:module.bzl", "b")

b()
a = 1 / 2
d = {"b": 2, "a": 1}
attr.foo(bar, cfg = "data")
EOF

cat > test_dir/fixed_golden.bzl <<EOF
load("//foo/bar/internal/baz:module.bzl", "b")

b()
a = 1 // 2
d = {"b": 2, "a": 1}
attr.foo(bar)
EOF

cat > test_dir/fixed_golden_all.bzl <<EOF
load("//foo/bar/internal/baz:module.bzl", "b")

b()
a = 1 // 2
d = {"a": 1, "b": 2}
attr.foo(bar)
EOF

cat > test_dir/fixed_golden_dict_cfg.bzl <<EOF
load("//foo/bar/internal/baz:module.bzl", "b")

b()
a = 1 / 2
d = {"a": 1, "b": 2}
attr.foo(bar)
EOF

cat > test_dir/fixed_golden_cfg.bzl <<EOF
load("//foo/bar/internal/baz:module.bzl", "b")

b()
a = 1 / 2
d = {"b": 2, "a": 1}
attr.foo(bar)
EOF

cat > test_dir/fix_report_golden <<EOF
test_dir/to_fix_tmp.bzl: applied fixes, 2 warnings left
fixed test_dir/to_fix_tmp.bzl
EOF

error_bzl="test_dir/to_fix_tmp.bzl:1: bzl-visibility: Module \"//foo/bar/internal/baz:module.bzl\" can only be loaded from files located inside \"//foo/bar\", not from \"//to_fix_tmp.bzl\". (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#bzl-visibility)"
error_docstring="test_dir/to_fix_tmp.bzl:1: module-docstring: The file has no module docstring."$'\n'"A module docstring is a string literal (not a comment) which should be the first statement of a file (it may follow comment lines). (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#module-docstring)"
error_integer="test_dir/to_fix_tmp.bzl:4: integer-division: The \"/\" operator for integer division is deprecated in favor of \"//\". (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#integer-division)"
error_dict="test_dir/to_fix_tmp.bzl:5: unsorted-dict-items: Dictionary items are out of their lexicographical order. (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#unsorted-dict-items)"
error_cfg="test_dir/to_fix_tmp.bzl:6: attr-cfg: cfg = \"data\" for attr definitions has no effect and should be removed. (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#attr-cfg)"

test_lint () {
  ret=0
  cp test_dir/to_fix.bzl test_dir/to_fix_tmp.bzl
  echo "$4" > golden/error_golden
  with_replaced_dir="${4//test_dir/another_test_dir}"
  echo "${with_replaced_dir//\/\/to_fix_tmp.bzl///another_test_dir/to_fix_tmp.bzl}" > golden/error_golden_another

  cat > golden/fix_report_golden <<EOF
test_dir/to_fix_tmp.bzl: applied fixes, $5 warnings left
fixed test_dir/to_fix_tmp.bzl
EOF

  # --lint=warn with --mode=check
  $buildifier --mode=check --lint=warn $2 test_dir/to_fix_tmp.bzl 2> test_dir/error || ret=$?
  if [[ $ret -ne 4 ]]; then
    die "$1: warn: Expected buildifier to exit with 4, actual: $ret"
  fi
  diff -u test_dir/error golden/error_golden || die "$1: wrong console output for --mode=check --lint=warn"
  diff -u test_dir/to_fix.bzl test_dir/to_fix.bzl || die "$1: --mode=check --lint=warn shouldn't modify files"

  # --lint=warn
  $buildifier --lint=warn $2 test_dir/to_fix_tmp.bzl 2> test_dir/error || ret=$?
  if [[ $ret -ne 4 ]]; then
    die "$1: warn: Expected buildifier to exit with 4, actual: $ret"
  fi
  diff -u test_dir/error golden/error_golden || die "$1: wrong console output for --lint=warn"

  # --lint=warn with --path
  $buildifier --lint=warn --path=another_test_dir/to_fix_tmp.bzl $2 test_dir/to_fix_tmp.bzl 2> test_dir/error || ret=$?
  if [[ $ret -ne 4 ]]; then
    die "$1: warn: Expected buildifier to exit with 4, actual: $ret"
  fi
  diff -u test_dir/error golden/error_golden_another || die "$1: wrong console output for --lint=warn and --path"

  # --lint=fix
  $buildifier --lint=fix $2 -v test_dir/to_fix_tmp.bzl 2> test_dir/fix_report || ret=$?
  if [[ $ret -ne 4 ]]; then
    die "$1: fix: Expected buildifier to exit with 4, actual: $ret"
  fi
  diff -u test_dir/to_fix_tmp.bzl $3 || die "$1: wrong file output for --lint=fix"
  diff -u test_dir/fix_report golden/fix_report_golden || die "$1: wrong console output for --lint=fix"
}

test_lint "default" "" "test_dir/fixed_golden.bzl" "$error_bzl"$'\n'"$error_docstring"$'\n'"$error_integer"$'\n'"$error_cfg" 2
test_lint "all" "--warnings=all" "test_dir/fixed_golden_all.bzl" "$error_bzl"$'\n'"$error_docstring"$'\n'"$error_integer"$'\n'"$error_dict"$'\n'"$error_cfg" 2
test_lint "cfg" "--warnings=attr-cfg" "test_dir/fixed_golden_cfg.bzl" "$error_cfg" 0
test_lint "custom" "--warnings=-bzl-visibility,-integer-division,+unsorted-dict-items" "test_dir/fixed_golden_dict_cfg.bzl" "$error_docstring"$'\n'"$error_dict"$'\n'"$error_cfg" 1

# Test --format=json

mkdir test_dir/json
cp test_dir/to_fix.bzl test_dir/json

# just not formatted
cat > test_dir/json/to_fix_2.bzl <<EOF
a=b
EOF

# not formatted with rewrites
cat > test_dir/json/to_fix_3.bzl <<EOF
x = 0123
EOF

# formatted, no warnings
cat > test_dir/json/to_fix_4.bzl <<EOF
a = b
EOF

# not a starlark file
cat > test_dir/json/foo.bar <<EOF
this is not a starlark file
EOF

cat > golden/json_report_golden <<EOF
{
    "success": false,
    "files": [
        {
            "filename": "to_fix.bzl",
            "formatted": true,
            "valid": true,
            "warnings": [
                {
                    "start": {
                        "line": 1,
                        "column": 6
                    },
                    "end": {
                        "line": 1,
                        "column": 41
                    },
                    "category": "bzl-visibility",
                    "actionable": true,
                    "autoFixable": false,
                    "message": "Module \"//foo/bar/internal/baz:module.bzl\" can only be loaded from files located inside \"//foo/bar\", not from \"//json/to_fix.bzl\".",
                    "url": "https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#bzl-visibility"
                },
                {
                    "start": {
                        "line": 4,
                        "column": 5
                    },
                    "end": {
                        "line": 4,
                        "column": 10
                    },
                    "category": "integer-division",
                    "actionable": true,
                    "autoFixable": true,
                    "message": "The \"/\" operator for integer division is deprecated in favor of \"//\".",
                    "url": "https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#integer-division"
                },
                {
                    "start": {
                        "line": 6,
                        "column": 15
                    },
                    "end": {
                        "line": 6,
                        "column": 27
                    },
                    "category": "attr-cfg",
                    "actionable": true,
                    "autoFixable": true,
                    "message": "cfg = \"data\" for attr definitions has no effect and should be removed.",
                    "url": "https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#attr-cfg"
                }
            ]
        },
        {
            "filename": "to_fix_2.bzl",
            "formatted": false,
            "valid": true,
            "warnings": []
        },
        {
            "filename": "to_fix_3.bzl",
            "formatted": false,
            "valid": true,
            "warnings": []
        },
        {
            "filename": "to_fix_4.bzl",
            "formatted": true,
            "valid": true,
            "warnings": []
        }
    ]
}
EOF

cat > golden/json_report_small_golden <<EOF
{
    "success": true,
    "files": [
        {
            "filename": "to_fix_4.bzl",
            "formatted": true,
            "valid": true,
            "warnings": []
        }
    ]
}
EOF

cat > golden/json_report_stdin_golden <<EOF
{
    "success": true,
    "files": [
        {
            "filename": "\u003cstdin\u003e",
            "formatted": true,
            "valid": true,
            "warnings": []
        }
    ]
}
EOF

cat > golden/json_report_invalid_file_golden <<EOF
{
    "success": false,
    "files": [
        {
            "filename": "to_fix_4.bzl",
            "formatted": true,
            "valid": true,
            "warnings": []
        },
        {
            "filename": "foo.bar",
            "formatted": false,
            "valid": false,
            "warnings": []
        }
    ]
}
EOF

cd test_dir/json

$buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v to_fix.bzl to_fix_2.bzl to_fix_3.bzl to_fix_4.bzl > json_report
diff -u json_report ../../golden/json_report_golden || die "$1: wrong console output for --mode=check --format=json --lint=warn with many files"

$buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v to_fix_4.bzl > json_report
diff -u json_report ../../golden/json_report_small_golden || die "$1: wrong console output for --mode=check --format=json --lint=warn with a single file"

$buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v < to_fix_4.bzl > json_report
diff -u json_report ../../golden/json_report_stdin_golden || die "$1: wrong console output for --mode=check --format=json --lint=warn with stdin"

$buildifier --mode=check --format=json --lint=warn --warnings=-module-docstring -v to_fix_4.bzl foo.bar > json_report
diff -u json_report ../../golden/json_report_invalid_file_golden || die "$1: wrong console output for --mode=check --format=json --lint=warn with an invalid file"

cd ../..

# Test the multifile functionality

mkdir multifile
cd multifile

cat > lib.bzl <<EOF
def foo():
  """
  This is a function foo.

  Please use it in favor of the
  deprecated function bar
  """
  pass

def bar():
  """
  This is a function bar.

  Deprecated:
    please use foo instead.
  """
  pass
EOF

touch WORKSPACE

cat > BUILD <<EOF
load(":lib.bzl", "foo", "bar")
load(":nonexistent.bzl", "foo2", "bar2")
EOF

cat > report_golden <<EOF
BUILD:1: deprecated-function: The function "bar" defined in "//lib.bzl" is deprecated. (https://github.com/bazelbuild/buildtools/blob/main/WARNINGS.md#deprecated-function)
EOF

$buildifier --lint=warn --warnings=deprecated-function BUILD 2> report || ret=$?
diff -u report_golden report || die "$1: wrong console output for multifile warnings (WORKSPACE exists)"
