package org.apache.wicket.osgi;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.wicket.util.string.Strings;
import org.junit.Assert;
import org.junit.Test;

/**
 * A test that verifies that there are no non-empty packages with the same name in
 * two or more wicket modules.
 *
 * Based on https://gist.github.com/1977817, contributed by Andreas Pieber
 */
public class OsgiClashingPackagesTest extends Assert
{

	@Test
	public void collectProjectPackages() throws IOException
	{
		char pathSeparator = System.getProperty("path.separator").charAt(0);
		String classpath = System.getProperty("java.class.path");
		String[] dependencies = Strings.split(classpath, pathSeparator);

		// packageName -> projects containing a package with this name
		Map<String, List<Project>> projectBuckets = new HashMap<>();

		for (String dependency : dependencies)
		{
			// process only wicket-xyz.jar
			if (dependency.contains("wicket-") && dependency.endsWith(".jar"))
			{
				JarFile jarFile = new JarFile(dependency);
				try
				{
					String projectName = Strings.afterLast(dependency, '/');
					Project project = new Project(projectName, jarFile);
					project.addTo(projectBuckets);
				} finally {
					jarFile.close();
				}
			}
		}

		Set<Entry<String, List<Project>>> entrySet = projectBuckets.entrySet();
		for (Entry<String, List<Project>> entry : entrySet) {
			List<Project> projects = entry.getValue();
			if (projects.size() > 1) {
				fail(entry);
			}
		}
	}

	private void fail(Entry<String, List<Project>> entry) {
		StringBuilder builder = new StringBuilder();
		String packageName = entry.getKey();
		builder.append("Package '").append(packageName).append("' has files in two or more modules: ");
		for (Project conflict : entry.getValue()) {
			builder.append(conflict.getName()).append(", ");
		}
		try
		{
			builder.append("\nResources:\n");
			Enumeration<URL> resources = getClass().getClassLoader().getResources(packageName);
			while (resources.hasMoreElements())
			{
				URL resource = resources.nextElement();
				builder.append("\n\t").append(resource.toExternalForm());
			}
		} catch (IOException e)
		{
			e.printStackTrace();
		}
		fail(builder.toString());
	}

	private static class Project {
		// a set with all package names in a dependency
		private final Set<String> packagesWithContent = new TreeSet<>();

		// the name of the dependency
		private final String name;

		public Project(String name, JarFile jarFile) {
			this.name = name;
			collectPackageNames(jarFile);
		}

		/**
		 * Adds this project to as a value in the global map that contains 'packageName -> List[Project]'
		 * @param projectBuckets
		 *      the global map
		 */
		public void addTo(Map<String, List<Project>> projectBuckets) {
			for (String packageWithContent : packagesWithContent) {
				if (!projectBuckets.containsKey(packageWithContent)) {
					projectBuckets.put(packageWithContent, new ArrayList<Project>());
				}
				projectBuckets.get(packageWithContent).add(this);
			}
		}

		/**
		 * Collects the names of all packages in this JarFile
		 * @param jarFile
		 *      the jar file to analyze
		 */
		private void collectPackageNames(final JarFile jarFile)
		{
			Enumeration<JarEntry> entries = jarFile.entries();
			while (entries.hasMoreElements())
			{
				JarEntry jarEntry = entries.nextElement();
				String entryName = jarEntry.getName();
				if (shouldCollect(entryName))
				{
					String packageName = Strings.beforeLast(entryName, '/');
					packagesWithContent.add(packageName);
				}
			}
		}

		public String getName() {
			return name;
		}

		@Override
		public String toString()
		{
			return "Project{" +
					"name='" + name + '\'' +
					'}';
		}

		private boolean shouldCollect(final String entryName)
		{
			if (
				// ignore folder names. count just files/resources
				entryName.endsWith("/") ||

				// all modules have META-INF {MANIFEST.MF, Maven stuff, ..}
				entryName.startsWith("META-INF/") ||

				// ignore Wicket's IInitializer conf files
				(entryName.startsWith("META-INF/wicket") && entryName.endsWith(".properties"))
			)
			{
				return false;
			}

			return true;
		}
	}

}
