/*******************************************************************************
 * Copyright (c) 2013, 2020 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.tests.hooks.framework;

import static org.eclipse.osgi.tests.bundles.AbstractBundleTests.stop;
import static org.eclipse.osgi.tests.bundles.AbstractBundleTests.stopQuietly;
import static org.junit.Assert.assertNotEquals;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.osgi.container.ModuleContainerAdaptor.ModuleEvent;
import org.eclipse.osgi.internal.hookregistry.HookRegistry;
import org.eclipse.osgi.tests.OSGiTestsActivator;
import org.eclipse.osgi.tests.bundles.SystemBundleTests;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.resource.Capability;
import org.osgi.service.packageadmin.PackageAdmin;

public class StorageHookTests extends AbstractFrameworkHookTests {
	private static final String TEST_BUNDLE = "test";
	private static final String HOOK_CONFIGURATOR_BUNDLE = "storage.hooks.a";
	private static final String HOOK_CONFIGURATOR_CLASS = "org.eclipse.osgi.tests.hooks.framework.storage.a.TestHookConfigurator";
	private static final String HOOK_CONFIGURATOR_FIELD_CREATE_STORAGE_HOOK_CALLED = "createStorageHookCalled";
	private static final String HOOK_CONFIGURATOR_FIELD_FAIL_LOAD = "failLoad";
	private static final String HOOK_CONFIGURATOR_FIELD_INVALID = "invalid";
	private static final String HOOK_CONFIGURATOR_FIELD_INVALID_FACTORY_CLASS = "invalidFactoryClass";
	private static final String HOOK_CONFIGURATOR_FIELD_VALIDATE_CALLED = "validateCalled";
	private static final String HOOK_CONFIGURATOR_FIELD_DELETING_CALLED = "deletingGenerationCalled";
	private static final String HOOK_CONFIGURATOR_FIELD_ADAPT_MANIFEST = "adaptManifest";
	private static final String HOOK_CONFIGURATOR_FIELD_ADAPT_CAPABILITY_ATTRIBUTE = "adaptCapabilityAttribute";
	private static final String HOOK_CONFIGURATOR_FIELD_REPLACE_BUILDER = "replaceModuleBuilder";
	private static final String HOOK_CONFIGURATOR_FIELD_HANDLE_CONTENT = "handleContentConnection";
	private static final String HOOK_CONFIGURATOR_FIELD_NULL_STORAGE_HOOK = "returnNullStorageHook";

	private Map<String, String> configuration;
	private Framework framework;
	private String location;

	/*
	 * Bundles must be discarded if a storage hook throws an
	 * IllegalStateException during validation.
	 */
	public void testBundleDiscardedWhenClasspathStorageHookInvalidates() throws Exception {
		initAndStartFramework();
		installBundle();
		setStorageHookInvalid(true);
		restartFramework();
		assertStorageHookValidateCalled();
		assertBundleDiscarded();
	}

	/*
	 * Bundles must not be discarded when a storage hook says they are valid.
	 */
	public void testBundleNotDiscardedWhenClasspathStorageHookValidates() throws Exception {
		initAndStartFramework();
		installBundle();
		setStorageHookInvalid(false);
		restartFramework();
		assertStorageHookValidateCalled();
		assertBundleNotDiscarded();
	}

	/*
	 * A storage hook with the wrong factory class should cause bundle
	 * installation to fail.
	 */
	public void testWrongStorageHookFactoryClassOnBundleInstall() throws Exception {
		setFactoryClassInvalid(true);
		initAndStartFramework();
		try {
			installBundle();
			fail("Bundle install should have failed");
		} catch (BundleException e) {
			assertBundleException(e);
		}
		assertCreateStorageHookCalled();
	}

	/*
	 * A storage hook with the wrong factory class should cause bundle update
	 * to fail.
	 */
	public void testWrongStorageHookFactoryClassOnBundleUpdate() throws Exception {
		initAndStartFramework();
		installBundle();
		setFactoryClassInvalid(true);
		try {
			updateBundle();
			fail("Bundle update should have failed");
		} catch (BundleException e) {
			assertBundleException(e);
		}
		assertCreateStorageHookCalled();
	}

	/*
	 * A storage hook with the wrong factory class should cause a framework
	 * restart with persisted bundles to fail.
	 */
	public void testWrongStorageHookFactoryClassOnFrameworkRestart() throws Exception {
		initAndStartFramework();
		installBundle();
		setFactoryClassInvalid(true);
		try {
			restartFramework();
			fail("Framework restart should have failed");
		} catch (IllegalStateException e) {
			assertThrowable(e);
		}
		assertCreateStorageHookCalled();
	}

	public void testCleanOnFailLoad() throws Exception {
		initAndStartFramework();
		installBundle();
		setFactoryHookFailLoad(true);
		restartFramework();
		assertBundleDiscarded();
		// install a bundle without reference to test that the staging area is created correctly after clean
		File bundlesBase = new File(OSGiTestsActivator.getContext().getDataFile(getName()), "bundles");
		bundlesBase.mkdirs();
		framework.getBundleContext().installBundle(SystemBundleTests.createBundle(bundlesBase, getName(), false, false).toURI().toString());
	}

	public void testDeletingGenerationCalledOnDiscard() throws Exception {
		initAndStartFramework();
		installBundle();
		setStorageHookInvalid(true);
		restartFramework();
		assertStorageHookDeletingGenerationCalled();
		assertBundleDiscarded();
	}

	public void testDeletingGenerationCalledUninstall() throws Exception {
		initAndStartFramework();
		installBundle();
		Bundle b = framework.getBundleContext().getBundle(location);
		assertNotNull("Missing test bundle.", b);
		b.uninstall();
		assertStorageHookDeletingGenerationCalled();
	}

	public void testDeletingGenerationCalledUpdate() throws Exception {
		initAndStartFramework();
		installBundle();
		Bundle b = framework.getBundleContext().getBundle(location);
		assertNotNull("Missing test bundle.", b);
		b.update();
		assertStorageHookDeletingGenerationCalled();
	}

	public void testAdaptModuleRevisionBuilder() throws Exception {
		setFactoryClassAdaptManifest(true);
		initAndStartFramework();

		installBundle();
		Bundle b = framework.getBundleContext().getBundle(location);
		assertNotEquals("Wrong ID.", 5678, b.getBundleId());
		assertNotNull("Missing test bundle.", b);
		List<Capability> testCaps = b.adapt(BundleRevision.class).getCapabilities("test.file.path");
		assertEquals("Wrong number of test caps.", 1, testCaps.size());
		String path1 = (String) testCaps.get(0).getAttributes().get("test.file.path");
		assertNotNull("No path", path1);
		String operation1 = (String) testCaps.get(0).getAttributes().get("test.operation");
		assertEquals("Wrong operation", ModuleEvent.INSTALLED.toString(), operation1);
		String location1 = (String) testCaps.get(0).getAttributes().get("test.origin");
		assertEquals("Wrong origin", framework.getBundleContext().getBundle().getLocation(), location1);

		b.update();
		assertNotEquals("Wrong ID.", 5678, b.getBundleId());
		testCaps = b.adapt(BundleRevision.class).getCapabilities("test.file.path");
		assertEquals("Wrong number of test caps.", 1, testCaps.size());
		String path2 = (String) testCaps.get(0).getAttributes().get("test.file.path");
		assertNotNull("No path", path2);
		String operation2 = (String) testCaps.get(0).getAttributes().get("test.operation");
		assertEquals("Wrong operation", ModuleEvent.UPDATED.toString(), operation2);
		String location2 = (String) testCaps.get(0).getAttributes().get("test.origin");
		assertEquals("Wrong origin", location, location2);

		assertNotEquals("Path of updated bundle is the same.", path1, path2);

		stop(framework);

		// create new framework object to test loading of persistent capability.
		framework = createFramework(configuration);
		framework.start();
		b = framework.getBundleContext().getBundle(location);
		assertNotEquals("Wrong ID.", 5678, b.getBundleId());
		testCaps = b.adapt(BundleRevision.class).getCapabilities("test.file.path");
		assertEquals("Wrong number of test caps.", 1, testCaps.size());
		path2 = (String) testCaps.get(0).getAttributes().get("test.file.path");
		assertNotNull("No path", path2);
		operation2 = (String) testCaps.get(0).getAttributes().get("test.operation");
		assertEquals("Wrong operation", ModuleEvent.UPDATED.toString(), operation2);
		location2 = (String) testCaps.get(0).getAttributes().get("test.origin");
		assertEquals("Wrong origin", location, location2);

		setFactoryClassAdaptManifest(false);
		setFactoryClassReplaceBuilder(true);
		b.uninstall();
		installBundle();
		b = framework.getBundleContext().getBundle(location);
		assertNotEquals("Wrong ID.", 5678, b.getBundleId());
		assertNotNull("Missing test bundle.", b);
		assertEquals("Wrong BSN.", "replace", b.getSymbolicName());
		testCaps = b.adapt(BundleRevision.class).getCapabilities("replace");
		assertEquals("Wrong number of capabilities.", 1, testCaps.size());

		setFactoryClassReplaceBuilder(false);
		setFactoryClassAdaptCapabilityAttribute(true);
		b.uninstall();
		installBundle();
		b = framework.getBundleContext().getBundle(location);
		BundleCapability bundleCap = b.adapt(BundleRevision.class).getDeclaredCapabilities(BundleNamespace.BUNDLE_NAMESPACE).iterator().next();
		assertEquals("Wrong attribute value", "testAttribute", bundleCap.getAttributes().get("matching.attribute"));
		assertEquals("Wrong attribute value", "testDirective", bundleCap.getDirectives().get("matching.directive"));
	}

	@SuppressWarnings("deprecation")
	public void testFrameworkUtilHelper() throws Exception {
		initAndStartFramework();
		Class<?> frameworkUtilClass = classLoader.loadClass("org.osgi.framework.FrameworkUtil");
		Bundle b = (Bundle) frameworkUtilClass.getMethod("getBundle", Class.class).invoke(null, String.class);
		assertEquals("Wrong bundle found.", framework.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION), b);
		PackageAdmin packageAdmin = framework.getBundleContext().getService(framework.getBundleContext().getServiceReference(PackageAdmin.class));
		b = packageAdmin.getBundle(String.class);
		assertEquals("Wrong bundle found.", framework.getBundleContext().getBundle(Constants.SYSTEM_BUNDLE_LOCATION), b);
	}

	public void testHandleContent() throws Exception {
		initAndStartFramework();

		// install with an empty stream, the hook will replace it will content to a real bundle
		setFactoryClassHandleContent(true);
		Bundle b = framework.getBundleContext().installBundle("testBundle", new ByteArrayInputStream(new byte[0]));
		assertEquals("Wrong symbolicName", "testHandleContentConnection", b.getSymbolicName());
		b.uninstall();

		// install with no stream, the hook will supply the real content of the bundle
		b = framework.getBundleContext().installBundle("testBundle");
		assertEquals("Wrong symbolicName", "testHandleContentConnection", b.getSymbolicName());
		b.uninstall();

		// tell the hook to no longer handle content, the default behavior of the framework will be used
		setFactoryClassHandleContent(false);
		b = installBundle();
		assertEquals("Wrong symbolicName", "test1", b.getSymbolicName());

		// tell the hook to handle content again, update will update to the content supplied from the hook
		setFactoryClassHandleContent(true);
		b.update(new ByteArrayInputStream(new byte[0]));
		assertEquals("Wrong symbolicName", "testHandleContentConnection", b.getSymbolicName());

		// tell the hook to no longer handle content, update will go back to using content derived from the original location
		setFactoryClassHandleContent(false);
		b.update();
		assertEquals("Wrong symbolicName", "test1", b.getSymbolicName());

		// now update again with hook handling content
		setFactoryClassHandleContent(true);
		b.update();
		assertEquals("Wrong symbolicName", "testHandleContentConnection", b.getSymbolicName());
	}

	public void testNullStorageHook() throws Exception {

		initAndStartFramework();
		File bundlesBase = new File(OSGiTestsActivator.getContext().getDataFile(getName()), "bundles");
		bundlesBase.mkdirs();
		String initialBundleLoc = SystemBundleTests.createBundle(bundlesBase, getName(), false, false).toURI().toString();
		Bundle initialBundle = framework.getBundleContext().installBundle(initialBundleLoc);
		assertNotNull("Expected to have an initial bundle.", initialBundle);

		// Have storage hook factory return null StorageHook
		setFactoryNullStorageHook(true);
		Bundle b = installBundle();
		assertNotNull("Expected to have a bundle after install.", b);
		stop(framework);

		// create new framework to make sure null storage hook works from persistence also.
		framework = createFramework(configuration);
		framework.init();

		initialBundle = framework.getBundleContext().getBundle(initialBundleLoc);
		assertNotNull("Expected to have initial bundle after restart.", initialBundle);
		b = framework.getBundleContext().getBundle(location);
		assertNotNull("Expected to have a bundle after restart.", b);
	}

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		String loc = bundleInstaller.getBundleLocation(HOOK_CONFIGURATOR_BUNDLE);
		loc = loc.substring(loc.indexOf("file:"));
		classLoader.addURL(new URL(loc));
		location = bundleInstaller.getBundleLocation(TEST_BUNDLE);
		File file = OSGiTestsActivator.getContext().getDataFile(getName());
		configuration = new HashMap<>();
		configuration.put(Constants.FRAMEWORK_STORAGE, file.getAbsolutePath());
		configuration.put(HookRegistry.PROP_HOOK_CONFIGURATORS_INCLUDE, HOOK_CONFIGURATOR_CLASS);
		framework = createFramework(configuration);
		resetStorageHook();
	}

	@Override
	protected void tearDown() throws Exception {
		stopQuietly(framework);
		super.tearDown();
	}

	private void assertBundleDiscarded() {
		assertBundleDiscarded(location, framework);
	}

	private void assertBundleException(BundleException e) {
		assertThrowable(e.getCause());
	}

	private void assertBundleNotDiscarded() {
		assertBundleNotDiscarded(location, framework);
	}

	private void assertCreateStorageHookCalled() throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		assertTrue("Storage hook factory createStorageHook not called by framework", clazz.getField(HOOK_CONFIGURATOR_FIELD_CREATE_STORAGE_HOOK_CALLED).getBoolean(null));
	}

	private void assertThrowable(Throwable t) {
		assertTrue("Unexpected exception", t != null && (t instanceof IllegalStateException) && t.getMessage().startsWith("The factory class "));
	}

	private void assertStorageHookValidateCalled() throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		assertTrue("Storage hook validate not called by framework", clazz.getField(HOOK_CONFIGURATOR_FIELD_VALIDATE_CALLED).getBoolean(null));
	}

	private void assertStorageHookDeletingGenerationCalled() throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		assertTrue("Storage hook deletingGeneration not called by framework", clazz.getField(HOOK_CONFIGURATOR_FIELD_DELETING_CALLED).getBoolean(null));
	}

	private void initAndStartFramework() throws Exception {
		initAndStart(framework);
	}

	private Bundle installBundle() throws Exception {
		return framework.getBundleContext().installBundle(location);
	}

	private void resetStorageHook() throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_CREATE_STORAGE_HOOK_CALLED).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_INVALID).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_VALIDATE_CALLED).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_INVALID_FACTORY_CLASS).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_DELETING_CALLED).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_ADAPT_MANIFEST).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_FAIL_LOAD).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_HANDLE_CONTENT).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_REPLACE_BUILDER).set(null, false);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_NULL_STORAGE_HOOK).set(null, false);
	}

	private void restartFramework() throws Exception {
		framework = restart(framework, configuration);
	}

	private void setFactoryClassInvalid(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_INVALID_FACTORY_CLASS).set(null, value);
	}

	private void setStorageHookInvalid(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_INVALID).set(null, value);
	}

	private void setFactoryClassAdaptManifest(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_ADAPT_MANIFEST).set(null, value);
	}

	private void setFactoryClassAdaptCapabilityAttribute(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_ADAPT_CAPABILITY_ATTRIBUTE).set(null, value);
	}

	private void setFactoryClassReplaceBuilder(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_REPLACE_BUILDER).set(null, value);
	}

	private void setFactoryHookFailLoad(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_FAIL_LOAD).set(null, value);
	}

	private void setFactoryClassHandleContent(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_HANDLE_CONTENT).set(null, value);
	}

	private void setFactoryNullStorageHook(boolean value) throws Exception {
		Class<?> clazz = classLoader.loadClass(HOOK_CONFIGURATOR_CLASS);
		clazz.getField(HOOK_CONFIGURATOR_FIELD_NULL_STORAGE_HOOK).set(null, value);
	}

	private void updateBundle() throws Exception {
		framework.getBundleContext().getBundle(location).update();
	}
}
