// 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 android

import (
	"strings"
	"testing"

	"github.com/google/blueprint"
	"github.com/google/blueprint/proptools"
)

// Module to be packaged
type componentTestModule struct {
	ModuleBase
	props struct {
		Deps         []string
		Skip_install *bool
		Overrides    []string
	}
}

// dep tag used in this test. All dependencies are considered as installable.
type installDepTag struct {
	blueprint.BaseDependencyTag
	InstallAlwaysNeededDependencyTag
}

func componentTestModuleFactory() Module {
	m := &componentTestModule{}
	m.AddProperties(&m.props)
	InitAndroidArchModule(m, HostAndDeviceSupported, MultilibBoth)
	return m
}

func (m *componentTestModule) DepsMutator(ctx BottomUpMutatorContext) {
	ctx.AddDependency(ctx.Module(), installDepTag{}, m.props.Deps...)
}

func (m *componentTestModule) GenerateAndroidBuildActions(ctx ModuleContext) {
	builtFile := PathForModuleOut(ctx, m.Name())
	dir := ctx.Target().Arch.ArchType.Multilib
	installDir := PathForModuleInstall(ctx, dir)
	if proptools.Bool(m.props.Skip_install) {
		m.SkipInstall()
	}
	ctx.InstallFile(installDir, m.Name(), builtFile)
}

// Module that itself is a package
type packageTestModule struct {
	ModuleBase
	PackagingBase
	properties struct {
		Install_deps []string
	}
	entries []string
}

func packageTestModuleFactory(multiTarget bool, depsCollectFirstTargetOnly bool) Module {
	module := &packageTestModule{}
	InitPackageModule(module)
	module.DepsCollectFirstTargetOnly = depsCollectFirstTargetOnly
	if multiTarget {
		InitAndroidMultiTargetsArchModule(module, DeviceSupported, MultilibCommon)
	} else {
		InitAndroidArchModule(module, DeviceSupported, MultilibBoth)
	}
	module.AddProperties(&module.properties)
	return module
}

type packagingDepTag struct {
	blueprint.BaseDependencyTag
	PackagingItemAlwaysDepTag
}

func (m *packageTestModule) DepsMutator(ctx BottomUpMutatorContext) {
	m.AddDeps(ctx, packagingDepTag{})
	ctx.AddDependency(ctx.Module(), installDepTag{}, m.properties.Install_deps...)
}

func (m *packageTestModule) GenerateAndroidBuildActions(ctx ModuleContext) {
	zipFile := PathForModuleOut(ctx, "myzip.zip")
	m.entries = m.CopyDepsToZip(ctx, m.GatherPackagingSpecs(ctx), zipFile)
}

type testConfig struct {
	multiTarget                bool
	depsCollectFirstTargetOnly bool
	debuggable                 bool
}

func runPackagingTest(t *testing.T, config testConfig, bp string, expected []string) {
	t.Helper()

	var archVariant string
	if config.multiTarget {
		archVariant = "android_common"
	} else {
		archVariant = "android_arm64_armv8-a"
	}

	moduleFactory := func() Module {
		return packageTestModuleFactory(config.multiTarget, config.depsCollectFirstTargetOnly)
	}

	result := GroupFixturePreparers(
		PrepareForTestWithDefaults,
		PrepareForTestWithArchMutator,
		FixtureRegisterWithContext(func(ctx RegistrationContext) {
			ctx.RegisterModuleType("component", componentTestModuleFactory)
			ctx.RegisterModuleType("package_module", moduleFactory)
		}),
		FixtureModifyProductVariables(func(variables FixtureProductVariables) {
			variables.Debuggable = proptools.BoolPtr(config.debuggable)
		}),
		FixtureWithRootAndroidBp(bp),
	).RunTest(t)

	p := result.Module("package", archVariant).(*packageTestModule)
	actual := p.entries
	actual = SortedUniqueStrings(actual)
	expected = SortedUniqueStrings(expected)
	AssertDeepEquals(t, "package entries", expected, actual)
}

func TestPackagingBaseMultiTarget(t *testing.T) {
	config := testConfig{
		multiTarget:                true,
		depsCollectFirstTargetOnly: false,
	}
	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			compile_multilib: "both",
		}
		`, []string{"lib32/foo", "lib32/bar", "lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
			compile_multilib: "32",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				lib32: {
					deps: ["bar"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib32/foo", "lib32/bar", "lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				first: {
					deps: ["bar"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib32/foo", "lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		component {
			name: "baz",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			arch: {
				arm64: {
					deps: ["bar"],
				},
				x86_64: {
					deps: ["baz"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib32/foo", "lib64/foo", "lib64/bar"})
}

func TestPackagingBaseSingleTarget(t *testing.T) {
	config := testConfig{
		multiTarget:                false,
		depsCollectFirstTargetOnly: false,
	}
	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
			compile_multilib: "32",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				lib32: {
					deps: ["bar"],
				},
			},
		}
		`, []string{"lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				lib64: {
					deps: ["bar"],
				},
			},
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		component {
			name: "baz",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			arch: {
				arm64: {
					deps: ["bar"],
				},
				x86_64: {
					deps: ["baz"],
				},
			},
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			install_deps: ["bar"],
		}
		`, []string{"lib64/foo"})
}

func TestPackagingWithSkipInstallDeps(t *testing.T) {
	// package -[dep]-> foo -[dep]-> bar      -[dep]-> baz
	// Packaging should continue transitively through modules that are not installed.
	config := testConfig{
		multiTarget:                false,
		depsCollectFirstTargetOnly: false,
	}
	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
			deps: ["baz"],
			skip_install: true,
		}

		component {
			name: "baz",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo", "lib64/bar", "lib64/baz"})
}

func TestPackagingWithDepsCollectFirstTargetOnly(t *testing.T) {
	config := testConfig{
		multiTarget:                true,
		depsCollectFirstTargetOnly: true,
	}
	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			compile_multilib: "both",
		}
		`, []string{"lib64/foo", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
			compile_multilib: "32",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				lib32: {
					deps: ["bar"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib32/bar", "lib64/foo"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			multilib: {
				both: {
					deps: ["bar"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib64/foo", "lib32/bar", "lib64/bar"})

	runPackagingTest(t, config,
		`
		component {
			name: "foo",
		}

		component {
			name: "bar",
		}

		component {
			name: "baz",
		}

		package_module {
			name: "package",
			deps: ["foo"],
			arch: {
				arm64: {
					deps: ["bar"],
				},
				x86_64: {
					deps: ["baz"],
				},
			},
			compile_multilib: "both",
		}
		`, []string{"lib64/foo", "lib64/bar"})
}

func TestDebuggableDeps(t *testing.T) {
	bp := `
		component {
			name: "foo",
		}

		component {
			name: "bar",
			deps: ["baz"],
		}

		component {
			name: "baz",
		}

		package_module {
			name: "package",
			deps: ["foo"] + select(product_variable("debuggable"), {
				true: ["bar"],
				default: [],
			}),
		}`
	testcases := []struct {
		debuggable bool
		expected   []string
	}{
		{
			debuggable: true,
			expected:   []string{"lib64/foo", "lib64/bar", "lib64/baz"},
		},
		{
			debuggable: false,
			expected:   []string{"lib64/foo"},
		},
	}
	for _, tc := range testcases {
		config := testConfig{
			debuggable: tc.debuggable,
		}
		runPackagingTest(t, config, bp, tc.expected)
	}
}

func TestPrefer32Deps(t *testing.T) {
	bpTemplate := `
		component {
			name: "foo",
			compile_multilib: "both", // not needed but for clarity
		}

		component {
			name: "foo_32only",
			compile_multilib: "prefer32",
		}

		component {
			name: "foo_64only",
			compile_multilib: "64",
		}

		package_module {
			name: "package",
			compile_multilib: "%COMPILE_MULTILIB%",
			multilib: {
				prefer32: {
					deps: %DEPS%,
				},
			},
		}
	`

	testcases := []struct {
		compileMultilib string
		deps            []string
		expected        []string
	}{
		{
			compileMultilib: "first",
			deps:            []string{"foo", "foo_64only"},
			expected:        []string{"lib64/foo", "lib64/foo_64only"},
		},
		{
			compileMultilib: "64",
			deps:            []string{"foo", "foo_64only"},
			expected:        []string{"lib64/foo", "lib64/foo_64only"},
		},
		{
			compileMultilib: "32",
			deps:            []string{"foo", "foo_32only"},
			expected:        []string{"lib32/foo", "lib32/foo_32only"},
		},
		{
			compileMultilib: "both",
			deps:            []string{"foo", "foo_32only", "foo_64only"},
			expected:        []string{"lib32/foo", "lib32/foo_32only", "lib64/foo_64only"},
		},
	}
	for _, tc := range testcases {
		config := testConfig{
			multiTarget:                true,
			depsCollectFirstTargetOnly: true,
		}
		bp := strings.Replace(bpTemplate, "%COMPILE_MULTILIB%", tc.compileMultilib, -1)
		bp = strings.Replace(bp, "%DEPS%", `["`+strings.Join(tc.deps, `", "`)+`"]`, -1)
		runPackagingTest(t, config, bp, tc.expected)
	}
}

func TestOverrides(t *testing.T) {
	bpTemplate := `
		component {
			name: "foo",
			deps: ["bar"],
		}

		component {
			name: "bar",
		}

		component {
			name: "bar_override",
			overrides: ["bar"],
		}

		component {
			name: "baz",
			deps: ["bar_override"],
		}

		package_module {
			name: "package",
			deps: %DEPS%,
		}
	`
	testcases := []struct {
		deps     []string
		expected []string
	}{
		{
			deps:     []string{"foo"},
			expected: []string{"lib64/foo", "lib64/bar"},
		},
		{
			deps:     []string{"foo", "bar_override"},
			expected: []string{"lib64/foo", "lib64/bar_override"},
		},
		{
			deps:     []string{"foo", "bar", "bar_override"},
			expected: []string{"lib64/foo", "lib64/bar_override"},
		},
		{
			deps:     []string{"bar", "bar_override"},
			expected: []string{"lib64/bar_override"},
		},
		{
			deps:     []string{"foo", "baz"},
			expected: []string{"lib64/foo", "lib64/baz", "lib64/bar_override"},
		},
	}
	for _, tc := range testcases {
		config := testConfig{
			multiTarget:                true,
			depsCollectFirstTargetOnly: false,
		}
		bp := strings.Replace(bpTemplate, "%DEPS%", `["`+strings.Join(tc.deps, `", "`)+`"]`, -1)
		runPackagingTest(t, config, bp, tc.expected)
	}
}
