// Copyright 2017 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 python

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"android/soong/android"
	"android/soong/cc"
)

type pyModule struct {
	name          string
	actualVersion string
	pyRunfiles    []string
	srcsZip       string
	depsSrcsZips  []string
}

var (
	buildNamePrefix          = "soong_python_test"
	moduleVariantErrTemplate = `%s: module %q variant "[a-zA-Z0-9_\-]*": `
	pkgPathErrTemplate       = moduleVariantErrTemplate +
		"pkg_path: %q must be a relative path contained in par file."
	badIdentifierErrTemplate = moduleVariantErrTemplate +
		"srcs: the path %q contains invalid subpath %q."
	dupRunfileErrTemplate = moduleVariantErrTemplate +
		"found two files to be placed at the same location within zip %q." +
		" First file: in module %s at path %q." +
		" Second file: in module %s at path %q."
	badSrcFileExtErr     = moduleVariantErrTemplate + `srcs: found non \(.py\|.proto\) file: %q!`
	badDataFileExtErr    = moduleVariantErrTemplate + `data: found \(.py\) file: %q!`
	sharedLibErrTemplate = moduleVariantErrTemplate +
		"shared_libs: shared_libs is not supported for device builds"
	bpFile = "Android.bp"

	data = []struct {
		desc      string
		mockFiles android.MockFS

		errors           []string
		expectedBinaries []pyModule
	}{
		{
			desc: "module with bad src file ext",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library_host {
						name: "lib1",
						srcs: [
							"file1.exe",
						],
					}`,
				),
				"dir/file1.exe": nil,
			},
			errors: []string{
				fmt.Sprintf(badSrcFileExtErr,
					"dir/Android.bp:3:11", "lib1", "dir/file1.exe"),
			},
		},
		{
			desc: "module with bad data file ext",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library_host {
						name: "lib1",
						srcs: [
							"file1.py",
						],
						data: [
							"file2.py",
						],
					}`,
				),
				"dir/file1.py": nil,
				"dir/file2.py": nil,
			},
			errors: []string{
				fmt.Sprintf(badDataFileExtErr,
					"dir/Android.bp:6:11", "lib1", "dir/file2.py"),
			},
		},
		{
			desc: "module with bad pkg_path format",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library_host {
						name: "lib1",
						pkg_path: "a/c/../../",
						srcs: [
							"file1.py",
						],
					}

					python_library_host {
						name: "lib2",
						pkg_path: "a/c/../../../",
						srcs: [
							"file1.py",
						],
					}

					python_library_host {
						name: "lib3",
						pkg_path: "/a/c/../../",
						srcs: [
							"file1.py",
						],
					}`,
				),
				"dir/file1.py": nil,
			},
			errors: []string{
				fmt.Sprintf(pkgPathErrTemplate,
					"dir/Android.bp:11:15", "lib2", "a/c/../../../"),
				fmt.Sprintf(pkgPathErrTemplate,
					"dir/Android.bp:19:15", "lib3", "/a/c/../../"),
			},
		},
		{
			desc: "module with bad runfile src path format",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library_host {
						name: "lib1",
						pkg_path: "a/b/c/",
						srcs: [
							".file1.py",
							"123/file1.py",
							"-e/f/file1.py",
						],
					}`,
				),
				"dir/.file1.py":     nil,
				"dir/123/file1.py":  nil,
				"dir/-e/f/file1.py": nil,
			},
			errors: []string{
				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
					"lib1", "a/b/c/-e/f/file1.py", "-e"),
				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
					"lib1", "a/b/c/.file1.py", ".file1"),
				fmt.Sprintf(badIdentifierErrTemplate, "dir/Android.bp:4:11",
					"lib1", "a/b/c/123/file1.py", "123"),
			},
		},
		{
			desc: "module with duplicate runfile path",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library_host {
						name: "lib1",
						pkg_path: "a/b/",
						srcs: [
							"c/file1.py",
						],
					}

					python_library_host {
						name: "lib2",
						pkg_path: "a/b/c/",
						srcs: [
							"file1.py",
						],
						libs: [
							"lib1",
						],
					}

					python_binary_host {
						name: "bin",
						pkg_path: "e/",
						srcs: [
							"bin.py",
						],
						libs: [
							"lib2",
						],
					}
					`,
				),
				"dir/c/file1.py": nil,
				"dir/file1.py":   nil,
				"dir/bin.py":     nil,
			},
			errors: []string{
				fmt.Sprintf(dupRunfileErrTemplate, "dir/Android.bp:20:6",
					"bin", "a/b/c/file1.py", "bin", "dir/file1.py",
					"lib1", "dir/c/file1.py"),
			},
		},
		{
			desc: "device module with shared libs",
			mockFiles: map[string][]byte{
				filepath.Join("dir", bpFile): []byte(
					`python_library {
						name: "lib1",
						srcs: [
							"file1.py",
						],
						shared_libs: [
							"clib1",
						],
					}`,
				),
				"dir/file1.py": nil,
			},
			errors: []string{
				fmt.Sprintf(sharedLibErrTemplate,
					"dir/Android.bp:6:18", "lib1"),
			},
		},
	}
)

func TestPythonModule(t *testing.T) {
	for _, d := range data {
		d.mockFiles[filepath.Join("common", bpFile)] = []byte(`
python_library {
  name: "py3-stdlib",
  host_supported: true,
}
cc_binary {
  name: "py3-launcher",
  host_supported: true,
}
`)

		t.Run(d.desc, func(t *testing.T) {
			result := android.GroupFixturePreparers(
				android.PrepareForTestWithDefaults,
				android.PrepareForTestWithArchMutator,
				android.PrepareForTestWithAllowMissingDependencies,
				cc.PrepareForTestWithCcDefaultModules,
				PrepareForTestWithPythonBuildComponents,
				d.mockFiles.AddToFixture(),
			).ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern(d.errors)).
				RunTest(t)

			if len(result.Errs) > 0 {
				return
			}

			for _, e := range d.expectedBinaries {
				t.Run(e.name, func(t *testing.T) {
					expectModule(t, result.TestContext, e.name, e.actualVersion, e.srcsZip, e.pyRunfiles)
				})
			}
		})
	}
}

func TestTestOnlyProvider(t *testing.T) {
	t.Parallel()
	ctx := android.GroupFixturePreparers(
		PrepareForTestWithPythonBuildComponents,
		android.PrepareForTestWithAllowMissingDependencies,
	).RunTestWithBp(t, `
                // These should be test-only
                python_library { name: "py-lib-test", test_only: true }
                python_library { name: "py-lib-test-host", test_only: true, host_supported: true }
                python_test {    name: "py-test", srcs: ["py-test.py"] }
                python_test_host { name: "py-test-host", srcs: ["py-test-host.py"] }
                python_binary_host { name: "py-bin-test", srcs: ["py-bin-test.py"] }

                // These should not be.
                python_library { name: "py-lib" }
                python_binary_host { name: "py-bin", srcs: ["py-bin.py"] }
	`)

	// Visit all modules and ensure only the ones that should
	// marked as test-only are marked as test-only.

	actualTestOnly := []string{}
	ctx.VisitAllModules(func(m android.Module) {
		if provider, ok := android.OtherModuleProvider(ctx.TestContext.OtherModuleProviderAdaptor(), m, android.TestOnlyProviderKey); ok {
			if provider.TestOnly {
				actualTestOnly = append(actualTestOnly, m.Name())
			}
		}
	})
	expectedTestOnlyModules := []string{
		"py-lib-test",
		"py-lib-test-host",
		"py-test",
		"py-test-host",
	}

	notEqual, left, right := android.ListSetDifference(expectedTestOnlyModules, actualTestOnly)
	if notEqual {
		t.Errorf("test-only: Expected but not found: %v, Found but not expected: %v", left, right)
	}
}

// Don't allow setting test-only on things that are always tests or never tests.
func TestInvalidTestOnlyTargets(t *testing.T) {
	testCases := []string{
		` python_test { name: "py-test", test_only: true, srcs: ["py-test.py"] } `,
		` python_test_host { name: "py-test-host", test_only: true, srcs: ["py-test-host.py"] } `,
		` python_defaults { name: "py-defaults", test_only: true, srcs: ["foo.py"] } `,
	}

	for i, bp := range testCases {
		ctx := android.GroupFixturePreparers(
			PrepareForTestWithPythonBuildComponents,
			android.PrepareForTestWithAllowMissingDependencies).
			ExtendWithErrorHandler(android.FixtureIgnoreErrors).
			RunTestWithBp(t, bp)
		if len(ctx.Errs) != 1 {
			t.Errorf("Expected err setting test_only in testcase #%d: %d errs", i, len(ctx.Errs))
			continue
		}
		if !strings.Contains(ctx.Errs[0].Error(), "unrecognized property \"test_only\"") {
			t.Errorf("ERR: %s bad bp: %s", ctx.Errs[0], bp)
		}
	}
}

func TestSharedLib(t *testing.T) {
	ctx := android.GroupFixturePreparers(
		android.PrepareForTestWithDefaults,
		android.PrepareForTestWithArchMutator,
		android.PrepareForTestWithAllowMissingDependencies,
		cc.PrepareForTestWithCcDefaultModules,
		PrepareForTestWithPythonBuildComponents,
	).RunTestWithBp(
		t,
		`python_library_host {
			name: "py-lib-host",
			srcs: ["py-lib-host.py"],
			shared_libs: ["clib-host"],
		}

		cc_library_shared {
			name: "clib-host",
			host_supported: true,
			shared_libs: ["clib-host-2"],
		}

		cc_library_shared {
			name: "clib-host-2",
			host_supported: true,
		}`,
	)
	if len(ctx.Errs) > 0 {
		t.Errorf("Expected got: %s, want: 0 errs", ctx.Errs)
	}

	mod, modOk := ctx.ModuleForTests(t, "py-lib-host", ctx.Config.BuildOSTarget.String()).Module().(*PythonLibraryModule)
	if !modOk {
		t.Fatalf("py-lib-host is not Python library!")
	}
	// ensure the shared lib is included in the data path mappings
	dataPathMappings := mod.getDataPathMappings()
	if len(dataPathMappings) != 1 {
		t.Fatalf("expected 1 data file, got: %d", len(dataPathMappings))
	}
	android.AssertStringMatches(
		t,
		"data path included shared lib",
		dataPathMappings[0].dest,
		"clib-host(.so|.dylib)",
	)
	// ensure any dependencies of the shared lib are included in the bundle shared
	// libs
	android.AssertStringMatches(
		t,
		"shared libs",
		mod.getBundleSharedLibs()[0].String(),
		"clib-host-2(.so|.dylib)$",
	)
	android.AssertStringMatches(
		t,
		"shared libs",
		mod.getBundleSharedLibs()[1].String(),
		"libc\\+\\+(.so|.dylib)$",
	)

}

func expectModule(t *testing.T, ctx *android.TestContext, name, variant, expectedSrcsZip string, expectedPyRunfiles []string) {
	module := ctx.ModuleForTests(t, name, variant)

	base, baseOk := module.Module().(*PythonLibraryModule)
	if !baseOk {
		t.Fatalf("%s is not Python module!", name)
	}

	actualPyRunfiles := []string{}
	for _, path := range base.srcsPathMappings {
		actualPyRunfiles = append(actualPyRunfiles, path.dest)
	}

	android.AssertDeepEquals(t, "pyRunfiles", expectedPyRunfiles, actualPyRunfiles)

	android.AssertPathRelativeToTopEquals(t, "srcsZip", expectedSrcsZip, base.srcsZip)
}

func TestMain(m *testing.M) {
	os.Exit(m.Run())
}
