package org.osgi.service.indexer.impl;

import static java.util.Collections.singletonMap;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.indexer.Capability;
import org.osgi.service.indexer.Requirement;
import org.osgi.service.indexer.Resource;
import org.osgi.service.indexer.ResourceAnalyzer;
import org.osgi.service.indexer.ResourceIndexer;
import org.osgi.service.log.LogService;

import junit.framework.TestCase;

public class TestIndexer extends TestCase {

	/**
	 * Test if the resolver is not affected if we use a dummy resolver
	 */
	@SuppressWarnings("deprecation")
	public void testResolverUnaffected() throws Exception {

		RepoIndex indexer = new RepoIndex();
		String without = index(indexer);

		indexer = new RepoIndex();
		indexer.setURLResolver(new URLResolver() {

			@Override
			public URI resolver(File artifact) throws Exception {
				return null;
			}
		});

		String with = index(indexer);

		assertEquals("Should be the same without a resolver or a resolver that returns null", without, with);

		indexer = new RepoIndex();
		indexer.setURLResolver(new URLResolver() {

			@Override
			public URI resolver(File artifact) throws Exception {
				throw new Exception();
			}
		});

		with = index(indexer);
		assertEquals("Should be the same without a resolver or a resolver that returns null", without, with);
	}

	/**
	 * Test if the resolver can change the URLs
	 */
	@SuppressWarnings("deprecation")
	public void testResolverEffect() throws Exception {

		RepoIndex indexer = new RepoIndex();
		indexer.setURLResolver(new URLResolver() {

			@Override
			public URI resolver(File artifact) throws Exception {
				return new URI("xyz://FOOBAR/" + artifact.getName());
			}
		});

		String with = index(indexer);

		Pattern p = Pattern.compile("xyz://FOOBAR/(03-export.jar|06-requirebundle.jar)(?:\"|')");
		Matcher m = p.matcher(with);
		int n = 0;
		while (m.find()) {
			n++;
		}
		assertEquals("There are two files indexed", 2, n);
	}

	public void testFragmentBsnVersion() throws Exception {
		assertFragmentMatch("testdata/fragment-01.txt", "testdata/01-bsn+version.jar");
	}

	public void testFragmentBsnVersionWithScrewyPath() throws Exception {
		assertFragmentMatch("testdata/fragment-01.txt", "testdata/../testdata/01-bsn+version.jar");
	}

	public void testFragmentBsnVersionWithBundleOutsideTheParentPath() throws Exception {
		RepoIndex indexer = new RepoIndex();

		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("generated/../testdata/01-bsn+version.jar")), writer,
				singletonMap(RepoIndex.ROOT_URL, new File("testdata").getAbsoluteFile().toURI().toURL().toString()));

		String expected = Utils.readStream(new FileInputStream("testdata/fragment-01-relative.txt"));
		assertEquals(expected, writer.toString().trim());
	}

	public void testFragmentLocalization() throws Exception {
		assertFragmentMatch("testdata/fragment-02.txt", "testdata/02-localization.jar");
	}

	public void testFragmentExport() throws Exception {
		assertFragmentMatch("testdata/fragment-03.txt", "testdata/03-export.jar");
	}

	public void testFragmentExportWithUses() throws Exception {
		assertFragmentMatch("testdata/fragment-04.txt", "testdata/04-export+uses.jar");
	}

	public void testFragmentImport() throws Exception {
		assertFragmentMatch("testdata/fragment-05.txt", "testdata/05-import.jar");
	}

	public void testFragmentRequireBundle() throws Exception {
		assertFragmentMatch("testdata/fragment-06.txt", "testdata/06-requirebundle.jar");
	}

	public void testFragmentOptionalImport() throws Exception {
		assertFragmentMatch("testdata/fragment-07.txt", "testdata/07-optionalimport.jar");
	}

	public void testFragmentFragmentHost() throws Exception {
		assertFragmentMatch("testdata/fragment-08.txt", "testdata/08-fragmenthost.jar");
	}

	public void testFragmentSingletonBundle() throws Exception {
		assertFragmentMatch("testdata/fragment-09.txt", "testdata/09-singleton.jar");
	}

	public void testFragmentExportService() throws Exception {
		assertFragmentMatch("testdata/fragment-10.txt", "testdata/10-exportservice.jar");
	}

	public void testFragmentImportService() throws Exception {
		assertFragmentMatch("testdata/fragment-11.txt", "testdata/11-importservice.jar");
	}

	public void testFragmentForbidFragments() throws Exception {
		assertFragmentMatch("testdata/fragment-12.txt", "testdata/12-nofragments.jar");
	}

	public void testFragmentBREE() throws Exception {
		assertFragmentMatch("testdata/fragment-13.txt", "testdata/13-bree.jar");
	}

	public void testFragmentProvideRequireCap() throws Exception {
		assertFragmentMatch("testdata/fragment-14.txt", "testdata/14-provide-require-cap.jar");
	}

	public void testFragmentRequireSCR() throws Exception {
		assertFragmentMatch("testdata/fragment-15.txt", "testdata/15-scr.jar");
	}

	public void testFragmentOptionalRequireBundle() throws Exception {
		assertFragmentMatch("testdata/fragment-16.txt", "testdata/16-optionalrequirebundle.jar");
	}

	public void testFragmentRequireSCR1_0() throws Exception {
		assertFragmentMatch("testdata/fragment-scr1_0.txt", "testdata/scr1_0.jar");
	}

	public void testFragmentRequireSCR1_1() throws Exception {
		assertFragmentMatch("testdata/fragment-scr1_1.txt", "testdata/scr1_1.jar");
	}

	public void testFragmentRequireSCR1_2() throws Exception {
		assertFragmentMatch("testdata/fragment-scr1_2.txt", "testdata/scr1_2.jar");
	}

	public void testFragmentSCRServices() throws Exception {
		assertFragmentMatch("testdata/fragment-scr_services.txt", "testdata/scr_services.jar");
	}

	private static void assertFragmentMatch(String expectedPath, String jarPath) throws Exception {
		RepoIndex indexer = new RepoIndex();
		assertFragmentMatch(indexer, expectedPath, jarPath);
	}

	private static void assertFragmentMatch(RepoIndex indexer, String expectedPath, String jarPath) throws Exception {
		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File(jarPath)), writer, null);

		String expected = Utils.readStream(new FileInputStream(expectedPath));
		assertEquals(expected, writer.toString().trim());
	}

	public void testEmptyIndex() throws Exception {
		RepoIndex indexer = new RepoIndex();

		ByteArrayOutputStream out = new ByteArrayOutputStream();
		Set<File> files = Collections.emptySet();

		Map<String,String> config = new HashMap<String,String>();
		config.put(RepoIndex.REPOSITORY_INCREMENT_OVERRIDE, "0");
		config.put(ResourceIndexer.REPOSITORY_NAME, "empty");
		config.put(ResourceIndexer.PRETTY, "true");
		indexer.index(files, out, config);

		String expected = Utils.readStream(new FileInputStream("testdata/empty.txt"));
		assertEquals(expected, out.toString());
	}

	public void testFullIndex() throws Exception {
		RepoIndex indexer = new RepoIndex();

		String decompressed = index(indexer);

		String unpackedXML = Utils.readStream(new FileInputStream("testdata/unpacked.xml"));
		String expected = unpackedXML.replaceAll("\\r?\\n|\\t", "");
		assertEquals(expected, decompressed);
	}

	private String index(RepoIndex indexer) throws Exception, IOException {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		Set<File> files = new LinkedHashSet<File>();
		files.add(new File("testdata/03-export.jar"));
		files.add(new File("testdata/06-requirebundle.jar"));

		Map<String,String> config = new HashMap<String,String>();
		config.put(RepoIndex.REPOSITORY_INCREMENT_OVERRIDE, "0");
		config.put(ResourceIndexer.REPOSITORY_NAME, "full-c+f");
		indexer.index(files, out, config);

		String decompressed = Utils.decompress(out.toByteArray());
		return decompressed;
	}

	public void testFullIndexPrettyCompressedPermutations() throws Exception {
		Boolean pretties[] = {
				null, Boolean.FALSE, Boolean.TRUE
		};
		Boolean compressions[] = {
				null, Boolean.FALSE, Boolean.TRUE
		};

		boolean outPretties[] = {
				false, false, false, true, false, false, true, true, true
		};
		boolean outCompressions[] = {
				true, false, true, false, false, true, false, false, true
		};

		String expectedPretty = Utils.readStream(new FileInputStream("testdata/full-03+06.txt"));
		String expectedNotPretty = Utils.readStream(new FileInputStream("testdata/full-03+06-not-pretty.txt"));

		RepoIndex indexer = new RepoIndex();
		ByteArrayOutputStream out = new ByteArrayOutputStream();

		Set<File> files = new LinkedHashSet<File>();
		files.add(new File("testdata/03-export.jar"));
		files.add(new File("testdata/06-requirebundle.jar"));

		Map<String,String> config = new HashMap<String,String>();

		int outIndex = 0;
		for (Boolean pretty : pretties) {
			for (Boolean compression : compressions) {
				config.put(RepoIndex.REPOSITORY_INCREMENT_OVERRIDE, "0");
				config.put(ResourceIndexer.REPOSITORY_NAME, "full-c+f");
				if (pretty != null) {
					config.put(ResourceIndexer.PRETTY, pretty.toString().toLowerCase());
				}
				if (compression != null) {
					config.put(ResourceIndexer.COMPRESSED, compression.toString().toLowerCase());
				}
				indexer.index(files, out, config);

				String expected = outPretties[outIndex] ? expectedPretty : expectedNotPretty;
				if (!outCompressions[outIndex]) {
					assertEquals("pretty/compression = " + pretty + "/" + compression, expected, out.toString());
				} else {
					assertEquals("pretty/compression = " + pretty + "/" + compression, expected,
							Utils.decompress(out.toByteArray()));
				}

				config.clear();
				out.reset();
				outIndex++;
			}
		}
	}

	public void testAddAnalyzer() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new WibbleAnalyzer(), null);

		StringWriter writer = new StringWriter();
		LinkedHashSet<File> files = new LinkedHashSet<File>();
		files.add(new File("testdata/01-bsn+version.jar"));
		files.add(new File("testdata/02-localization.jar"));

		indexer.indexFragment(files, writer, null);
		String expected = Utils.readStream(new FileInputStream("testdata/fragment-wibble.txt"));

		assertEquals(expected, writer.toString().trim());
	}

	public void testAddAnalyzerWithFilter() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new WibbleAnalyzer(), FrameworkUtil.createFilter("(location=*sion.jar)"));

		StringWriter writer = new StringWriter();
		LinkedHashSet<File> files = new LinkedHashSet<File>();
		files.add(new File("testdata/01-bsn+version.jar"));
		files.add(new File("testdata/02-localization.jar"));

		indexer.indexFragment(files, writer, null);
		String expected = Utils.readStream(new FileInputStream("testdata/fragment-wibble-filtered.txt"));

		assertEquals(expected, writer.toString().trim());
	}

	public void testAnalyzerException() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new BadAnalyzer(), null);

		StringWriter writer = new StringWriter();
		LinkedHashSet<File> files = new LinkedHashSet<File>();
		files.add(new File("testdata/01-bsn+version.jar"));
		files.add(new File("testdata/02-localization.jar"));

		indexer.indexFragment(files, writer, null);

		String xml = writer.toString().trim();
		String comment = "  <!-- Error calling analyzer \"org.osgi.service.indexer.impl.BadAnalyzer\" on resource testdata/01-bsn+version.jar with message java.lang.IllegalStateException: Bwa Ha Ha Ha! and stack: java.lang.IllegalStateException: Bwa Ha Ha Ha!";
		assertTrue(String.format("Did not contain the correct comment %s in %s", comment, xml), xml.contains(comment));

		comment = "  <!-- Error calling analyzer \"org.osgi.service.indexer.impl.BadAnalyzer\" on resource testdata/02-localization.jar with message java.lang.IllegalStateException: Bwa Ha Ha Ha! and stack: java.lang.IllegalStateException: Bwa Ha Ha Ha!";
		assertTrue(String.format("Did not contain the correct comment %s in %s", comment, xml), xml.contains(comment));
	}

	public void testRootInSubdirectory() throws Exception {
		RepoIndex indexer = new RepoIndex();

		Map<String,String> props = new HashMap<String,String>();
		props.put(ResourceIndexer.ROOT_URL, new File("testdata").getAbsoluteFile().toURI().toURL().toString());

		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("testdata/01-bsn+version.jar")), writer, props);

		String expected = Utils.readStream(new FileInputStream("testdata/fragment-subdir1.txt"));
		assertEquals(expected, writer.toString().trim());
	}

	public void testRootInSubSubdirectory() throws Exception {
		RepoIndex indexer = new RepoIndex();

		Map<String,String> props = new HashMap<String,String>();
		props.put(ResourceIndexer.ROOT_URL, new File("testdata").getAbsoluteFile().toURI().toURL().toString());

		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("testdata/subdir/01-bsn+version.jar")), writer, props);

		String expected = Utils.readStream(new FileInputStream("testdata/fragment-subdir2.txt"));
		assertEquals(expected, writer.toString().trim());
	}

	public void testLogErrorsFromAnalyzer() throws Exception {
		ResourceAnalyzer badAnalyzer = new ResourceAnalyzer() {
			public void analyzeResource(Resource resource, List<Capability> capabilities,
					List<Requirement> requirements) throws Exception {
				throw new Exception("Bang!");
			}
		};
		ResourceAnalyzer goodAnalyzer = mock(ResourceAnalyzer.class);

		LogService log = mock(LogService.class);
		RepoIndex indexer = new RepoIndex(log);
		indexer.addAnalyzer(badAnalyzer, null);
		indexer.addAnalyzer(goodAnalyzer, null);

		// Run the indexer
		Map<String,String> props = new HashMap<String,String>();
		props.put(ResourceIndexer.ROOT_URL, new File("testdata").getAbsoluteFile().toURI().toURL().toString());
		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("testdata/subdir/01-bsn+version.jar")), writer, props);

		// The "good" analyzer should have been called
		verify(goodAnalyzer).analyzeResource(any(Resource.class), anyListOf(Capability.class),
				anyListOf(Requirement.class));

		// The log service should have been notified about the exception
		ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
		verify(log).log(eq(LogService.LOG_ERROR), any(String.class), exceptionCaptor.capture());
		assertEquals("Bang!", exceptionCaptor.getValue().getMessage());
	}

	public void testBundleOutsideRootDirectory() throws Exception {
		LogService log = mock(LogService.class);
		RepoIndex indexer = new RepoIndex(log);

		Map<String,String> props = new HashMap<String,String>();
		props.put(ResourceIndexer.ROOT_URL, new File("testdata/subdir").getAbsoluteFile().toURI().toURL().toString());

		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("testdata/01-bsn+version.jar")), writer, props);

		verify(log).log(eq(LogService.LOG_ERROR), anyString(), isA(IllegalArgumentException.class));
	}

	public void testRemoveDisallowed() throws Exception {
		LogService log = mock(LogService.class);
		RepoIndex indexer = new RepoIndex(log);
		indexer.addAnalyzer(new NaughtyAnalyzer(), null);

		Map<String,String> props = new HashMap<String,String>();
		props.put(ResourceIndexer.ROOT_URL, new File("testdata").getAbsoluteFile().toURI().toURL().toString());

		StringWriter writer = new StringWriter();
		indexer.indexFragment(Collections.singleton(new File("testdata/subdir/01-bsn+version.jar")), writer, props);

		verify(log).log(eq(LogService.LOG_ERROR), anyString(), isA(UnsupportedOperationException.class));
	}

	public void testRecogniseFelixSCR() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new KnownBundleAnalyzer(), FrameworkUtil.createFilter("(name=*)"));
		assertFragmentMatch(indexer, "testdata/org.apache.felix.scr-1.6.0.xml",
				"testdata/org.apache.felix.scr-1.6.0.jar");
	}

	public void testRecogniseAriesBlueprint() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new KnownBundleAnalyzer(), FrameworkUtil.createFilter("(name=*)"));
		assertFragmentMatch(indexer, "testdata/org.apache.aries.blueprint-1.0.0.xml",
				"testdata/org.apache.aries.blueprint-1.0.0.jar");
	}

	public void testRecogniseGeminiBlueprint() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new KnownBundleAnalyzer(), FrameworkUtil.createFilter("(name=*)"));
		assertFragmentMatch(indexer, "testdata/gemini-blueprint-extender-1.0.0.RELEASE.xml",
				"testdata/gemini-blueprint-extender-1.0.0.RELEASE.jar");
	}

	public void testRecogniseFelixJetty() throws Exception {
		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new KnownBundleAnalyzer(), FrameworkUtil.createFilter("(name=*)"));
		assertFragmentMatch(indexer, "testdata/org.apache.felix.http.jetty-2.2.0.xml",
				"testdata/org.apache.felix.http.jetty-2.2.0.jar");
	}

	public void testMacroExpansion() throws Exception {
		Properties props = new Properties();
		try (InputStream in = new FileInputStream("testdata/known-bundles.properties")) {
			props.load(in);
		}

		RepoIndex indexer = new RepoIndex();
		indexer.addAnalyzer(new KnownBundleAnalyzer(props), FrameworkUtil.createFilter("(name=*)"));
		assertFragmentMatch(indexer, "testdata/org.apache.felix.eventadmin-1.2.14.xml",
				"testdata/org.apache.felix.eventadmin-1.2.14.jar");
	}

	public void testFragmentRequireBlueprint() throws Exception {
		assertFragmentMatch("testdata/fragment-17.txt", "testdata/17-blueprint1.jar");
	}

	public void testFragmentRequireBlueprintUsingHeader() throws Exception {
		assertFragmentMatch("testdata/fragment-18.txt", "testdata/18-blueprint2.jar");
	}

	public void testFragmentBundleNativeCode() throws Exception {
		assertFragmentMatch("testdata/fragment-19.txt", "testdata/19-bundlenativecode.jar");
	}

	public void testFragmentBundleNativeCodeOptional() throws Exception {
		assertFragmentMatch("testdata/fragment-20.txt", "testdata/20-bundlenativecode-optional.jar");
	}

	public void testFragmentPlainJar() throws Exception {
		LogService mockLog = Mockito.mock(LogService.class);
		RepoIndex indexer = new RepoIndex(mockLog);
		indexer.addAnalyzer(new KnownBundleAnalyzer(), FrameworkUtil.createFilter("(name=*)"));

		assertFragmentMatch(indexer, "testdata/fragment-plainjar.txt", "testdata/jcip-annotations.jar");
		Mockito.verifyZeroInteractions(mockLog);
	}

	public void testFragmentPlainJarWithVersion() throws Exception {
		assertFragmentMatch("testdata/fragment-plainjar-versioned.txt", "testdata/jcip-annotations-2.5.6.wibble.jar");
	}

	public void testImportServiceOptional() throws Exception {
		assertFragmentMatch("testdata/org.apache.felix.eventadmin-1.3.2.xml",
				"testdata/org.apache.felix.eventadmin-1.3.2.jar");
	}
}
