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.jupiter.api.Assertions;
import org.junit.jupiter.api.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
 */
class OsgiClashingPackagesTest
{

	@Test
	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();
		}
		Assertions.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 (!"".equals(packageWithContent))
				{
					if (!projectBuckets.containsKey(packageWithContent))
					{
						projectBuckets.put(packageWithContent, new ArrayList<>());
					}
					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)
		{
            return !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"));
        }
	}

}
