/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.aries.subsystem.ctt.itests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.osgi.framework.namespace.BundleNamespace.BUNDLE_NAMESPACE;
import static org.osgi.framework.namespace.PackageNamespace.PACKAGE_NAMESPACE;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.aries.subsystem.itests.Header;
import org.apache.aries.subsystem.itests.SubsystemTest;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerMethod;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.wiring.BundleWire;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.service.subsystem.Subsystem;

/*
 * A set of tests to cover OSGi Subsystems CTT section 4, "Subsystem Dependency Tests"
 * This is going to look a bit like ProvisionPolicyTest with a bit of 
 * DependencyLifecycle thrown in. 
 * 
 * 	- The following bundles are used for the tests
	- Bundle A that export package x
	- Bundle B that provides capability y
	- Bundle C that imports package x
	- Bundle D that requires bundle A
	- Bundle E that requires capability y
	- Bundle F that export package x
	- Bundle G that provides capability y
	- The following repositories are defined
	- Repository R1
	  - Bundle A
	  - Bundle B
	  - Bundle C
	  - Bundle D
	  - Bundle E
	  - Bundle F
	  - Bundle G
	- Repository R2
	  - Bundle A
	  - Bundle B
	  - Bundle C
	  - Bundle D
	  - Bundle E
 */

@ExamReactorStrategy(PerMethod.class)
public abstract class SubsystemDependencyTestBase extends SubsystemTest 
{
	protected static String BUNDLE_A = "sdt_bundle.a.jar";
	protected static String BUNDLE_B = "sdt_bundle.b.jar";
	protected static String BUNDLE_C = "sdt_bundle.c.jar";
	protected static String BUNDLE_D = "sdt_bundle.d.jar";
	protected static String BUNDLE_E = "sdt_bundle.e.jar";
	protected static String BUNDLE_F = "sdt_bundle.f.jar";
	protected static String BUNDLE_G = "sdt_bundle.g.jar";

	@Override
	protected void createApplications() throws Exception {
		// We'd like to do this in an @BeforeClass method, but files written in @BeforeClass
		// go into the project's target/ directory whereas those written in @Before go into 
		// paxexam's temp directory, which is where they're needed. 
		createBundleA();
		createBundleB();
		createBundleC();
		createBundleD();
		createBundleE();
		createBundleF();
		createBundleG();
	}
	
	private void createBundleA() throws Exception
	{ 
		createBundle(name(BUNDLE_A), version("1.0.0"), exportPackage("x"));
	}
	
	private void createBundleB() throws Exception
	{
		// TODO: see comment below about bug=true
		createBundle(name(BUNDLE_B), version("1.0.0"), new Header(Constants.PROVIDE_CAPABILITY, "y;y=randomNamespace"));
	}
	
	private void createBundleC() throws Exception
	{
		createBundle(name(BUNDLE_C), version("1.0.0"), importPackage("x"));
	}
	
	private void createBundleD() throws Exception
	{
		createBundle(name(BUNDLE_D), version("1.0.0"), requireBundle(BUNDLE_A));
	}

	// TODO:
	/*
	 * According to the OSGi Core Release 5 spec section 3.3.6 page 35, 
	 *   "A filter is optional, if no filter directive is specified the requirement always matches."
	 *  
	 * If omitted, we first get an NPE in DependencyCalculator.MissingCapability.initializeAttributes(). 
	 * If that's fixed, we get exceptions of the form, 
	 * 
	 *  Caused by: java.lang.IllegalArgumentException: The filter must not be null.
	 *    at org.eclipse.equinox.internal.region.StandardRegionFilterBuilder.allow(StandardRegionFilterBuilder.java:49)
	 *    at org.apache.aries.subsystem.core.internal.SubsystemResource.setImportIsolationPolicy(SubsystemResource.java:655)
     * 
     * This looks to be an Equinox defect - at least in the level of 3.8.0 currently being used by these tests. 
	 */
	private void createBundleE() throws Exception 
	{
		createBundle(name(BUNDLE_E), version("1.0.0"), new Header(Constants.REQUIRE_CAPABILITY, "y"));
	}

	private void createBundleF() throws Exception 
	{
		createBundle(name(BUNDLE_F), version("1.0.0"), exportPackage("x"));
	}
	
	// TODO: see comment above about bug=true
	private void createBundleG() throws Exception 
	{
		createBundle(name(BUNDLE_G), version("1.0.0"), new Header(Constants.PROVIDE_CAPABILITY, "y;y=randomNamespace"));
	}
	
	protected void registerRepositoryR1() throws Exception
	{ 
		registerRepositoryService(BUNDLE_A, BUNDLE_B, 
				BUNDLE_C, BUNDLE_D, BUNDLE_E, BUNDLE_F, BUNDLE_G);
	}
	
	protected void registerRepositoryR2() throws Exception
	{
		registerRepositoryService(BUNDLE_A, BUNDLE_B, 
				BUNDLE_C, BUNDLE_D, BUNDLE_E);
	}
	
	/**
	 *  - Verify that bundles C, D and E in subsystem s wire to A->x, A, B->y respectively
	 */
	protected void checkBundlesCDandEWiredToAandB (Subsystem s) 
	{
		verifySinglePackageWiring (s, BUNDLE_C, "x", BUNDLE_A);
		verifyRequireBundleWiring (s, BUNDLE_D, BUNDLE_A);
		verifyCapabilityWiring (s, BUNDLE_E, "y", BUNDLE_B);
	}

	/**
	 * Check that wiredBundleName in subsystem s is wired to a single package, 
	 * expectedPackage, from expectedProvidingBundle
	 * @param s
	 * @param wiredBundleName
	 * @param expectedPackage
	 * @param expectedProvidingBundle
	 */
	protected void verifySinglePackageWiring (Subsystem s, String wiredBundleName, String expectedPackage, String expectedProvidingBundle)
	{
		Bundle wiredBundle = context(s).getBundleByName(wiredBundleName);
		assertNotNull ("Bundle not found", wiredBundleName);

		BundleWiring wiring = (BundleWiring) wiredBundle.adapt(BundleWiring.class);
		List<BundleWire> wiredPackages = wiring.getRequiredWires(PACKAGE_NAMESPACE);
		assertEquals ("Only one package expected", 1, wiredPackages.size());
		
		String packageName = (String) 
			wiredPackages.get(0).getCapability().getAttributes().get(PACKAGE_NAMESPACE);
		assertEquals ("Wrong package found", expectedPackage, packageName);
		
		String providingBundle = wiredPackages.get(0).getProvider().getSymbolicName();
		assertEquals ("Package provided by wrong bundle", expectedProvidingBundle, providingBundle);
	}
	
	/**
	 * Verify that the Require-Bundle of wiredBundleName in subsystem s is met by a wire
	 * to expectedProvidingBundleName
	 * @param s
	 * @param wiredBundleName
	 * @param expectedProvidingBundleName
	 */
	protected void verifyRequireBundleWiring (Subsystem s, String wiredBundleName, String expectedProvidingBundleName)
	{
		Bundle wiredBundle = context(s).getBundleByName(BUNDLE_D);
		assertNotNull ("Target bundle " + wiredBundleName + " not found", wiredBundle);
	
		BundleWiring wiring = (BundleWiring) wiredBundle.adapt(BundleWiring.class);
		List<BundleWire> wiredBundles = wiring.getRequiredWires(BUNDLE_NAMESPACE);
		assertEquals ("Only one bundle expected", 1, wiredBundles.size());
	
		String requiredBundleName = (String)
			wiredBundles.get(0).getCapability().getAttributes().get(BUNDLE_NAMESPACE);
		assertEquals ("Wrong bundle requirement", BUNDLE_A, requiredBundleName);
	
		String providingBundle = wiredBundles.get(0).getProvider().getSymbolicName();
		assertEquals ("Wrong bundle provider", expectedProvidingBundleName, providingBundle);
	}
	
	/**
	 * Verify that a bundle with wiredBundleName imports a single capability in namespace
	 * from expectedProvidingBundleName
	 * @param s
	 * @param wiredBundleName
	 * @param namespace
	 * @param expectedProvidingBundleName
	 */
	protected void verifyCapabilityWiring (Subsystem s, String wiredBundleName, 
			String namespace, String expectedProvidingBundleName)
	{
		Bundle wiredBundle = context(s).getBundleByName(wiredBundleName);
		assertNotNull ("Targt bundle " + wiredBundleName + " not found", wiredBundleName);
		
		BundleWiring wiring = (BundleWiring) wiredBundle.adapt(BundleWiring.class);
		List<BundleWire> wiredProviders = wiring.getRequiredWires(namespace);
		assertEquals("Only one wire for capability namespace " + namespace +" expected", 
				1, wiredProviders.size());
		
		String capabilityNamespace = (String)
			wiredProviders.get(0).getCapability().getNamespace();
		assertEquals ("Wrong namespace", namespace, capabilityNamespace);
		
		String providingBundle = wiredProviders.get(0).getProvider().getSymbolicName();
		assertEquals ("Wrong bundle provider", expectedProvidingBundleName, providingBundle);
	}

	/**
	 * Verify that bundles with names bundleNames are installed into the subsystem with subsystemName
	 * and bundle context bc
	 * @param bc
	 * @param subsystemName
	 * @param bundleNames
	 */
	protected void verifyBundlesInstalled (BundleContext bc, String subsystemName, String ... bundleNames)
	{
		for (String bundleName: bundleNames) {
			boolean bundleFound = false;
			inner: for (Bundle b: bc.getBundles()) { 
				if (b.getSymbolicName().equals(bundleName)) { 
					bundleFound = true;
					break inner;
				}
			}
			assertTrue ("Bundle " + bundleName + " not found in subsystem " + subsystemName, bundleFound);
		}
	}
	
	/**
	 * Check that no new bundles have been provisioned by [x]
	 * @param failText where the failure occurred
	 * @param rootBundlesBefore Bundles before [x]
	 * @param rootBundlesAfter Bundles after [x]
	 */
	protected void checkNoNewBundles(String failText, Bundle[] rootBundlesBefore, Bundle[] rootBundlesAfter) {
		Set<String> bundlesBefore = new HashSet<String>();
		for (Bundle b : rootBundlesBefore) { 
			bundlesBefore.add(b.getSymbolicName() + "_" + b.getVersion().toString());
		}
		
		Set<String> bundlesAfter = new HashSet<String>();
		for (Bundle b : rootBundlesAfter) { 
			bundlesAfter.add(b.getSymbolicName() + "_" + b.getVersion().toString());
		}
		
		boolean unchanged = bundlesBefore.containsAll(bundlesAfter) && 
			bundlesAfter.containsAll(bundlesBefore);
		
		if (!unchanged) { 
			bundlesAfter.removeAll(bundlesBefore);
			fail ("Extra bundles provisioned in " + failText + " : " + bundlesAfter);
		}
	}
}
