// Copyright (C) 2021 The Android Open Source Project
//
// 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 apex

import (
	"reflect"
	"testing"

	"android/soong/android"
	"android/soong/java"
)

// Contains tests for java.CreateClasspathElements logic from java/classpath_element.go that
// requires apexes.

// testClasspathElementContext is a ClasspathElementContext suitable for use in tests.
type testClasspathElementContext struct {
	android.OtherModuleProviderContext
	testContext *android.TestContext
	module      android.ModuleProxy
	errs        []error
}

func (t *testClasspathElementContext) ModuleErrorf(fmt string, args ...interface{}) {
	t.errs = append(t.errs, t.testContext.ModuleErrorf(t.module, fmt, args...))
}

var _ java.ClasspathElementContext = (*testClasspathElementContext)(nil)

func TestCreateClasspathElements(t *testing.T) {
	t.Parallel()
	preparer := android.GroupFixturePreparers(
		prepareForTestWithPlatformBootclasspath,
		prepareForTestWithArtApex,
		prepareForTestWithMyapex,
		prepareForTestWithOtherapex,
		java.PrepareForTestWithJavaSdkLibraryFiles,
		java.FixtureWithLastReleaseApis("foo", "othersdklibrary"),
		java.FixtureConfigureApexBootJars("myapex:bar"),
		android.FixtureWithRootAndroidBp(`
		apex {
			name: "com.android.art",
			key: "com.android.art.key",
 			bootclasspath_fragments: [
				"art-bootclasspath-fragment",
			],
			java_libs: [
				"othersdklibrary",
			],
			updatable: false,
		}

		apex_key {
			name: "com.android.art.key",
			public_key: "com.android.art.avbpubkey",
			private_key: "com.android.art.pem",
		}

		bootclasspath_fragment {
			name: "art-bootclasspath-fragment",
			image_name: "art",
			apex_available: [
				"com.android.art",
			],
			contents: [
				"baz",
				"quuz",
			],
			hidden_api: {
				split_packages: ["*"],
			},
		}

		java_library {
			name: "baz",
			apex_available: [
				"com.android.art",
			],
			srcs: ["b.java"],
			installable: true,
			sdk_version: "core_current",
		}

		java_library {
			name: "quuz",
			apex_available: [
				"com.android.art",
			],
			srcs: ["b.java"],
			installable: true,
		}

		apex {
			name: "myapex",
			key: "myapex.key",
 			bootclasspath_fragments: [
				"mybootclasspath-fragment",
			],
			java_libs: [
				"othersdklibrary",
			],
			updatable: false,
		}

		apex_key {
			name: "myapex.key",
			public_key: "testkey.avbpubkey",
			private_key: "testkey.pem",
		}

		bootclasspath_fragment {
			name: "mybootclasspath-fragment",
			apex_available: [
				"myapex",
			],
			contents: [
				"bar",
			],
			hidden_api: {
				split_packages: ["*"],
			},
		}

		java_library {
			name: "bar",
			srcs: ["b.java"],
			installable: true,
			apex_available: ["myapex"],
			permitted_packages: ["bar"],
		}

		java_sdk_library {
			name: "foo",
			srcs: ["b.java"],
		}

		java_sdk_library {
			name: "othersdklibrary",
			srcs: ["b.java"],
			shared_library: false,
			apex_available: [
				"com.android.art",
				"myapex",
			],
		}

		apex {
			name: "otherapex",
			key: "otherapex.key",
			java_libs: [
				"otherapexlibrary",
			],
			updatable: false,
		}

		apex_key {
			name: "otherapex.key",
			public_key: "testkey.avbpubkey",
			private_key: "testkey.pem",
		}

		java_library {
			name: "otherapexlibrary",
			srcs: ["b.java"],
			installable: true,
			apex_available: ["otherapex"],
			permitted_packages: ["otherapexlibrary"],
		}

		platform_bootclasspath {
			name: "myplatform-bootclasspath",

			fragments: [
				{
					apex: "com.android.art",
					module: "art-bootclasspath-fragment",
				},
				{
					apex: "myapex",
					module: "mybootclasspath-fragment",
				},
			],
		}
	`),
	)

	result := preparer.RunTest(t)

	artFragment := result.ModuleProxy("art-bootclasspath-fragment", "android_common_com.android.art")
	artBaz := result.ModuleProxy("baz", "android_common_apex10000")
	artQuuz := result.ModuleProxy("quuz", "android_common_apex10000")

	myFragment := result.ModuleProxy("mybootclasspath-fragment", "android_common_myapex")
	myBar := result.ModuleProxy("bar", "android_common_apex10000")

	otherApexLibrary := result.ModuleProxy("otherapexlibrary", "android_common_apex10000")

	platformFoo := result.ModuleProxy("quuz", "android_common")

	bootclasspath := result.ModuleProxy("myplatform-bootclasspath", "android_common")

	// Use a custom assertion method instead of AssertDeepEquals as the latter formats the output
	// using %#v which results in meaningless output as ClasspathElements are pointers.
	assertElementsEquals := func(t *testing.T, message string, expected, actual java.ClasspathElements) {
		if !reflect.DeepEqual(expected, actual) {
			t.Errorf("%s: expected:\n  %s\n got:\n  %s", message, expected, actual)
		}
	}

	expectFragmentElement := func(module android.ModuleProxy, contents ...android.ModuleProxy) java.ClasspathElement {
		return &java.ClasspathFragmentElement{module, contents}
	}
	expectLibraryElement := func(module android.ModuleProxy) java.ClasspathElement {
		return &java.ClasspathLibraryElement{module}
	}

	newCtx := func() *testClasspathElementContext {
		return &testClasspathElementContext{
			OtherModuleProviderContext: result.TestContext.OtherModuleProviderAdaptor(),
			testContext:                result.TestContext,
			module:                     bootclasspath,
		}
	}

	// Verify that CreateClasspathElements works when given valid input.
	t.Run("art:baz, art:quuz, my:bar, foo", func(t *testing.T) {
		t.Parallel()
		ctx := newCtx()
		elements := java.CreateClasspathElements(ctx,
			[]android.ModuleProxy{artBaz, artQuuz, myBar, platformFoo},
			[]android.ModuleProxy{artFragment, myFragment},
			map[android.ModuleProxy]string{artBaz: "com.android.art", artQuuz: "com.android.art", myBar: "myapex"},
			map[string]android.ModuleProxy{"com.android.art": artFragment, "myapex": myFragment})
		expectedElements := java.ClasspathElements{
			expectFragmentElement(artFragment, artBaz, artQuuz),
			expectFragmentElement(myFragment, myBar),
			expectLibraryElement(platformFoo),
		}
		assertElementsEquals(t, "elements", expectedElements, elements)
	})

	// Verify that CreateClasspathElements detects when a fragment's contents are not contiguous and
	// are separated by a library from another fragment.
	t.Run("discontiguous separated by fragment", func(t *testing.T) {
		t.Parallel()
		ctx := newCtx()
		elements := java.CreateClasspathElements(ctx,
			[]android.ModuleProxy{artBaz, myBar, artQuuz, platformFoo},
			[]android.ModuleProxy{artFragment, myFragment},
			map[android.ModuleProxy]string{artBaz: "com.android.art", artQuuz: "com.android.art", myBar: "myapex"},
			map[string]android.ModuleProxy{"com.android.art": artFragment, "myapex": myFragment})
		expectedElements := java.ClasspathElements{
			expectFragmentElement(artFragment, artBaz, artQuuz),
			expectFragmentElement(myFragment, myBar),
			expectLibraryElement(platformFoo),
		}
		assertElementsEquals(t, "elements", expectedElements, elements)
		android.FailIfNoMatchingErrors(t, "libraries from the same fragment must be contiguous, however baz{.*} and quuz{os:android,arch:common,apex:apex10000} from fragment art-bootclasspath-fragment{.*} are separated by libraries from fragment mybootclasspath-fragment{.*} like bar{.*}", ctx.errs)
	})

	// Verify that CreateClasspathElements detects when a fragment's contents are not contiguous and
	// are separated by a standalone library.
	t.Run("discontiguous separated by library", func(t *testing.T) {
		t.Parallel()
		ctx := newCtx()
		elements := java.CreateClasspathElements(ctx,
			[]android.ModuleProxy{artBaz, platformFoo, artQuuz, myBar},
			[]android.ModuleProxy{artFragment, myFragment},
			map[android.ModuleProxy]string{artBaz: "com.android.art", artQuuz: "com.android.art", myBar: "myapex"},
			map[string]android.ModuleProxy{"com.android.art": artFragment, "myapex": myFragment})
		expectedElements := java.ClasspathElements{
			expectFragmentElement(artFragment, artBaz, artQuuz),
			expectLibraryElement(platformFoo),
			expectFragmentElement(myFragment, myBar),
		}
		assertElementsEquals(t, "elements", expectedElements, elements)
		android.FailIfNoMatchingErrors(t, "libraries from the same fragment must be contiguous, however baz{.*} and quuz{os:android,arch:common,apex:apex10000} from fragment art-bootclasspath-fragment{.*} are separated by library quuz{.*}", ctx.errs)
	})

	// Verify that CreateClasspathElements detects when there a library on the classpath that
	// indicates it is from an apex the supplied fragments list does not contain a fragment for that
	// apex.
	t.Run("no fragment for apex", func(t *testing.T) {
		t.Parallel()
		ctx := newCtx()
		elements := java.CreateClasspathElements(ctx,
			[]android.ModuleProxy{artBaz, otherApexLibrary},
			[]android.ModuleProxy{artFragment},
			map[android.ModuleProxy]string{artBaz: "com.android.art", otherApexLibrary: "otherapex"},
			map[string]android.ModuleProxy{"com.android.art": artFragment})
		expectedElements := java.ClasspathElements{
			expectFragmentElement(artFragment, artBaz),
		}
		assertElementsEquals(t, "elements", expectedElements, elements)
		android.FailIfNoMatchingErrors(t, `library otherapexlibrary{.*} is from apexes \[otherapex\] which have no corresponding fragment in \[art-bootclasspath-fragment{.*}\]`, ctx.errs)
	})
}
