/*******************************************************************************
 * Copyright (c) 2013, 2014 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.container;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.*;
import org.eclipse.osgi.container.Module;
import org.eclipse.osgi.container.ModuleContainer;
import org.eclipse.osgi.report.resolution.ResolutionReport;
import org.eclipse.osgi.tests.container.dummys.*;
import org.junit.Test;
import org.osgi.framework.Constants;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.resource.*;

public class ResolutionReportTest extends AbstractTest {
	// TODO Add test for dynamic resolution in conjunction with UNRESOLVED_PROVIDER entries.

	@Test
	public void testResolutionReportListenerService() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		registerService(org.osgi.framework.hooks.resolver.ResolverHookFactory.class, new DummyResolverHookFactory(hook));
		getSystemBundle().adapt(FrameworkWiring.class).resolveBundles(Collections.singleton(getSystemBundle()));
		assertEquals("No resolution report listener callback", 1, hook.getResolutionReports().size());
		assertNotNull("Resolution report was null", hook.getResolutionReports().get(0));
	}

	@Test
	public void testResolutionReportListenerModule() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module systemBundle = installDummyModule("system.bundle.MF", Constants.SYSTEM_BUNDLE_LOCATION, container);
		container.resolve(Arrays.asList(systemBundle), true);
		assertEquals("No resolution report listener callback", 1, hook.getResolutionReports().size());
		assertNotNull("Resolution report was null", hook.getResolutionReports().get(0));
	}

	@Test
	public void testResolutionReportEntryFilteredByResolverHook() throws Exception {
		DummyResolverHook hook = new DummyResolverHook() {
			@Override
			public void filterResolvable(Collection<BundleRevision> candidates) {
				candidates.clear();
			}
		};
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module module = installDummyModule("resolution.report.a.MF", "resolution.report.a", container);
		assertResolutionDoesNotSucceed(container, Arrays.asList(module));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 1);
		List<ResolutionReport.Entry> entries = resourceToEntries.get(module.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeFilteredByResolverHook(entry.getType());
		assertResolutionReportEntryDataNull(entry.getData());
	}

	@Test
	public void testResolutionReportEntrySingletonSelectionNoneResolved() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReporta = installDummyModule("resolution.report.a.MF", "resolution.report.a", container);
		Module resolutionReportaV1 = installDummyModule("resolution.report.a.v1.MF", "resolution.report.a.v1", container);
		assertResolutionDoesNotSucceed(container, Arrays.asList(resolutionReporta, resolutionReportaV1));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 1);
		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReporta.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeSingletonSelection(entry.getType());
		assertResolutionReportEntryDataNotNull(entry.getData());
	}

	@Test
	public void testResolutionReportEntrySingletonSelectionHighestVersionResolved() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReporta = installDummyModule("resolution.report.a.MF", "resolution.report.a", container);
		Module resolutionReportaV1 = installDummyModule("resolution.report.a.v1.MF", "resolution.report.a.v1", container);
		container.resolve(Arrays.asList(resolutionReportaV1), true);
		clearResolutionReports(hook);
		assertResolutionDoesNotSucceed(container, Arrays.asList(resolutionReporta));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 1);
		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReporta.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeSingletonSelection(entry.getType());
		assertResolutionReportEntryDataNotNull(entry.getData());
	}

	@Test
	public void testResolutionReportEntrySingletonSelectionLowestVersionResolved() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReporta = installDummyModule("resolution.report.a.MF", "resolution.report.a", container);
		container.resolve(Arrays.asList(resolutionReporta), true);
		clearResolutionReports(hook);
		Module resolutionReportaV1 = installDummyModule("resolution.report.a.v1.MF", "resolution.report.a.v1", container);
		assertResolutionDoesNotSucceed(container, Arrays.asList(resolutionReportaV1));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 1);
		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReportaV1.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeSingletonSelection(entry.getType());
		assertResolutionReportEntryDataNotNull(entry.getData());
	}

	@Test
	public void testResolutionReportEntryMissingCapability() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReportB = installDummyModule("resolution.report.b.MF", "resolution.report.b", container);
		assertResolutionDoesNotSucceed(container, Arrays.asList(resolutionReportB));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 1);
		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReportB.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeMissingCapability(entry.getType());
		assertResolutionReportEntryDataMissingCapability(entry.getData(), "osgi.wiring.package", "resolution.report.a");
	}

	@Test
	public void testResolutionReportEntryUnresolvedProvider01() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReportC = installDummyModule("resolution.report.c.MF", "resolution.report.c", container);
		Module resolutionReportD = installDummyModule("resolution.report.d.MF", "resolution.report.d", container);
		assertResolutionDoesNotSucceed(container, Arrays.asList(resolutionReportC, resolutionReportD));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 2);

		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReportC.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeUnresolvedProvider(entry.getType());
		assertResolutionReportEntryDataUnresolvedProvider(entry.getData(), new UnresolvedProviderEntryBuilder().requirement(resolutionReportC.getCurrentRevision().getRequirements("resolution.report.d").get(0)).capability(resolutionReportD.getCurrentRevision().getCapabilities("resolution.report.d").get(0)).build());

		entries = resourceToEntries.get(resolutionReportD.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		entry = entries.get(0);
		assertResolutionReportEntryTypeMissingCapability(entry.getType());
		assertResolutionReportEntryDataMissingCapability(entry.getData(), "does.not.exist", null);
	}

	@Test
	public void testResolutionReportEntryUnresolvedProvider02() throws Exception {
		DummyResolverHook hook = new DummyResolverHook();
		DummyContainerAdaptor adaptor = createDummyAdaptor(hook);
		ModuleContainer container = adaptor.getContainer();
		Module resolutionReportE = installDummyModule("resolution.report.e.MF", "resolution.report.e", container);
		Module resolutionReportF = installDummyModule("resolution.report.f.MF", "resolution.report.f", container);
		Module resolutionReportG = installDummyModule("resolution.report.g.MF", "resolution.report.g", container);
		assertResolutionSucceeds(container, Arrays.asList(resolutionReportG, resolutionReportE, resolutionReportF));
		ResolutionReport report = hook.getResolutionReports().get(0);
		Map<Resource, List<ResolutionReport.Entry>> resourceToEntries = report.getEntries();
		assertResolutionReportEntriesSize(resourceToEntries, 2);

		List<ResolutionReport.Entry> entries = resourceToEntries.get(resolutionReportG.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		ResolutionReport.Entry entry = entries.get(0);
		assertResolutionReportEntryTypeUnresolvedProvider(entry.getType());
		assertResolutionReportEntryDataUnresolvedProvider(entry.getData(), new UnresolvedProviderEntryBuilder().requirement(resolutionReportG.getCurrentRevision().getRequirements(PackageNamespace.PACKAGE_NAMESPACE).get(1)).capability(resolutionReportF.getCurrentRevision().getCapabilities(PackageNamespace.PACKAGE_NAMESPACE).get(0)).build());

		entries = resourceToEntries.get(resolutionReportF.getCurrentRevision());
		assertResolutionReportEntriesSize(entries, 1);
		entry = entries.get(0);
		assertResolutionReportEntryTypeMissingCapability(entry.getType());
		assertResolutionReportEntryDataMissingCapability(entry.getData(), "does.not.exist", null);
	}

	private void clearResolutionReports(DummyResolverHook hook) {
		hook.getResolutionReports().clear();
	}

	// TODO Need to test both mandatory and optional triggers.

	private void assertResolutionDoesNotSucceed(ModuleContainer container, Collection<Module> modules) {
		ResolutionReport report = container.resolve(modules, true);
		assertNotNull("Resolution should not have succeeded", report.getResolutionException());
	}

	private void assertResolutionSucceeds(ModuleContainer container, Collection<Module> modules) {
		ResolutionReport report = container.resolve(modules, false);
		assertNull("Unexpected resolution exception", report.getResolutionException());
	}

	private void assertResolutionReportEntriesNotNull(Map<Resource, List<ResolutionReport.Entry>> entries) {
		assertNotNull("Resolution report entries was null", entries);
	}

	private void assertResolutionReportEntriesSize(Map<Resource, List<ResolutionReport.Entry>> entries, int expected) {
		assertResolutionReportEntriesNotNull(entries);
		assertEquals("Wrong number of total resolution report entries", expected, entries.size());
	}

	private void assertResolutionReportEntriesNotNull(List<ResolutionReport.Entry> entries) {
		assertNotNull("Resolution report entries for resource was null", entries);
	}

	private void assertResolutionReportEntriesSize(List<ResolutionReport.Entry> entries, int expected) {
		assertResolutionReportEntriesNotNull(entries);
		assertEquals("Wrong number of resolution report entries", expected, entries.size());
	}

	private void assertResolutionReportEntryDataNotNull(Object data) {
		assertNotNull("No resolution report entry data", data);
	}

	private void assertResolutionReportEntryDataNull(Object data) {
		assertEquals("Unexpected resolution report entry data", null, data);
	}

	private void assertResolutionReportEntryTypeFilteredByResolverHook(ResolutionReport.Entry.Type type) {
		assertResolutionReportEntryType(ResolutionReport.Entry.Type.FILTERED_BY_RESOLVER_HOOK, type);
	}

	private void assertResolutionReportEntryTypeSingletonSelection(ResolutionReport.Entry.Type type) {
		assertResolutionReportEntryType(ResolutionReport.Entry.Type.SINGLETON_SELECTION, type);
	}

	private void assertResolutionReportEntryTypeUnresolvedProvider(ResolutionReport.Entry.Type type) {
		assertResolutionReportEntryType(ResolutionReport.Entry.Type.UNRESOLVED_PROVIDER, type);
	}

	private void assertResolutionReportEntryDataMissingCapability(Object data, String namespace, String namespaceValue) {
		assertResolutionReportEntryDataNotNull(data);
		assertTrue("Wrong resolution report entry data type", data instanceof Requirement);
		Requirement requirement = (Requirement) data;
		assertEquals("Wrong requirement namespace", namespace, requirement.getNamespace());
		if (namespaceValue == null)
			return;
		assertTrue("Wrong requirement namespace value", requirement.getDirectives().get(Namespace.REQUIREMENT_FILTER_DIRECTIVE).contains(namespace + "=" + namespaceValue));
	}

	@SuppressWarnings("unchecked")
	private void assertResolutionReportEntryDataUnresolvedProvider(Object data, Map<Requirement, List<Capability>> expected) {
		assertResolutionReportEntryDataNotNull(data);
		assertTrue("Wrong resolution report entry data type", data instanceof Map);
		Map<Requirement, Set<Capability>> unresolvedProviders = (Map<Requirement, Set<Capability>>) data;
		assertEquals("Wrong number of unresolved providers", expected.size(), unresolvedProviders.size());
		for (Map.Entry<Requirement, List<Capability>> entry : expected.entrySet()) {
			Set<Capability> actualCapabilities = unresolvedProviders.get(entry.getKey());
			assertEquals("Wrong number of capabilities", entry.getValue().size(), actualCapabilities.size());
			for (Capability c : entry.getValue()) {
				assertTrue("Capability not found", actualCapabilities.contains(c));
			}
		}
	}

	private void assertResolutionReportEntryTypeMissingCapability(ResolutionReport.Entry.Type type) {
		assertResolutionReportEntryType(ResolutionReport.Entry.Type.MISSING_CAPABILITY, type);
	}

	private void assertResolutionReportEntryType(ResolutionReport.Entry.Type expected, ResolutionReport.Entry.Type actual) {
		assertEquals("Wrong resolution report entry type", expected, actual);
	}
}
