/*******************************************************************************
 * Copyright (c) 2013, 2014 UT-Battelle, LLC.
 * 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:
 *   Initial API and implementation and/or initial documentation - Jay Jay Billings,
 *   Jordan H. Deyton, Dasha Gorin, Alexander J. McCaskey, Taylor Patterson,
 *   Claire Saunders, Matthew Wang, Anna Wojtowicz
 *******************************************************************************/
package org.eclipse.ice.tests.persistence.xml;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ice.datastructures.form.Form;
import org.eclipse.ice.datastructures.jaxbclassprovider.ICEJAXBClassProvider;
import org.eclipse.ice.item.Item;
import org.eclipse.ice.item.nuclear.MOOSEModelBuilder;
import org.eclipse.ice.persistence.xml.XMLPersistenceProvider;
import org.eclipse.ice.vibe.launcher.VibeLauncherBuilder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * This class tests the XMLPersistenceProvider.
 * 
 * @author Jay Jay Billings
 * 
 */
public class XMLPersistenceProviderTester {

	/**
	 * The Eclipse project used in the test.
	 */
	private static IProject project;

	/**
	 * The other Eclipse project used in the test.
	 */
	private static IProject otherProject;

	/**
	 * The XMLPersistenceProvider that will be tested.
	 */
	private static XMLPersistenceProvider xmlpp;

	/**
	 * This operation sets up the tester and creates the project space. It also
	 * copies a data file for the MOOSEModel Item into the workspace so that it
	 * can be used. It is the biggest normal Item in ICE and using it makes the
	 * test realistic.
	 * 
	 * The project name is chosen to match the name expected by the
	 * XMLPersistenceProvider.
	 */
	@BeforeClass
	static public void setup() {

		project = createProject("itemDB");
		otherProject = createProject("otherItemDB");

		// Setup the XMLPersistenceProvider
		xmlpp = new XMLPersistenceProvider(project);
		// Register the MOOSE model and the CAEBAT key-value pair builders with
		// it so that it can determine class information for unmarshalling
		// Items.
		xmlpp.addBuilder(new MOOSEModelBuilder());
		xmlpp.addBuilder(new VibeLauncherBuilder());

		// Add a Class Provider so that we can persist the forms.
		xmlpp.registerClassProvider(new ICEJAXBClassProvider());

		try {
			// Start the service
			xmlpp.start();
		} catch (JAXBException | CoreException e) {
			e.printStackTrace();
			fail();
		}

		return;
	}

	/**
	 * This operation creates a project that is used during the persistence and
	 * loading tests.
	 * 
	 * @param projectName
	 *            The name for the new project.
	 * @return The new Eclipse project.
	 */
	private static IProject createProject(String projectName) {

		// Local Declarations
		IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
		URI defaultProjectLocation = null;
		String separator = System.getProperty("file.separator");
		String userDir = System.getProperty("user.home") + separator
				+ "ICETests" + separator + "persistenceData";
		String projectPath = userDir + separator + projectName;
		IProject createdProject = null;

		// Setup the project
		try {
			// Get the project handle
			createdProject = workspaceRoot.getProject(projectName);
			// If the project does not exist, create it
			if (!createdProject.exists()) {
				// Set the location as ${workspace_loc}/ItemTesterWorkspace
				defaultProjectLocation = (new File(projectPath).toURI());
				// Create the project description
				IProjectDescription desc = ResourcesPlugin.getWorkspace()
						.newProjectDescription(projectName);
				// Set the location of the project
				desc.setLocationURI(defaultProjectLocation);
				// Create the project
				createdProject.create(desc, null);
			}
			// Open the project if it is not already open
			if (createdProject.exists() && !createdProject.isOpen()) {
				createdProject.open(null);
			}
			// Refresh the workspace
			createdProject.refreshLocal(IResource.DEPTH_INFINITE, null);
		} catch (CoreException e) {
			// Catch exception for creating the project
			e.printStackTrace();
			fail();
		}

		return createdProject;
	}

	/**
	 * This operation cleans up after the test and removes the project space.
	 */
	@AfterClass
	static public void teardown() {

		// Delete the projects.
		try {
			project.delete(true, null);
			otherProject.delete(true, null);
		} catch (CoreException e) {
			// Complain
			e.printStackTrace();
		}

		// Stop the provider
		xmlpp.stop();

		return;
	}

	/**
	 * This is a private utility operation used by the tests.
	 * 
	 * @param name
	 *            the name that the resource should have
	 * @param projectToCheck
	 *            The project to check
	 * @return true if the file was found, false if not
	 */
	private boolean checkPersistedFile(String name, IProject projectToCheck) {

		System.out.println("XMLPersistenceProviderTester Message: "
				+ "Searching for " + name);

		try {
			// Get the list of resources
			IResource[] resources = projectToCheck.members();
			// Check the list and make sure the file was stored
			for (IResource resource : resources) {
				System.out.println("XMLPersistenceProviderTester Message: "
						+ "Found resource " + resource.getName());
				if (resource.getName().equals(name)) {
					return true;
				}
			}
		} catch (CoreException e) {
			// Complain
			e.printStackTrace();
			fail();
		}

		return false;
	}

	/**
	 * This is a utility operation that just delays the execution of the program
	 * for the specified number of seconds.
	 * 
	 * @param seconds
	 *            The time to delay.
	 */
	private void pause(int seconds) {

		// Just delay the thread
		Thread.currentThread();
		try {
			Thread.sleep(seconds * 1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return;
	}

	/**
	 * Check that we can rename Items with the persistence provider.
	 * 
	 */
	@Test
	public void checkRename() {
		// Create a MOOSE item
		MOOSEModelBuilder builder = new MOOSEModelBuilder();
		Item item = builder.build(project);
		String name = item.getName().replace(" ", "_") + ".xml";

		// Persist it
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		xmlpp.renameItem(item, "testItemName");

		pause(2);

		assertTrue(project.getFile("testItemName.xml").exists());
		assertFalse(project.getFile(name).exists());

	}

	/**
	 * This operation checks the ability of the XMLPersistenceProvider to
	 * persist Items to its project space. It also checks update() since that
	 * operation is identical to persist().
	 */
	@Test
	public void checkPersist() {

		// Create a MOOSE item
		MOOSEModelBuilder builder = new MOOSEModelBuilder();
		Item item = builder.build(project);
		String name = item.getName().replace(" ", "_") + ".xml";

		// Persist it
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Check the project space to make sure it was persisted
		assertTrue(checkPersistedFile(name, project));

		// Update the file
		assertTrue(xmlpp.updateItem(item));

		// Check the project space to make sure it was not deleted or something
		assertTrue(checkPersistedFile(name, project));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Change the id of the item and persist it so that it shows up in the
		// project space as a different item
		item.setId(2);
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Check the project space to make sure it was persisted
		name = item.getName().replace(" ", "_") + ".xml";
		assertTrue(checkPersistedFile(name, project));

		// Delete the file
		xmlpp.deleteItem(item);

		// Check persistence into the second project
		item = builder.build(otherProject);
		name = item.getName().replace(" ", "_") + ".xml";

		// Persist it
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Check the project space to make sure it was persisted
		assertTrue(checkPersistedFile(name, otherProject));

		// Delete the file
		xmlpp.deleteItem(item);

		return;
	}

	/**
	 * This operation checks the load operation to make sure that the
	 * XMLPersistenceProvider can properly load Items from the workspace. It
	 * also checks the loadAll() and delete() operation since they are closely
	 * related to the ability to load a single Item.
	 */
	@Test
	public void checkLoad() {

		// Create a MOOSE item
		MOOSEModelBuilder builder = new MOOSEModelBuilder();
		VibeLauncherBuilder vibeBuilder = new VibeLauncherBuilder();
		Item item = builder.build(project);
		String name;
		int passedCount = 0;

		// Persist it
		item.setId(3);
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Load the Item and check it
		Item loadedItem = xmlpp.loadItem(3);
		assertNotNull(loadedItem);
		// Set the project so that the Items will match. Recall that serialized
		// Items do not store their project info!
		loadedItem.setProject(project);
		assertEquals(item, loadedItem);

		// Now load "both" of the Items by calling load all
		ArrayList<Item> items = xmlpp.loadItems();
		assertNotNull(items);

		// Check the list
		Item listItem = items.get(0);
		// Look for the correct name and item id
		assertEquals(listItem.getName(), MOOSEModelBuilder.name);
		assertEquals(listItem.getId(), 3);

		// Delete the item
		assertTrue(xmlpp.deleteItem(item));

		// Wait while the file is deleted.
		pause(2);

		// Check the project and make sure it is gone
		name = item.getName().replace(" ", "_") + ".xml";
		assertFalse(checkPersistedFile(name, project));

		// Add a CAEBAT KVPair item, which has a hyphenated name, to make sure
		// the the provider can handle it.
		Item vibeItem = vibeBuilder.build(project);
		vibeItem.setId(5);
		assertTrue(xmlpp.persistItem(vibeItem));
		pause(2);
		items = xmlpp.loadItems();
		assertEquals(1, items.size());
		listItem = items.get(0);
		assertEquals(vibeItem.getName(), listItem.getName());

		// Delete the Item
		xmlpp.deleteItem(vibeItem);
		pause(2);

		// Check loading from the other project. Create a new Item.
		item = builder.build(otherProject);
		// Persist it
		item.setId(3);
		assertTrue(xmlpp.persistItem(item));

		// Wait while the file is persisted. The MOOSE Model takes about a half
		// a second, but lets wait two.
		pause(2);

		// Load the Item and check it
		name = item.getName().replace(" ", "_") + ".xml";
		loadedItem = xmlpp.loadItem(otherProject.getFile(name));
		assertNotNull(loadedItem);
		// Set the project so that the Items will match. Recall that serialized
		// Items do not store their project info!
		loadedItem.setProject(otherProject);
		assertEquals(item, loadedItem);

		return;
	}

	/**
	 * This operation insures that IWriter interface is implemented as described
	 * by the XML persistence provider and that the operations function.
	 * 
	 * @throws JAXBException
	 *             JAXB wasn't able to load
	 * @throws CoreException
	 *             Eclipse Resources failed to load the file
	 */
	@Test
	public void checkIWriterOperations() throws JAXBException, CoreException {

		// Create a Form
		Form form = new Form();
		form.setName("The artist formerly known as Prince");
		IFile file = project.getFile("iwriter_test_form.xml");

		// Try to persist it
		xmlpp.write(form, file);
		pause(2);

		// Check the project to see if it was written
		project.refreshLocal(IResource.DEPTH_INFINITE, null);
		assertTrue(file.exists());
		// Read it back in and compare the results. Create new instance of
		// object from file and then return it.
		JAXBContext context = JAXBContext.newInstance(Form.class);
		Unmarshaller unmarshaller = context.createUnmarshaller();
		// New object created
		Form loadedForm = (Form) unmarshaller.unmarshal(file.getContents());
		assertEquals(form, loadedForm);

		return;
	}

	/**
	 * This operation insures that IReader interface is implemented as described
	 * by the XML persistence provider and that the operations function.
	 * 
	 * @throws JAXBException
	 *             JAXB could not load
	 * @throws CoreException
	 *             Eclispe Resources could not read the file
	 */
	@Test
	public void checkIReaderOperations() throws JAXBException, CoreException {

		// Create a Form
		Form form = new Form();
		form.setName("The artist formerly known as Prince");
		IFile file = project.getFile("ireader_test_form.xml");

		// Create a context and write the Form to a stream
		JAXBContext jaxbContext = JAXBContext.newInstance(Form.class);
		Marshaller marshaller = jaxbContext.createMarshaller();
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		marshaller.marshal(form, outputStream);
		// Convert it to an input stream so it can be pushed to file
		ByteArrayInputStream inputStream = new ByteArrayInputStream(
				outputStream.toByteArray());
		// Update the output file if it already exists
		if (file.exists()) {
			file.setContents(inputStream, IResource.FORCE, null);
		} else {
			// Or create it from scratch
			file.create(inputStream, IResource.FORCE, null);
		}

		// Read the Form back in with the provider
		Form loadedForm = xmlpp.read(file);
		assertNotNull(loadedForm);
		assertEquals(loadedForm, form);

		return;
	}

}