// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.set.v0_6;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.openstreetmap.osmosis.core.Osmosis;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.merge.common.ConflictResolutionMethod;
import org.openstreetmap.osmosis.core.misc.v0_6.EmptyReader;
import org.openstreetmap.osmosis.testutil.AbstractDataTest;
import org.openstreetmap.osmosis.testutil.v0_6.RunTaskUtilities;
import org.openstreetmap.osmosis.xml.common.CompressionMethod;
import org.openstreetmap.osmosis.xml.v0_6.XmlReader;

/**
 * Test the --merge task.
 * 
 * @author Igor Podolskiy
 */
public class EntityMergerTest extends AbstractDataTest {
	
	/**
	 * Tests empty + X == X.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void firstEmpty() throws Exception {
		File sourceFile;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile.getPath(),
				"--read-empty-0.6",
				"--merge",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);
	}
	
	/**
	 * Tests X + empty == X.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void secondEmpty() throws Exception {
		File sourceFile;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-empty-0.6",
				"--read-xml-0.6", sourceFile.getPath(),
				"--merge",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);
	}
	
	/**
	 * Tests X + X == X.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void selfMerge() throws Exception {
		File sourceFile1;
		File sourceFile2;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile1 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		sourceFile2 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile2.getPath(),
				"--read-xml-0.6", sourceFile1.getPath(),
				"--merge",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);
	}
	
	/**
	 * Tests the timestamp conflict resolution strategy.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void timestampConflictResolution() throws Exception {
		File sourceFile1;
		File sourceFile2;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile1 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		sourceFile2 = dataUtils.createDataFile("v0_6/merge/merge-in-2-timestamp.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-out-timestamp.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile2.getPath(),
				"--read-xml-0.6", sourceFile1.getPath(),
				"--merge",
				"conflictResolutionMethod=timestamp",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

		// Timestamp conflict resolution should be commutative.
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile1.getPath(),
				"--read-xml-0.6", sourceFile2.getPath(),
				"--merge",
				"conflictResolutionMethod=timestamp",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

	}
	
	/**
	 * Tests the version conflict resolution strategy.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void versionConflictResolution() throws Exception {
		File sourceFile1;
		File sourceFile2;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile1 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		sourceFile2 = dataUtils.createDataFile("v0_6/merge/merge-in-2-version.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-out-version.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile2.getPath(),
				"--read-xml-0.6", sourceFile1.getPath(),
				"--merge",
				"conflictResolutionMethod=version",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

		// Version conflict resolution should be commutative.
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile1.getPath(),
				"--read-xml-0.6", sourceFile2.getPath(),
				"--merge",
				"conflictResolutionMethod=version",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

	}
	
	/**
	 * Tests the version conflict resolution strategy.
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void secondSourceConflictResolution() throws Exception {
		File sourceFile1;
		File sourceFile2;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile1 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		sourceFile2 = dataUtils.createDataFile("v0_6/merge/merge-in-2-secondSource.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-out-secondSource.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile2.getPath(),
				"--read-xml-0.6", sourceFile1.getPath(),
				"--merge",
				"conflictResolutionMethod=lastSource",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

		// Timestamp conflict resolution is NOT commutative, 
		// but it deserves testing as well.
		// As the mergen-in-2 input does not contain any entities that are not 
		// in the second source, the output should be identical to the first source.
		
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");

		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile1.getPath(),
				"--read-xml-0.6", sourceFile2.getPath(),
				"--merge",
				"conflictResolutionMethod=lastSource",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

	}
	
	/**
	 * Tests merging two completely disjunct datasets (no conflicts).
	 * 
	 * @throws Exception if something fails
	 */
	@Test
	public void disjunctDatasets() throws Exception {
		File sourceFile1;
		File sourceFile2;
		File expectedOutputFile;
		File actualOutputFile;
		
		// Generate files.
		sourceFile1 = dataUtils.createDataFile("v0_6/merge/merge-in-1.osm");
		sourceFile2 = dataUtils.createDataFile("v0_6/merge/merge-in-2-disjunct.osm");
		expectedOutputFile = dataUtils.createDataFile("v0_6/merge/merge-out-disjunct.osm");
		actualOutputFile = dataUtils.newFile();
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile2.getPath(),
				"--read-xml-0.6", sourceFile1.getPath(),
				"--merge",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

		// Merging of disjunct datasets should be commutative.
		
		// Run the merge.
		Osmosis.run(
			new String[] {
				"-q",
				"--read-xml-0.6", sourceFile1.getPath(),
				"--read-xml-0.6", sourceFile2.getPath(),
				"--merge",
				"conflictResolutionMethod=version",
				"--write-xml-0.6", actualOutputFile.getPath()
			}
		);
		
		// Validate that the output file matches the expected result.
		dataUtils.compareFiles(expectedOutputFile, actualOutputFile);

	}
	
	/**
	 * Tests bad sort order in an input stream (node, way, relations not in
	 * order).
	 * 
	 * @throws Exception
	 *             if something fails
	 */
	@Test
	public void badSortOrderType() throws Exception {
		File sourceFile = dataUtils.createDataFile("v0_6/merge/merge-in-badorder-type.osm");

		mergeAndLookForException(sourceFile, "Pipeline entities are not sorted");
	}
	
	/**
	 * Tests bad sort order in an input stream (ids not sorted).
	 * 
	 * @throws Exception
	 *             if something fails
	 */
	@Test
	public void badSortOrderId() throws Exception {
		File sourceFile = dataUtils.createDataFile("v0_6/merge/merge-in-badorder-id.osm");

		mergeAndLookForException(sourceFile, "Pipeline entities are not sorted");
	}

	/**
	 * Runs a merge and records the exceptions that happened during the merge.
	 * 
	 * The test is considered passed iff at least one exception was thrown and 
	 * the exception message begins with a given string.
	 * 
	 * This method does not use command line parsing because it is impossible 
	 * to check whether the right exception has been thrown. Also, as all 
	 * exceptions are OsmosisRuntimeExceptions, we need to check the message
	 * so the JUnit expected exception facility is no good here. 
	 * 
	 * To add insult to injury, running a merge task involves three worker threads; 
	 * the exception we expect is thrown on one of those worker threads which brings 
	 * the pipeline down. But we want to check for that one source exception
	 * is thrown for the correct reason, otherwise the test becomes very unspecific.
	 * 
	 */
	private void mergeAndLookForException(File sourceFile, String exceptionMessagePrefix) throws Exception {
		final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<Throwable>());
		
		XmlReader reader = new XmlReader(sourceFile, false, CompressionMethod.None);

		EntityMerger merger = new EntityMerger(
				ConflictResolutionMethod.LatestSource, 1, BoundRemovedAction.Ignore);

		RunTaskUtilities.run(merger, reader, new EmptyReader(), new Thread.UncaughtExceptionHandler() {
			@Override
			public void uncaughtException(Thread t, Throwable e) {
				exceptions.add(e);
			}
		});
		
		// At least one of those exceptions should be a "Pipeline not sorted" one
		boolean sortExceptionFound = false;
		for (Throwable t : exceptions) {
			if (!(t instanceof OsmosisRuntimeException)) {
				Assert.fail("Unexpected exception thrown: " + t);
			}
			
			sortExceptionFound |= t.getMessage().startsWith(exceptionMessagePrefix);
		}
		
		if (!sortExceptionFound) {
			Assert.fail("Expected exception not thrown");
		}
	}
}
