/*
 * Copyright (c) 2013 Red Hat, Inc. and/or its affiliates.
 *  
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  which accompanies this distribution, and is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *  
 *  Contributors:
 *      Ian Tewksbury - ian@itewk.com - Initial API and implementation
 */
package org.jboss.windup.test.helpers;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.jboss.windup.WindupEngine;
import org.jboss.windup.WindupEnvironment;
import org.jboss.windup.metadata.decoration.AbstractDecoration;
import org.jboss.windup.metadata.decoration.Hash;
import org.jboss.windup.metadata.type.FileMetadata;
import org.jboss.windup.metadata.type.archive.ArchiveMetadata;
import org.jboss.windup.reporting.ReportEngine;
import org.junit.Assert;

import com.esotericsoftware.kryo.io.Input;

/**
 * <p>
 * The parent class for all Windup test cases.
 * </p>
 */
public class WindupTestCase {
	/**
	 * The path to the parent directory where the resources used by this test
	 * should be stored
	 */
	private final String PARENT_DIR_PATH = new File(this.getClass().getResource(this.getClass().getSimpleName() + ".class").getFile()).getParent() + File.separator;

	/**
	 * <p>
	 * Runs a test on a single file processed by the Windup engine.
	 * </p>
	 * 
	 * @param fileName
	 *            name of the file to test, relative to the test class
	 * @param expectedFileDecorations
	 *            expected file decorations
	 * @param expectedArhciveDecorations
	 *            expected archive decorations
	 * 
	 * @throws IOException
	 *             can happen when reading files
	 */
	protected void runSingleFileMetaTest(String fileName, String[] expectedFileDecorations, String[] expectedArhciveDecorations) throws IOException {

		if (expectedFileDecorations == null) {
			expectedFileDecorations = new String[0];
		}

		if (expectedArhciveDecorations == null) {
			expectedArhciveDecorations = new String[0];
		}

		File inputFile = new File(this.getResourcePath(fileName));
		FileMetadata meta = this.getMetaEngine().processFile(inputFile);

		Assert.assertNotNull("Returned meta should not be null", meta);

		// search the file for the expected decorations
		for (String expectedFileDecoration : expectedFileDecorations) {
			boolean found = false;
			for (AbstractDecoration decoration : meta.getDecorations()) {
				if (decoration.getDescription().equals(expectedFileDecoration)) {
					found = true;
					break;
				}
			}

			if (!found) {
				Assert.fail("Did not find expected file decoration: '" + expectedFileDecoration + "'");
			}
		}
		Assert.assertEquals("The number of file decorations does not match expected", expectedFileDecorations.length, meta.getDecorations().size());

		// search the archive for the expected decorations
		for (String expectedArchiveDecoration : expectedArhciveDecorations) {
			boolean found = false;
			for (AbstractDecoration decoration : meta.getArchiveMeta().getDecorations()) {
				if (decoration.getDescription().equals(expectedArchiveDecoration)) {
					found = true;
					break;
				}
			}

			if (!found) {
				Assert.fail("Did not find expected archive decoration: '" + expectedArchiveDecoration + "'");
			}
		}
		Assert.assertEquals("The number of archvie decorations does not match expected", expectedArhciveDecorations.length, meta.getArchiveMeta().getDecorations().size());
	}

	/**
	 * <p>
	 * Run a test on a single archive.
	 * </p>
	 * 
	 * @param archivePath
	 *            path to the archive to test
	 * @param expectedArchiveDecorations
	 *            expected decorations on the archive
	 * @param expectedArchiveEntryDecorations
	 *            map of file paths to their expected decorations
	 * @param outputPath
	 *            Optional. The output directory to be used when generating the
	 *            meta
	 * 
	 * @throws IOException
	 *             can happen when reading files
	 */
	protected void runSourceDirectoryMetaTest(String archivePath, String[] expectedArchiveDecorations, Map<String, String[]> expectedArchiveEntryDecorations, String outputPath) throws IOException {

		File input = new File(this.getResourcePath(archivePath));
		File output = new File(this.getResourcePath(outputPath));
		ArchiveMetadata archiveMeta = this.getMetaEngine().processSourceDirectory(input, output);

		this.runArchiveMetaTest(archiveMeta, expectedArchiveDecorations, expectedArchiveEntryDecorations);
	}

	
	/**
	 * <p>
	 * Run a test on a single archive.
	 * </p>
	 * 
	 * @param archivePath
	 *            path to the archive to test
	 * @param expectedArchiveDecorations
	 *            expected decorations on the archive
	 * @param expectedArchiveEntryDecorations
	 *            map of file paths to their expected decorations
	 * @param outputPath
	 *            Optional. The output directory to be used when generating the
	 *            meta
	 * 
	 * @throws IOException
	 *             can happen when reading files
	 */
	protected void runArchiveMetaTest(String archivePath, String[] expectedArchiveDecorations, Map<String, String[]> expectedArchiveEntryDecorations, String outputPath) throws IOException {

		File input = new File(this.getResourcePath(archivePath));
		File output = new File(this.getResourcePath(outputPath));
		ArchiveMetadata archiveMeta = this.getArchiveMeta(input, output);

		this.runArchiveMetaTest(archiveMeta, expectedArchiveDecorations, expectedArchiveEntryDecorations);
	}

	/**
	 * <p>
	 * Runs an archive report test by comparing a generated report to an
	 * expected report.
	 * </p>
	 * 
	 * @param archivePath
	 *            archive to generate a report for
	 * @param reportPath
	 *            location to generate the report to
	 * @param expectedReportPath
	 *            location of the expected report contents
	 * 
	 * @throws IOException
	 *             this can happen when performing file operations
	 */
	protected void runArchiveReportTest(String archivePath, String reportPath, String expectedReportPath) throws IOException {
		// cleanup any existing output
		File output = null;
		String outputPath = null;
		
		if(reportPath != null) {
			outputPath = this.getResourcePath(reportPath);
			output = new File(outputPath);
			output.delete();
		}

		// generate report
		File input = new File(this.getResourcePath(archivePath));
		WindupEnvironment environment = this.getEnvironment();
		environment.setInputPath(input);
		environment.setOutputPath(output);
		ReportEngine reportEngine = new ReportEngine(environment);
		reportEngine.process();

		//if not output specified the engine will append -doc
		if(outputPath == null) {
			outputPath = StringUtils.substringBeforeLast(input.getAbsolutePath(), ".");
			outputPath += "-" + StringUtils.substringAfterLast(input.getAbsolutePath(), ".") + "-doc";
		}
		
		// compare generated report to expected report
		FileHelpers.compareFolders(this.getResourcePath(expectedReportPath), outputPath);

		// cleanup generated report depending on where it is
		if(output != null) {
			output.delete();
		} else {
			(new File(outputPath)).delete();
		}
	}

	/**
	 * <p>
	 * Run a test on a directory of archives.
	 * </p>
	 * 
	 * @param parentPath
	 *            path to the directory containing the archives to test
	 * @param expectedArchiveDecorations
	 *            map of archive names to expected decorations on the archive
	 * @param expectedArchiveEntryDecorations
	 *            map of archive names to maps of file paths to their expected
	 *            decorations
	 * @param outputDir
	 *            Optional. The output directory to be used when generating the
	 *            meta
	 * 
	 * @throws IOException
	 *             can happen when reading files
	 */
	protected void runMultipleArchiveMetaTest(String parentPath, Map<String, String[]> expectedArchiveDecorations, Map<String, Map<String, String[]>> expectedArchiveEntryDecorations, String outputPath) throws IOException {

		if (expectedArchiveDecorations == null) {
			expectedArchiveDecorations = new HashMap<String, String[]>();
		}

		if (expectedArchiveEntryDecorations == null) {
			expectedArchiveEntryDecorations = new HashMap<String, Map<String, String[]>>();
		}

		File input = new File(this.getResourcePath(parentPath));
		File output = new File(this.getResourcePath(outputPath));
		Collection<ArchiveMetadata> parentArchiveMetas = this.getReportEngine().batchInputDirectory(input);
		Assert.assertNotNull("Returned meta should not be null", parentArchiveMetas);

		for (ArchiveMetadata childArchiveMeta : parentArchiveMetas) {
			String name = childArchiveMeta.getName();

			this.runArchiveMetaTest(childArchiveMeta, expectedArchiveDecorations.get(name), expectedArchiveEntryDecorations.get(name));
		}
	}


	/**
	 * @return a fully setup {@link WindupMetaEngine} for use during testing
	 */
	protected ReportEngine getReportEngine() {
		WindupEnvironment settings = getEnvironment();
		ReportEngine metaEngine = new ReportEngine(settings);

		return metaEngine;
	}

	
	/**
	 * @return a fully setup {@link WindupMetaEngine} for use during testing
	 */
	protected WindupEngine getMetaEngine() {
		WindupEnvironment settings = getEnvironment();
		WindupEngine metaEngine = new WindupEngine(settings);

		return metaEngine;
	}

	/**
	 * @return {@link WindupEnvironment} setup for testing
	 */
	protected WindupEnvironment getEnvironment() {
		WindupEnvironment settings = new WindupEnvironment();

		return settings;
	}

	/**
	 * @param resourceName
	 *            name of the resource relative to this test to get the path for
	 * 
	 * @return full path to the resource
	 */
	protected final String getResourcePath(String resourceName) {
		return PARENT_DIR_PATH + resourceName;
	}

	/**
	 * <p>
	 * Run a test on a single {@link ArchiveMetadata}
	 * </p>
	 * 
	 * @param archiveMeta
	 *            {@link ArchiveMetadata} to test
	 * @param expectedArchiveDecorations
	 *            expected decorations on the archive
	 * @param expectedArchiveEntryDecorations
	 *            map of file paths to their expected decorations
	 */
	private void runArchiveMetaTest(ArchiveMetadata archiveMeta, String[] expectedArchiveDecorations, Map<String, String[]> expectedArchiveEntryDecorations) {
		Assert.assertNotNull("ArchiveMetadata should not be null", archiveMeta);

		if (expectedArchiveDecorations == null) {
			expectedArchiveDecorations = new String[0];
		}

		if (expectedArchiveEntryDecorations == null) {
			expectedArchiveEntryDecorations = new HashMap<String, String[]>();
		}

		// search the archive for the expected decorations
		Collection<AbstractDecoration> archiveDecorations = new ArrayList<AbstractDecoration>(archiveMeta.getDecorations());
		for (String expectedArchiveDecoration : expectedArchiveDecorations) {
			boolean foundExpectedArchiveDecoration = false;

			// check entry decorators for expected decorator
			Iterator<AbstractDecoration> decorationsIter = archiveDecorations.iterator();
			while (decorationsIter.hasNext() && !foundExpectedArchiveDecoration) {
				AbstractDecoration decoration = decorationsIter.next();
				if (decoration.getDescription().equals(expectedArchiveDecoration)) {
					foundExpectedArchiveDecoration = true;

					// remove the matched decoration
					decorationsIter.remove();
				}
			}
			Assert.assertTrue("Did not find expected archive decoration: '" + expectedArchiveDecoration + "' in '" + archiveMeta.getName() + "'", foundExpectedArchiveDecoration);
		}
		
		//remove all "hash" decorators becuase they are always expected
		Iterator<AbstractDecoration> unexpecedDecorationsIter = archiveDecorations.iterator();
		while(unexpecedDecorationsIter.hasNext()) {
			if(unexpecedDecorationsIter.next() instanceof Hash) {
				unexpecedDecorationsIter.remove();
			}
		}
		
		// error if found any unexpected decorations on the archive
		if (!archiveDecorations.isEmpty()) {
			StringBuffer unexpectedArchiveDecorators = new StringBuffer();
			unexpectedArchiveDecorators.append("The archive '" + archiveMeta.getName() + "' has unexpected decorators:\n");
			for (AbstractDecoration unexpectedDecoration : archiveDecorations) {
				unexpectedArchiveDecorators.append("\t");
				unexpectedArchiveDecorators.append(unexpectedDecoration);
				unexpectedArchiveDecorators.append("\n");
			}

			Assert.fail(unexpectedArchiveDecorators.toString());
		}

		Set<FileMetadata> entries = new HashSet<FileMetadata>(archiveMeta.getEntries());
		Set<String> expectedEntryPaths = expectedArchiveEntryDecorations.keySet();
		for (String expectedEntryFilePath : expectedEntryPaths) {
			boolean foundEntry = false;

			// search entries for expected entry
			Iterator<FileMetadata> entriesIter = entries.iterator();
			while (entriesIter.hasNext()) {
				FileMetadata entry = entriesIter.next();

				// if entry matches expected entry path search for expected
				// decorators on that entry
				String relativeEntrypath = FilenameUtils.normalize(entry.getPathRelativeToArchive());
				if (relativeEntrypath.equals(FilenameUtils.normalize(expectedEntryFilePath))) {
					foundEntry = true;

					// remove this entry since it has matched
					entriesIter.remove();

					// for each expected entry decorator see if it exists
					String[] expectedEntryDecorators = expectedArchiveEntryDecorations.get(expectedEntryFilePath);
					Collection<AbstractDecoration> entryDecorations = new ArrayList<AbstractDecoration>(entry.getDecorations());
					for (String expectedEntryDecorator : expectedEntryDecorators) {
						boolean foundExpectedEntryDecorator = false;

						// check entry decorators for expected decorator
						Iterator<AbstractDecoration> decorationsIter = entryDecorations.iterator();
						while (decorationsIter.hasNext() && !foundExpectedEntryDecorator) {
							AbstractDecoration decoration = decorationsIter.next();
							if (decoration.getDescription().equals(expectedEntryDecorator)) {
								foundExpectedEntryDecorator = true;

								// remove the matched decoration
								decorationsIter.remove();
							}
						}
						Assert.assertTrue("Did not find expected archive entry decoration '" + expectedEntryDecorator + "' on '" + expectedEntryFilePath + "' in '" + archiveMeta.getName() + "'", foundExpectedEntryDecorator);
					}

					// error if found any unexpected decorations on the expected
					// entry
					if (!entryDecorations.isEmpty()) {
						StringBuffer unexpectedEntryDecorators = new StringBuffer();
						unexpectedEntryDecorators.append("The entry '" + expectedEntryFilePath + "' in '" + archiveMeta.getName() + "' contained unexpected entries:\n");
						for (AbstractDecoration unexpectedDecoration : entryDecorations) {
							unexpectedEntryDecorators.append("\t");
							unexpectedEntryDecorators.append(unexpectedDecoration.getDescription());
							unexpectedEntryDecorators.append("\n");
						}

						Assert.fail(unexpectedEntryDecorators.toString());
					}
				}
			}

			Assert.assertTrue("Did not find expected archive entry in '" + archiveMeta.getName() + "' for '" + expectedEntryFilePath + "'", foundEntry);
		}

		// error if found any unexpected entries
		if (!entries.isEmpty()) {
			StringBuffer unexpectedEntries = new StringBuffer();
			unexpectedEntries.append("The archive '" + archiveMeta.getName() + "' contained unexpected entries:\n");
			for (FileMetadata entry : entries) {
				unexpectedEntries.append("\t");
				unexpectedEntries.append(entry);
				unexpectedEntries.append("\n");
			}

			Assert.fail(unexpectedEntries.toString());
		}
	}

	/**
	 * <p>
	 * Uses {@link WindupMetaEngine} to generate {@link ArchiveMetadata} for the
	 * given input.
	 * </p>
	 * 
	 * @param input
	 *            {@link File} to generate {@link ArchiveMetadata} for
	 * @param outputDir
	 *            Optional. The output directory to be used when generating the
	 *            meta
	 * 
	 * @return {@link ArchiveMetadata} generated for the given {@link File}
	 * 
	 * @throws IOException
	 *             can happen when trying to read files
	 */
	private ArchiveMetadata getArchiveMeta(File input, File outputDir) throws IOException {
		WindupEngine engine = this.getMetaEngine();
		ArchiveMetadata meta = engine.processArchive(input, outputDir);

		return meta;
	}
}