// Copyright 2020 Google Inc. 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 bp2build

import (
	"android/soong/android"
	"android/soong/genrule"
	"strings"
	"testing"
)

func TestGenerateSoongModuleTargets(t *testing.T) {
	testCases := []struct {
		bp                  string
		expectedBazelTarget string
	}{
		{
			bp: `custom {
	name: "foo",
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
)`,
		},
		{
			bp: `custom {
	name: "foo",
	ramdisk: true,
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    ramdisk = True,
)`,
		},
		{
			bp: `custom {
	name: "foo",
	owner: "a_string_with\"quotes\"_and_\\backslashes\\\\",
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    owner = "a_string_with\"quotes\"_and_\\backslashes\\\\",
)`,
		},
		{
			bp: `custom {
	name: "foo",
	required: ["bar"],
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    required = [
        "bar",
    ],
)`,
		},
		{
			bp: `custom {
	name: "foo",
	target_required: ["qux", "bazqux"],
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    target_required = [
        "qux",
        "bazqux",
    ],
)`,
		},
		{
			bp: `custom {
	name: "foo",
	dist: {
		targets: ["goal_foo"],
		tag: ".foo",
	},
	dists: [
		{
			targets: ["goal_bar"],
			tag: ".bar",
		},
	],
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    dist = {
        "tag": ".foo",
        "targets": [
            "goal_foo",
        ],
    },
    dists = [
        {
            "tag": ".bar",
            "targets": [
                "goal_bar",
            ],
        },
    ],
)`,
		},
		{
			bp: `custom {
	name: "foo",
	required: ["bar"],
	target_required: ["qux", "bazqux"],
	ramdisk: true,
	owner: "custom_owner",
	dists: [
		{
			tag: ".tag",
			targets: ["my_goal"],
		},
	],
}
		`,
			expectedBazelTarget: `soong_module(
    name = "foo",
    soong_module_name = "foo",
    soong_module_type = "custom",
    soong_module_variant = "",
    soong_module_deps = [
    ],
    dists = [
        {
            "tag": ".tag",
            "targets": [
                "my_goal",
            ],
        },
    ],
    owner = "custom_owner",
    ramdisk = True,
    required = [
        "bar",
    ],
    target_required = [
        "qux",
        "bazqux",
    ],
)`,
		},
	}

	dir := "."
	for _, testCase := range testCases {
		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
		ctx := android.NewTestContext(config)

		ctx.RegisterModuleType("custom", customModuleFactory)
		ctx.Register()

		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
		android.FailIfErrored(t, errs)
		_, errs = ctx.PrepareBuildActions(config)
		android.FailIfErrored(t, errs)

		codegenCtx := NewCodegenContext(config, *ctx.Context, QueryView)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)
		if actualCount, expectedCount := len(bazelTargets), 1; actualCount != expectedCount {
			t.Fatalf("Expected %d bazel target, got %d", expectedCount, actualCount)
		}

		actualBazelTarget := bazelTargets[0]
		if actualBazelTarget.content != testCase.expectedBazelTarget {
			t.Errorf(
				"Expected generated Bazel target to be '%s', got '%s'",
				testCase.expectedBazelTarget,
				actualBazelTarget.content,
			)
		}
	}
}

func TestGenerateBazelTargetModules(t *testing.T) {
	testCases := []struct {
		bp                  string
		expectedBazelTarget string
	}{
		{
			bp: `custom {
	name: "foo",
    string_list_prop: ["a", "b"],
    string_prop: "a",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTarget: `custom(
    name = "foo",
    string_list_prop = [
        "a",
        "b",
    ],
    string_prop = "a",
)`,
		},
		{
			bp: `custom {
	name: "control_characters",
    string_list_prop: ["\t", "\n"],
    string_prop: "a\t\n\r",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTarget: `custom(
    name = "control_characters",
    string_list_prop = [
        "\t",
        "\n",
    ],
    string_prop = "a\t\n\r",
)`,
		},
	}

	dir := "."
	for _, testCase := range testCases {
		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
		ctx := android.NewTestContext(config)

		ctx.RegisterModuleType("custom", customModuleFactory)
		ctx.RegisterBp2BuildMutator("custom", customBp2BuildMutator)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
		if Errored(t, "", errs) {
			continue
		}
		_, errs = ctx.ResolveDependencies(config)
		if Errored(t, "", errs) {
			continue
		}

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)

		if actualCount, expectedCount := len(bazelTargets), 1; actualCount != expectedCount {
			t.Errorf("Expected %d bazel target, got %d", expectedCount, actualCount)
		} else {
			actualBazelTarget := bazelTargets[0]
			if actualBazelTarget.content != testCase.expectedBazelTarget {
				t.Errorf(
					"Expected generated Bazel target to be '%s', got '%s'",
					testCase.expectedBazelTarget,
					actualBazelTarget.content,
				)
			}
		}
	}
}

func TestLoadStatements(t *testing.T) {
	testCases := []struct {
		bazelTargets           BazelTargets
		expectedLoadStatements string
	}{
		{
			bazelTargets: BazelTargets{
				BazelTarget{
					name:            "foo",
					ruleClass:       "cc_library",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
			},
			expectedLoadStatements: `load("//build/bazel/rules:cc.bzl", "cc_library")`,
		},
		{
			bazelTargets: BazelTargets{
				BazelTarget{
					name:            "foo",
					ruleClass:       "cc_library",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
				BazelTarget{
					name:            "bar",
					ruleClass:       "cc_library",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
			},
			expectedLoadStatements: `load("//build/bazel/rules:cc.bzl", "cc_library")`,
		},
		{
			bazelTargets: BazelTargets{
				BazelTarget{
					name:            "foo",
					ruleClass:       "cc_library",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
				BazelTarget{
					name:            "bar",
					ruleClass:       "cc_binary",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
			},
			expectedLoadStatements: `load("//build/bazel/rules:cc.bzl", "cc_binary", "cc_library")`,
		},
		{
			bazelTargets: BazelTargets{
				BazelTarget{
					name:            "foo",
					ruleClass:       "cc_library",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
				BazelTarget{
					name:            "bar",
					ruleClass:       "cc_binary",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
				BazelTarget{
					name:            "baz",
					ruleClass:       "java_binary",
					bzlLoadLocation: "//build/bazel/rules:java.bzl",
				},
			},
			expectedLoadStatements: `load("//build/bazel/rules:cc.bzl", "cc_binary", "cc_library")
load("//build/bazel/rules:java.bzl", "java_binary")`,
		},
		{
			bazelTargets: BazelTargets{
				BazelTarget{
					name:            "foo",
					ruleClass:       "cc_binary",
					bzlLoadLocation: "//build/bazel/rules:cc.bzl",
				},
				BazelTarget{
					name:            "bar",
					ruleClass:       "java_binary",
					bzlLoadLocation: "//build/bazel/rules:java.bzl",
				},
				BazelTarget{
					name:      "baz",
					ruleClass: "genrule",
					// Note: no bzlLoadLocation for native rules
				},
			},
			expectedLoadStatements: `load("//build/bazel/rules:cc.bzl", "cc_binary")
load("//build/bazel/rules:java.bzl", "java_binary")`,
		},
	}

	for _, testCase := range testCases {
		actual := testCase.bazelTargets.LoadStatements()
		expected := testCase.expectedLoadStatements
		if actual != expected {
			t.Fatalf("Expected load statements to be %s, got %s", expected, actual)
		}
	}

}

func TestGenerateBazelTargetModules_OneToMany_LoadedFromStarlark(t *testing.T) {
	testCases := []struct {
		bp                       string
		expectedBazelTarget      string
		expectedBazelTargetCount int
		expectedLoadStatements   string
	}{
		{
			bp: `custom {
    name: "bar",
    bazel_module: { bp2build_available: true  },
}`,
			expectedBazelTarget: `my_library(
    name = "bar",
)

my_proto_library(
    name = "bar_my_proto_library_deps",
)

proto_library(
    name = "bar_proto_library_deps",
)`,
			expectedBazelTargetCount: 3,
			expectedLoadStatements: `load("//build/bazel/rules:proto.bzl", "my_proto_library", "proto_library")
load("//build/bazel/rules:rules.bzl", "my_library")`,
		},
	}

	dir := "."
	for _, testCase := range testCases {
		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
		ctx := android.NewTestContext(config)
		ctx.RegisterModuleType("custom", customModuleFactory)
		ctx.RegisterBp2BuildMutator("custom", customBp2BuildMutatorFromStarlark)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
		android.FailIfErrored(t, errs)
		_, errs = ctx.ResolveDependencies(config)
		android.FailIfErrored(t, errs)

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)
		if actualCount := len(bazelTargets); actualCount != testCase.expectedBazelTargetCount {
			t.Fatalf("Expected %d bazel target, got %d", testCase.expectedBazelTargetCount, actualCount)
		}

		actualBazelTargets := bazelTargets.String()
		if actualBazelTargets != testCase.expectedBazelTarget {
			t.Errorf(
				"Expected generated Bazel target to be '%s', got '%s'",
				testCase.expectedBazelTarget,
				actualBazelTargets,
			)
		}

		actualLoadStatements := bazelTargets.LoadStatements()
		if actualLoadStatements != testCase.expectedLoadStatements {
			t.Errorf(
				"Expected generated load statements to be '%s', got '%s'",
				testCase.expectedLoadStatements,
				actualLoadStatements,
			)
		}
	}
}

func TestModuleTypeBp2Build(t *testing.T) {
	otherGenruleBp := map[string]string{
		"other/Android.bp": `genrule {
    name: "foo.tool",
    out: ["foo_tool.out"],
    srcs: ["foo_tool.in"],
    cmd: "cp $(in) $(out)",
}
genrule {
    name: "other.tool",
    out: ["other_tool.out"],
    srcs: ["other_tool.in"],
    cmd: "cp $(in) $(out)",
}`,
	}

	testCases := []struct {
		description                        string
		moduleTypeUnderTest                string
		moduleTypeUnderTestFactory         android.ModuleFactory
		moduleTypeUnderTestBp2BuildMutator func(android.TopDownMutatorContext)
		preArchMutators                    []android.RegisterMutatorFunc
		depsMutators                       []android.RegisterMutatorFunc
		bp                                 string
		expectedBazelTargets               []string
		fs                                 map[string]string
		dir                                string
	}{
		{
			description:                        "filegroup with does not specify srcs",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{
				`filegroup(
    name = "fg_foo",
)`,
			},
		},
		{
			description:                        "filegroup with no srcs",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    srcs: [],
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{
				`filegroup(
    name = "fg_foo",
)`,
			},
		},
		{
			description:                        "filegroup with srcs",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    srcs: ["a", "b"],
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`filegroup(
    name = "fg_foo",
    srcs = [
        "a",
        "b",
    ],
)`,
			},
		},
		{
			description:                        "filegroup with excludes srcs",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    srcs: ["a", "b"],
    exclude_srcs: ["a"],
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`filegroup(
    name = "fg_foo",
    srcs = [
        "b",
    ],
)`,
			},
		},
		{
			description:                        "filegroup with glob",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foo",
    srcs: ["**/*.txt"],
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`filegroup(
    name = "foo",
    srcs = [
        "other/a.txt",
        "other/b.txt",
        "other/subdir/a.txt",
    ],
)`,
			},
			fs: map[string]string{
				"other/a.txt":        "",
				"other/b.txt":        "",
				"other/subdir/a.txt": "",
				"other/file":         "",
			},
		},
		{
			description:                        "filegroup with glob in subdir",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foo",
    srcs: ["a.txt"],
    bazel_module: { bp2build_available: true },
}`,
			dir: "other",
			expectedBazelTargets: []string{`filegroup(
    name = "fg_foo",
    srcs = [
        "a.txt",
        "b.txt",
        "subdir/a.txt",
    ],
)`,
			},
			fs: map[string]string{
				"other/Android.bp": `filegroup {
    name: "fg_foo",
    srcs: ["**/*.txt"],
    bazel_module: { bp2build_available: true },
}`,
				"other/a.txt":        "",
				"other/b.txt":        "",
				"other/subdir/a.txt": "",
				"other/file":         "",
			},
		},
		{
			description:                        "depends_on_other_dir_module",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foobar",
    srcs: [
      ":foo",
        "c",
    ],
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`filegroup(
    name = "foobar",
    srcs = [
        "//other:foo",
        "c",
    ],
)`,
			},
			fs: map[string]string{
				"other/Android.bp": `filegroup {
    name: "foo",
    srcs: ["a", "b"],
}`,
			},
		},
		{
			description:                        "genrule with command line variable replacements",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo.tool",
    out: ["foo_tool.out"],
    srcs: ["foo_tool.in"],
    cmd: "cp $(in) $(out)",
    bazel_module: { bp2build_available: true },
}

genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    tools: [":foo.tool"],
    cmd: "$(location :foo.tool) --genDir=$(genDir) arg $(in) $(out)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{
				`genrule(
    name = "foo",
    cmd = "$(location :foo.tool) --genDir=$(GENDIR) arg $(SRCS) $(OUTS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
    tools = [
        ":foo.tool",
    ],
)`,
				`genrule(
    name = "foo.tool",
    cmd = "cp $(SRCS) $(OUTS)",
    outs = [
        "foo_tool.out",
    ],
    srcs = [
        "foo_tool.in",
    ],
)`,
			},
		},
		{
			description:                        "genrule using $(locations :label)",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo.tools",
    out: ["foo_tool.out", "foo_tool2.out"],
    srcs: ["foo_tool.in"],
    cmd: "cp $(in) $(out)",
    bazel_module: { bp2build_available: true },
}

genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    tools: [":foo.tools"],
    cmd: "$(locations :foo.tools) -s $(out) $(in)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "$(locations :foo.tools) -s $(OUTS) $(SRCS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
    tools = [
        ":foo.tools",
    ],
)`,
				`genrule(
    name = "foo.tools",
    cmd = "cp $(SRCS) $(OUTS)",
    outs = [
        "foo_tool.out",
        "foo_tool2.out",
    ],
    srcs = [
        "foo_tool.in",
    ],
)`,
			},
		},
		{
			description:                        "genrule using $(locations //absolute:label)",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    tool_files: [":foo.tool"],
    cmd: "$(locations :foo.tool) -s $(out) $(in)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
    tools = [
        "//other:foo.tool",
    ],
)`,
			},
			fs: otherGenruleBp,
		},
		{
			description:                        "genrule srcs using $(locations //absolute:label)",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: [":other.tool"],
    tool_files: [":foo.tool"],
    cmd: "$(locations :foo.tool) -s $(out) $(location :other.tool)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(location //other:other.tool)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "//other:other.tool",
    ],
    tools = [
        "//other:foo.tool",
    ],
)`,
			},
			fs: otherGenruleBp,
		},
		{
			description:                        "genrule using $(location) label should substitute first tool label automatically",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    tool_files: [":foo.tool", ":other.tool"],
    cmd: "$(location) -s $(out) $(in)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "$(location //other:foo.tool) -s $(OUTS) $(SRCS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
    tools = [
        "//other:foo.tool",
        "//other:other.tool",
    ],
)`,
			},
			fs: otherGenruleBp,
		},
		{
			description:                        "genrule using $(locations) label should substitute first tool label automatically",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    tools: [":foo.tool", ":other.tool"],
    cmd: "$(locations) -s $(out) $(in)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "$(locations //other:foo.tool) -s $(OUTS) $(SRCS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
    tools = [
        "//other:foo.tool",
        "//other:other.tool",
    ],
)`,
			},
			fs: otherGenruleBp,
		},
		{
			description:                        "genrule without tools or tool_files can convert successfully",
			moduleTypeUnderTest:                "genrule",
			moduleTypeUnderTestFactory:         genrule.GenRuleFactory,
			moduleTypeUnderTestBp2BuildMutator: genrule.GenruleBp2Build,
			depsMutators:                       []android.RegisterMutatorFunc{genrule.RegisterGenruleBp2BuildDeps},
			bp: `genrule {
    name: "foo",
    out: ["foo.out"],
    srcs: ["foo.in"],
    cmd: "cp $(in) $(out)",
    bazel_module: { bp2build_available: true },
}`,
			expectedBazelTargets: []string{`genrule(
    name = "foo",
    cmd = "cp $(SRCS) $(OUTS)",
    outs = [
        "foo.out",
    ],
    srcs = [
        "foo.in",
    ],
)`,
			},
		},
	}

	dir := "."
	for _, testCase := range testCases {
		fs := make(map[string][]byte)
		toParse := []string{
			"Android.bp",
		}
		for f, content := range testCase.fs {
			if strings.HasSuffix(f, "Android.bp") {
				toParse = append(toParse, f)
			}
			fs[f] = []byte(content)
		}
		config := android.TestConfig(buildDir, nil, testCase.bp, fs)
		ctx := android.NewTestContext(config)
		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
		for _, m := range testCase.depsMutators {
			ctx.DepsBp2BuildMutators(m)
		}
		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, toParse)
		if Errored(t, testCase.description, errs) {
			continue
		}
		_, errs = ctx.ResolveDependencies(config)
		if Errored(t, testCase.description, errs) {
			continue
		}

		checkDir := dir
		if testCase.dir != "" {
			checkDir = testCase.dir
		}

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, checkDir)
		if actualCount, expectedCount := len(bazelTargets), len(testCase.expectedBazelTargets); actualCount != expectedCount {
			t.Errorf("%s: Expected %d bazel target, got %d", testCase.description, expectedCount, actualCount)
		} else {
			for i, target := range bazelTargets {
				if w, g := testCase.expectedBazelTargets[i], target.content; w != g {
					t.Errorf(
						"%s: Expected generated Bazel target to be '%s', got '%s'",
						testCase.description,
						w,
						g,
					)
				}
			}
		}
	}
}

func Errored(t *testing.T, desc string, errs []error) bool {
	t.Helper()
	if len(errs) > 0 {
		for _, err := range errs {
			t.Errorf("%s: %s", desc, err)
		}
		return true
	}
	return false
}

type bp2buildMutator = func(android.TopDownMutatorContext)

func TestBp2BuildInlinesDefaults(t *testing.T) {
	testCases := []struct {
		moduleTypesUnderTest      map[string]android.ModuleFactory
		bp2buildMutatorsUnderTest map[string]bp2buildMutator
		bp                        string
		expectedBazelTarget       string
		description               string
	}{
		{
			moduleTypesUnderTest: map[string]android.ModuleFactory{
				"genrule":          genrule.GenRuleFactory,
				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
			},
			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
				"genrule": genrule.GenruleBp2Build,
			},
			bp: `genrule_defaults {
    name: "gen_defaults",
    cmd: "do-something $(in) $(out)",
}
genrule {
    name: "gen",
    out: ["out"],
    srcs: ["in1"],
    defaults: ["gen_defaults"],
    bazel_module: { bp2build_available: true },
}
`,
			expectedBazelTarget: `genrule(
    name = "gen",
    cmd = "do-something $(SRCS) $(OUTS)",
    outs = [
        "out",
    ],
    srcs = [
        "in1",
    ],
)`,
			description: "genrule applies properties from a genrule_defaults dependency if not specified",
		},
		{
			moduleTypesUnderTest: map[string]android.ModuleFactory{
				"genrule":          genrule.GenRuleFactory,
				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
			},
			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
				"genrule": genrule.GenruleBp2Build,
			},
			bp: `genrule_defaults {
    name: "gen_defaults",
    out: ["out-from-defaults"],
    srcs: ["in-from-defaults"],
    cmd: "cmd-from-defaults",
}
genrule {
    name: "gen",
    out: ["out"],
    srcs: ["in1"],
    defaults: ["gen_defaults"],
    cmd: "do-something $(in) $(out)",
    bazel_module: { bp2build_available: true },
}
`,
			expectedBazelTarget: `genrule(
    name = "gen",
    cmd = "do-something $(SRCS) $(OUTS)",
    outs = [
        "out-from-defaults",
        "out",
    ],
    srcs = [
        "in-from-defaults",
        "in1",
    ],
)`,
			description: "genrule does merges properties from a genrule_defaults dependency, latest-first",
		},
		{
			moduleTypesUnderTest: map[string]android.ModuleFactory{
				"genrule":          genrule.GenRuleFactory,
				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
			},
			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
				"genrule": genrule.GenruleBp2Build,
			},
			bp: `genrule_defaults {
    name: "gen_defaults1",
    cmd: "cp $(in) $(out)",
}

genrule_defaults {
    name: "gen_defaults2",
    srcs: ["in1"],
}

genrule {
    name: "gen",
    out: ["out"],
    defaults: ["gen_defaults1", "gen_defaults2"],
    bazel_module: { bp2build_available: true },
}
`,
			expectedBazelTarget: `genrule(
    name = "gen",
    cmd = "cp $(SRCS) $(OUTS)",
    outs = [
        "out",
    ],
    srcs = [
        "in1",
    ],
)`,
			description: "genrule applies properties from list of genrule_defaults",
		},
		{
			moduleTypesUnderTest: map[string]android.ModuleFactory{
				"genrule":          genrule.GenRuleFactory,
				"genrule_defaults": func() android.Module { return genrule.DefaultsFactory() },
			},
			bp2buildMutatorsUnderTest: map[string]bp2buildMutator{
				"genrule": genrule.GenruleBp2Build,
			},
			bp: `genrule_defaults {
    name: "gen_defaults1",
    defaults: ["gen_defaults2"],
    cmd: "cmd1 $(in) $(out)", // overrides gen_defaults2's cmd property value.
}

genrule_defaults {
    name: "gen_defaults2",
    defaults: ["gen_defaults3"],
    cmd: "cmd2 $(in) $(out)",
    out: ["out-from-2"],
    srcs: ["in1"],
}

genrule_defaults {
    name: "gen_defaults3",
    out: ["out-from-3"],
    srcs: ["srcs-from-3"],
}

genrule {
    name: "gen",
    out: ["out"],
    defaults: ["gen_defaults1"],
    bazel_module: { bp2build_available: true },
}
`,
			expectedBazelTarget: `genrule(
    name = "gen",
    cmd = "cmd1 $(SRCS) $(OUTS)",
    outs = [
        "out-from-3",
        "out-from-2",
        "out",
    ],
    srcs = [
        "in1",
        "srcs-from-3",
    ],
)`,
			description: "genrule applies properties from genrule_defaults transitively",
		},
	}

	dir := "."
	for _, testCase := range testCases {
		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
		ctx := android.NewTestContext(config)
		for m, factory := range testCase.moduleTypesUnderTest {
			ctx.RegisterModuleType(m, factory)
		}
		for mutator, f := range testCase.bp2buildMutatorsUnderTest {
			ctx.RegisterBp2BuildMutator(mutator, f)
		}
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
		android.FailIfErrored(t, errs)
		_, errs = ctx.ResolveDependencies(config)
		android.FailIfErrored(t, errs)

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)
		if actualCount := len(bazelTargets); actualCount != 1 {
			t.Fatalf("%s: Expected 1 bazel target, got %d", testCase.description, actualCount)
		}

		actualBazelTarget := bazelTargets[0]
		if actualBazelTarget.content != testCase.expectedBazelTarget {
			t.Errorf(
				"%s: Expected generated Bazel target to be '%s', got '%s'",
				testCase.description,
				testCase.expectedBazelTarget,
				actualBazelTarget.content,
			)
		}
	}
}

func TestAllowlistingBp2buildTargetsExplicitly(t *testing.T) {
	testCases := []struct {
		moduleTypeUnderTest                string
		moduleTypeUnderTestFactory         android.ModuleFactory
		moduleTypeUnderTestBp2BuildMutator bp2buildMutator
		bp                                 string
		expectedCount                      int
		description                        string
	}{
		{
			description:                        "explicitly unavailable",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foo",
    srcs: ["a", "b"],
    bazel_module: { bp2build_available: false },
}`,
			expectedCount: 0,
		},
		{
			description:                        "implicitly unavailable",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foo",
    srcs: ["a", "b"],
}`,
			expectedCount: 0,
		},
		{
			description:                        "explicitly available",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "foo",
    srcs: ["a", "b"],
    bazel_module: { bp2build_available: true },
}`,
			expectedCount: 1,
		},
		{
			description:                        "generates more than 1 target if needed",
			moduleTypeUnderTest:                "custom",
			moduleTypeUnderTestFactory:         customModuleFactory,
			moduleTypeUnderTestBp2BuildMutator: customBp2BuildMutatorFromStarlark,
			bp: `custom {
    name: "foo",
    bazel_module: { bp2build_available: true },
}`,
			expectedCount: 3,
		},
	}

	dir := "."
	for _, testCase := range testCases {
		config := android.TestConfig(buildDir, nil, testCase.bp, nil)
		ctx := android.NewTestContext(config)
		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, []string{"Android.bp"})
		android.FailIfErrored(t, errs)
		_, errs = ctx.ResolveDependencies(config)
		android.FailIfErrored(t, errs)

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)
		bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)
		if actualCount := len(bazelTargets); actualCount != testCase.expectedCount {
			t.Fatalf("%s: Expected %d bazel target, got %d", testCase.description, testCase.expectedCount, actualCount)
		}
	}
}

func TestAllowlistingBp2buildTargetsWithConfig(t *testing.T) {
	testCases := []struct {
		moduleTypeUnderTest                string
		moduleTypeUnderTestFactory         android.ModuleFactory
		moduleTypeUnderTestBp2BuildMutator bp2buildMutator
		expectedCount                      map[string]int
		description                        string
		bp2buildConfig                     android.Bp2BuildConfig
		checkDir                           string
		fs                                 map[string]string
	}{
		{
			description:                        "test bp2build config package and subpackages config",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			expectedCount: map[string]int{
				"migrated":                           1,
				"migrated/but_not_really":            0,
				"migrated/but_not_really/but_really": 1,
				"not_migrated":                       0,
				"also_not_migrated":                  0,
			},
			bp2buildConfig: android.Bp2BuildConfig{
				"migrated":                android.Bp2BuildDefaultTrueRecursively,
				"migrated/but_not_really": android.Bp2BuildDefaultFalse,
				"not_migrated":            android.Bp2BuildDefaultFalse,
			},
			fs: map[string]string{
				"migrated/Android.bp":                           `filegroup { name: "a" }`,
				"migrated/but_not_really/Android.bp":            `filegroup { name: "b" }`,
				"migrated/but_not_really/but_really/Android.bp": `filegroup { name: "c" }`,
				"not_migrated/Android.bp":                       `filegroup { name: "d" }`,
				"also_not_migrated/Android.bp":                  `filegroup { name: "e" }`,
			},
		},
		{
			description:                        "test bp2build config opt-in and opt-out",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			expectedCount: map[string]int{
				"package-opt-in":             2,
				"package-opt-in/subpackage":  0,
				"package-opt-out":            1,
				"package-opt-out/subpackage": 0,
			},
			bp2buildConfig: android.Bp2BuildConfig{
				"package-opt-in":  android.Bp2BuildDefaultFalse,
				"package-opt-out": android.Bp2BuildDefaultTrueRecursively,
			},
			fs: map[string]string{
				"package-opt-in/Android.bp": `
filegroup { name: "opt-in-a" }
filegroup { name: "opt-in-b", bazel_module: { bp2build_available: true } }
filegroup { name: "opt-in-c", bazel_module: { bp2build_available: true } }
`,

				"package-opt-in/subpackage/Android.bp": `
filegroup { name: "opt-in-d" } // parent package not configured to DefaultTrueRecursively
`,

				"package-opt-out/Android.bp": `
filegroup { name: "opt-out-a" }
filegroup { name: "opt-out-b", bazel_module: { bp2build_available: false } }
filegroup { name: "opt-out-c", bazel_module: { bp2build_available: false } }
`,

				"package-opt-out/subpackage/Android.bp": `
filegroup { name: "opt-out-g", bazel_module: { bp2build_available: false } }
filegroup { name: "opt-out-h", bazel_module: { bp2build_available: false } }
`,
			},
		},
	}

	dir := "."
	for _, testCase := range testCases {
		fs := make(map[string][]byte)
		toParse := []string{
			"Android.bp",
		}
		for f, content := range testCase.fs {
			if strings.HasSuffix(f, "Android.bp") {
				toParse = append(toParse, f)
			}
			fs[f] = []byte(content)
		}
		config := android.TestConfig(buildDir, nil, "", fs)
		ctx := android.NewTestContext(config)
		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
		ctx.RegisterBp2BuildConfig(testCase.bp2buildConfig)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, toParse)
		android.FailIfErrored(t, errs)
		_, errs = ctx.ResolveDependencies(config)
		android.FailIfErrored(t, errs)

		codegenCtx := NewCodegenContext(config, *ctx.Context, Bp2Build)

		// For each directory, test that the expected number of generated targets is correct.
		for dir, expectedCount := range testCase.expectedCount {
			bazelTargets := generateBazelTargetsForDir(codegenCtx, dir)
			if actualCount := len(bazelTargets); actualCount != expectedCount {
				t.Fatalf(
					"%s: Expected %d bazel target for %s package, got %d",
					testCase.description,
					expectedCount,
					dir,
					actualCount)
			}

		}
	}
}

func TestCombineBuildFilesBp2buildTargets(t *testing.T) {
	testCases := []struct {
		description                        string
		moduleTypeUnderTest                string
		moduleTypeUnderTestFactory         android.ModuleFactory
		moduleTypeUnderTestBp2BuildMutator func(android.TopDownMutatorContext)
		preArchMutators                    []android.RegisterMutatorFunc
		depsMutators                       []android.RegisterMutatorFunc
		bp                                 string
		expectedBazelTargets               []string
		fs                                 map[string]string
		dir                                string
	}{
		{
			description:                        "filegroup bazel_module.label",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    bazel_module: { label: "//other:fg_foo" },
}`,
			expectedBazelTargets: []string{
				`// BUILD file`,
			},
			fs: map[string]string{
				"other/BUILD.bazel": `// BUILD file`,
			},
		},
		{
			description:                        "multiple bazel_module.label same BUILD",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    bazel_module: { label: "//other:fg_foo" },
}

filegroup {
    name: "foo",
    bazel_module: { label: "//other:foo" },
}`,
			expectedBazelTargets: []string{
				`// BUILD file`,
			},
			fs: map[string]string{
				"other/BUILD.bazel": `// BUILD file`,
			},
		},
		{
			description:                        "filegroup bazel_module.label and bp2build",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    bazel_module: {
      label: "//other:fg_foo",
      bp2build_available: true,
    },
}`,
			expectedBazelTargets: []string{
				`filegroup(
    name = "fg_foo",
)`,
				`// BUILD file`,
			},
			fs: map[string]string{
				"other/BUILD.bazel": `// BUILD file`,
			},
		},
		{
			description:                        "filegroup bazel_module.label and filegroup bp2build",
			moduleTypeUnderTest:                "filegroup",
			moduleTypeUnderTestFactory:         android.FileGroupFactory,
			moduleTypeUnderTestBp2BuildMutator: android.FilegroupBp2Build,
			bp: `filegroup {
    name: "fg_foo",
    bazel_module: {
      label: "//other:fg_foo",
    },
}

filegroup {
    name: "fg_bar",
    bazel_module: {
      bp2build_available: true,
    },
}`,
			expectedBazelTargets: []string{
				`filegroup(
    name = "fg_bar",
)`,
				`// BUILD file`,
			},
			fs: map[string]string{
				"other/BUILD.bazel": `// BUILD file`,
			},
		},
	}

	dir := "."
	for _, testCase := range testCases {
		fs := make(map[string][]byte)
		toParse := []string{
			"Android.bp",
		}
		for f, content := range testCase.fs {
			if strings.HasSuffix(f, "Android.bp") {
				toParse = append(toParse, f)
			}
			fs[f] = []byte(content)
		}
		config := android.TestConfig(buildDir, nil, testCase.bp, fs)
		ctx := android.NewTestContext(config)
		ctx.RegisterModuleType(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestFactory)
		for _, m := range testCase.depsMutators {
			ctx.DepsBp2BuildMutators(m)
		}
		ctx.RegisterBp2BuildMutator(testCase.moduleTypeUnderTest, testCase.moduleTypeUnderTestBp2BuildMutator)
		ctx.RegisterForBazelConversion()

		_, errs := ctx.ParseFileList(dir, toParse)
		if Errored(t, testCase.description, errs) {
			continue
		}
		_, errs = ctx.ResolveDependencies(config)
		if Errored(t, testCase.description, errs) {
			continue
		}

		checkDir := dir
		if testCase.dir != "" {
			checkDir = testCase.dir
		}
		bazelTargets := generateBazelTargetsForDir(NewCodegenContext(config, *ctx.Context, Bp2Build), checkDir)
		if actualCount, expectedCount := len(bazelTargets), len(testCase.expectedBazelTargets); actualCount != expectedCount {
			t.Errorf("%s: Expected %d bazel target, got %d\n%s", testCase.description, expectedCount, actualCount, bazelTargets)
		} else {
			for i, target := range bazelTargets {
				if w, g := testCase.expectedBazelTargets[i], target.content; w != g {
					t.Errorf(
						"%s: Expected generated Bazel target to be '%s', got '%s'",
						testCase.description,
						w,
						g,
					)
				}
			}
		}
	}
}
