/*******************************************************************************
 *  Copyright (c) 2007, 2017 IBM Corporation and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.p2.tests.engine;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.equinox.internal.p2.engine.EngineActivator;
import org.eclipse.equinox.internal.p2.engine.Profile;
import org.eclipse.equinox.internal.p2.engine.SimpleProfileRegistry;
import org.eclipse.equinox.p2.core.IAgentLocation;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
import org.eclipse.equinox.p2.tests.TestActivator;

/**
 * Simple test of the engine API.
 */
public class ProfileRegistryTest extends AbstractProvisioningTest {
	private static final String PROFILE_NAME = "ProfileRegistryTest.profile";
	private IProfileRegistry registry;

	public ProfileRegistryTest() {
		super("");
	}

	public ProfileRegistryTest(String name) {
		super(name);
	}

	protected void getServices() {
		registry = getAgent().getService(IProfileRegistry.class);
	}

	private void ungetServices() {
		registry = null;
	}

	private static void saveProfile(IProfileRegistry iRegistry, Profile profile) {
		SimpleProfileRegistry registry = (SimpleProfileRegistry) iRegistry;
		profile.setChanged(false);
		registry.lockProfile(profile);
		try {
			profile.setChanged(true);
			registry.updateProfile(profile);
		} finally {
			registry.unlockProfile(profile);
			profile.setChanged(false);
		}
	}

	private void restart() {
		try {
			ungetServices();
			getServices();
		} catch (Exception e) {
			fail();
			e.printStackTrace();
		}
	}

	@Override
	protected void setUp() throws Exception {
		getServices();
		//ensure we start in a clean state
		registry.removeProfile(PROFILE_NAME);
	}

	@Override
	protected void tearDown() throws Exception {
		super.tearDown();
		ungetServices();
	}

	public void testAddRemoveProfile() {
		assertNull(registry.getProfile(PROFILE_NAME));
		IProfile test = createProfile(PROFILE_NAME);
		assertEquals(test.getProfileId(), registry.getProfile(PROFILE_NAME).getProfileId());
		registry.removeProfile(PROFILE_NAME);
		assertNull(registry.getProfile(PROFILE_NAME));
	}

	public void testBasicPeristence() {
		assertNull(registry.getProfile(PROFILE_NAME));
		IProfile test = createProfile(PROFILE_NAME);
		assertEquals(test.getProfileId(), registry.getProfile(PROFILE_NAME).getProfileId());

		restart();
		test = registry.getProfile(PROFILE_NAME);
		assertNotNull(test);
		registry.removeProfile(PROFILE_NAME);

		restart();
		assertNull(registry.getProfile(PROFILE_NAME));
	}

	public void testPropertyPeristence() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Map<String, String> properties = new HashMap<>();
		properties.put("test", "test");
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME, properties);
		assertTrue(profile.getProperties().containsKey("test"));
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertNotNull(profile);
		assertTrue(profile.getProperties().containsKey("test"));
		profile.removeProperty("test");
		assertNull(profile.getProperty("test"));
		saveProfile(registry, profile);
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertNull(profile.getProperty("test"));
		registry.removeProfile(PROFILE_NAME);
		restart();

		assertNull(registry.getProfile(PROFILE_NAME));
	}

	public void testIUPeristence() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);
		assertTrue(profile.query(QueryUtil.createIUAnyQuery(), null).isEmpty());
		profile.addInstallableUnit(createIU("test"));
		assertEquals(1, queryResultSize(profile.query(QueryUtil.createIUAnyQuery(), null)));
		saveProfile(registry, profile);
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertEquals(1, queryResultSize(profile.query(QueryUtil.createIUAnyQuery(), null)));
		profile.removeInstallableUnit(createIU("test"));
		assertTrue(profile.query(QueryUtil.createIUAnyQuery(), null).isEmpty());
		saveProfile(registry, profile);
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertTrue(profile.query(QueryUtil.createIUAnyQuery(), null).isEmpty());
		registry.removeProfile(PROFILE_NAME);
		restart();
		assertNull(registry.getProfile(PROFILE_NAME));
	}

	public void testIUPropertyPeristence() throws ProvisionException {
		Map<String, String> properties = new HashMap<>();
		properties.put("test", "test");
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);
		profile.addInstallableUnit(createIU("test"));
		profile.addInstallableUnitProperties(createIU("test"), properties);
		assertEquals("test", profile.getInstallableUnitProperty(createIU("test"), "test"));
		saveProfile(registry, profile);
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertEquals("test", profile.getInstallableUnitProperty(createIU("test"), "test"));
		profile.removeInstallableUnitProperty(createIU("test"), "test");
		assertNull(profile.getInstallableUnitProperty(createIU("test"), "test"));
		saveProfile(registry, profile);
		restart();

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertNull(profile.getInstallableUnitProperty(createIU("test"), "test"));
		registry.removeProfile(PROFILE_NAME);
		restart();
		assertNull(registry.getProfile(PROFILE_NAME));
	}

	public void testBogusRegistry() {
		File testData = getTestData("0.1", "testData/engineTest/bogusRegistryContent");
		File tempFolder = getTempFolder();
		copy("0.2", testData, tempFolder);
		createAndValidateProfileRegistry(tempFolder, "SDKProfile");
	}

	public void testTimestampedProfiles() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Map<String, String> properties = new HashMap<>();
		properties.put("test", "test");
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME, properties);
		long oldtimestamp = profile.getTimestamp();
		assertTrue(profile.getProperties().containsKey("test"));
		long[] timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(1, timestamps.length);

		assertTrue(profile.getProperties().containsKey("test"));
		profile.removeProperty("test");
		assertNull(profile.getProperty("test"));
		saveProfile(registry, profile);
		timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(2, timestamps.length);

		Profile oldProfile = (Profile) registry.getProfile(PROFILE_NAME, oldtimestamp);
		assertTrue(oldProfile.getProperties().containsKey("test"));
		assertFalse(profile.getTimestamp() == oldProfile.getTimestamp());

		registry.removeProfile(PROFILE_NAME);
		assertNull(registry.getProfile(PROFILE_NAME, oldtimestamp));
		timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(0, timestamps.length);
	}

	public void testIsCurrent() throws Exception {
		assertNull(registry.getProfile(PROFILE_NAME));
		Map<String, String> properties = new HashMap<>();
		properties.put("test", "test");
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME, properties);

		assertTrue(registry.isCurrent(profile));
		profile.setProperty("x", "1");
		assertFalse(registry.isCurrent(profile));

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertTrue(registry.isCurrent(profile));

		SimpleProfileRegistry simpleRegistry = (SimpleProfileRegistry) registry;
		Profile profile2 = (Profile) registry.getProfile(PROFILE_NAME);

		simpleRegistry.lockProfile(profile2);
		try {
			profile2.setProperty("x", "1");
			simpleRegistry.updateProfile(profile2);
		} finally {
			simpleRegistry.unlockProfile(profile2);
		}

		assertFalse(registry.isCurrent(profile));

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertTrue(registry.isCurrent(profile));
		IAgentLocation agentLocation = getAgentLocation();
		SimpleProfileRegistry simpleRegistry2 = new SimpleProfileRegistry(getAgent(), SimpleProfileRegistry.getDefaultRegistryDirectory(agentLocation));
		profile2 = (Profile) simpleRegistry2.getProfile(PROFILE_NAME);
		simpleRegistry2.lockProfile(profile2);
		try {
			profile2.setProperty("x", "2");
			simpleRegistry2.updateProfile(profile2);
		} finally {
			simpleRegistry2.unlockProfile(profile2);
		}

		assertFalse(registry.isCurrent(profile));

		profile = (Profile) registry.getProfile(PROFILE_NAME);
		assertTrue(registry.isCurrent(profile));

	}

	public void testProfileLockingNested() {
		File testData = getTestData("0.1", "testData/engineTest/SimpleRegistry");
		File tempFolder = getTempFolder();
		copy("0.2", testData, tempFolder);

		final String SIMPLE_PROFILE = "Simple";

		SimpleProfileRegistry simpleRgy = createAndValidateProfileRegistry(tempFolder, SIMPLE_PROFILE);
		Profile simpleProfile = (Profile) simpleRgy.getProfile(SIMPLE_PROFILE);
		assertNotNull(simpleProfile);

		// Try lock, unlock and then unlock again which should fail
		simpleRgy.lockProfile(simpleProfile);
		simpleRgy.unlockProfile(simpleProfile);
		try {
			simpleRgy.unlockProfile(simpleProfile);
			fail("Should not allow unlock() without calling lock() first");
		} catch (IllegalStateException e) {
			// Expected
		}

		// Try lock, lock, unlock, unlock should pass
		simpleRgy.lockProfile(simpleProfile);
		try {
			simpleRgy.lockProfile(simpleProfile);
			fail("Profile does not permit reentrant locking");
		} catch (IllegalStateException e) {
			// expected
		}
		simpleRgy.unlockProfile(simpleProfile);

		// NOTE: remaining nested tests are commented out for now
		// These tests will work on Win XP and Linux with Java 6. On the Mac and Linux (at least with Java 5) it appears
		// that the class libraries will permit more than one file lock from the same process.
		// It should be noted that the cross-process locking still works correctly and that the profile registry shares
		// the same file lock to prevent these problems.

		/*
				// Try nested locks with checks for lock file
				simpleRgy.lockProfile(simpleProfile);
				simpleRgy.lockProfile(simpleProfile);
				simpleRgy.unlockProfile(simpleProfile);
				// Create a lock file to confirm locking

				File lockDirectory = new File(getResourceAsBundleRelFile("testData/engineTest/SimpleRegistry/"), SIMPLE_PROFILE + ".profile");
				File lockFile = new File(lockDirectory, ".lock");
				assertTrue("Lock file does not exist", lockFile.exists());

				ProfileLock profileLock = new ProfileLock(lockDirectory);
				boolean locked = profileLock.lock();
				try {
					assertFalse("Lock file was not locked", locked);
				} finally {
					if (locked)
						profileLock.unlock();
				}
				simpleRgy.unlockProfile(simpleProfile);
				simpleRgy.lockProfile(simpleProfile);
				simpleRgy.unlockProfile(simpleProfile);
				locked = profileLock.lock();
				try {
					assertTrue("Lock file could not be locked", locked);
				} finally {
					if (locked)
						profileLock.unlock();
				}
				assertTrue("Lock file could not removed", lockFile.delete());
		*/
	}

	public void testProfileLockingInProcessMultiThreads() {
		File testData = getTestData("0.1", "testData/engineTest/SimpleRegistry");
		File tempFolder = getTempFolder();
		copy("0.2", testData, tempFolder);

		final String SIMPLE_PROFILE = "Simple";
		final SimpleProfileRegistry simpleRgy = createAndValidateProfileRegistry(tempFolder, SIMPLE_PROFILE);
		final Profile simpleProfile = (Profile) simpleRgy.getProfile(SIMPLE_PROFILE);
		assertNotNull(simpleProfile);

		// Lock and then lock/unlock from another thread which should fail
		simpleRgy.lockProfile(simpleProfile);
		final Object lock = new Object();
		Thread t1 = new Thread() {
			@Override
			public void run() {
				try {
					simpleRgy.unlockProfile(simpleProfile);
					fail("This thread is not the owner and unlock should have failed!");
				} catch (IllegalStateException e) {
					// Expected!
				}
				synchronized (lock) {
					lock.notify();
				}
			}
		};
		synchronized (lock) {
			t1.start();
			try {
				lock.wait(5000);
			} catch (InterruptedException e) {
				// ignore
			}
		}
		Thread t2 = new Thread() {
			@Override
			public void run() {
				try {
					simpleRgy.lockProfile(simpleProfile);
				} catch (IllegalStateException e) {
					fail("The profile should let the thread wait until it can be locked!");
				} finally {
					simpleRgy.unlockProfile(simpleProfile);
				}
				synchronized (lock) {
					lock.notify();
				}
			}
		};
		synchronized (lock) {
			t2.start();
			yieldAndWait();
			simpleRgy.unlockProfile(simpleProfile);
			try {
				lock.wait(500);
			} catch (InterruptedException e) {
				// ignore
			}
		}

		// Remove the newly created profile file
		simpleRgy.removeProfile(PROFILE_NAME); // To avoid it locking the latest file
	}

	//	public void testProfileLockingMultiProcesses() {
	//		File testData = getTestData("0.1", "testData/engineTest/SimpleRegistry");
	//		File tempFolder = getTempFolder();
	//		copy("0.2", testData, tempFolder);
	//
	//		final String SIMPLE_PROFILE = "Simple";
	//
	//		SimpleProfileRegistry simpleRgy = createAndValidateProfileRegistry(tempFolder, SIMPLE_PROFILE);
	//		Profile simpleProfile = (Profile) simpleRgy.getProfile(SIMPLE_PROFILE);
	//		assertNotNull(simpleProfile);
	//
	//		// Make a dummy change to the profile
	//		Properties props = new Properties();
	//		props.put("test", "locking");
	//		simpleProfile.addProperties(props);
	//
	//		// Create a lock file to simulate cross-process locking
	//		MockFileLock mockLock = new MockFileLock(tempFolder, SIMPLE_PROFILE);
	//		mockLock.createExternalProcessUsedForLocking();
	//
	//		// Now save profile - this should fail
	//		try {
	//			saveProfile(simpleRgy, simpleProfile);
	//			fail("This should have failed because profile is already locked!");
	//		} catch (IllegalStateException e) {
	//			// Expected!
	//		}
	//
	//		// Get rid of the lock
	//		mockLock.shutdownExternalProcessUsedForLocking();
	//		mockLock = null;
	//
	//		// Try again, it should succeed
	//		saveProfile(simpleRgy, simpleProfile);
	//
	//		// Remove the newly created profile file
	//		simpleRgy.removeProfile(PROFILE_NAME); // To avoid it locking the latest file
	//	}

	private SimpleProfileRegistry createAndValidateProfileRegistry(File registryFolder, String id) {
		SimpleProfileRegistry simpleRegistry = new SimpleProfileRegistry(getAgent(), registryFolder, null, false);
		IProfile[] profiles = simpleRegistry.getProfiles();
		assertNotNull(profiles);
		assertEquals(1, profiles.length);
		assertEquals(id, profiles[0].getProfileId());
		return simpleRegistry;
	}

	static File getResourceAsBundleRelFile(final String bundleRelPathStr) throws IOException {
		return new File(FileLocator.resolve(TestActivator.getContext().getBundle().getEntry(bundleRelPathStr)).getPath());
	}

	static void yieldAndWait() {
		Thread.yield();
		try {
			Thread.sleep(100);
		} catch (InterruptedException e1) {
			fail(e1.getMessage());
		}
	}

	//	private static class MockFileLock {
	//		File lockFile;
	//
	//		MockFileLock(File registryFolder, String name) {
	//			final String lOCK_FILENAME = ".lock";
	//			File lockDir = null;
	//			lockDir = new File(registryFolder, name + ".profile");
	//			lockDir.mkdir();
	//			lockFile = new File(lockDir, lOCK_FILENAME);
	//		}
	//
	//		File getLockFile() {
	//			return lockFile;
	//		}
	//
	//		/**
	//		 * This assumes that
	//		 * 	(1) "java" is present in the System path
	//		 * 	(2) "SimpleFileLockerApp.jar" is present in /org.eclipse.equinox.p2.tests/testData/engineTest
	//		 *
	//		 * @see {@link SimpleFileLockerApp}.
	//		 */
	//		void createExternalProcessUsedForLocking() {
	//			File appJar = null;
	//			try {
	//				appJar = getResourceAsBundleRelFile("testData/engineTest/SimpleFileLockerApp.jar");
	//			} catch (IOException e1) {
	//				fail(e1.getMessage());
	//			}
	//			String lockFileDir = lockFile.getParentFile().getAbsolutePath();
	//			String[] cmdArray = {"java", "-cp", appJar.getAbsolutePath(), "org/eclipse/equinox/p2/tests/engine/SimpleFileLockerApp", lockFileDir, "10"};
	//			try {
	//				Runtime.getRuntime().exec(cmdArray);
	//				watForInitialization();
	//			} catch (IOException e) {
	//				fail(e.getMessage());
	//			}
	//		}
	//
	//		void watForInitialization() {
	//			waitFor(/*startup=>*/true);
	//		}
	//
	//		void waitForCompletion() {
	//			waitFor(/*startup=>*/false);
	//		}
	//
	//		private final int MAX_RETRIES = 10; // A guard against looping indefinitely!
	//
	//		private void waitFor(boolean startup) {
	//			int attempts = 0;
	//			boolean shouldWait = true;
	//			do {
	//				sleep(1000);
	//				shouldWait = startup ? !lockFile.exists() : lockFile.exists();
	//			} while (attempts++ < MAX_RETRIES && shouldWait);
	//			String errMsg = "SimpleFileLockerApp hasn't yet " + (startup ? "started up!" : "completed!");
	//			assertTrue(errMsg, attempts < MAX_RETRIES);
	//		}
	//
	//		private void sleep(int millisecs) {
	//			try {
	//				Thread.sleep(millisecs);
	//			} catch (InterruptedException e) {
	//				// Ignore
	//			}
	//		}
	//
	//		void shutdownExternalProcessUsedForLocking() {
	//			File done = new File(lockFile.getParentFile(), ".done");
	//			try {
	//				done.createNewFile();
	//				waitForCompletion();
	//			} catch (IOException e) {
	//				fail(e.getMessage());
	//			}
	//		}
	//	}

	public void testPersistenceFormatNotGzipped() {
		//in a profile with an engine version from 3.5.0 or earlier, we must not gzip the profile registry
		IInstallableUnit engineIU = createEclipseIU("org.eclipse.equinox.p2.engine", Version.create("1.0.100.v20090605"));
		File folder = getTempFolder();
		folder.mkdirs();
		SimpleProfileRegistry profileRegistry = new SimpleProfileRegistry(getAgent(), folder, null, false);
		Profile profile = new Profile(getAgent(), getName(), null, null);
		profile.addInstallableUnit(engineIU);
		Method saveMethod;
		try {
			saveMethod = registry.getClass().getDeclaredMethod("saveProfile", Profile.class);
			saveMethod.setAccessible(true);
			saveMethod.invoke(profileRegistry, profile);
		} catch (SecurityException e) {
			fail();
		} catch (NoSuchMethodException e) {
			fail();
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		} catch (InvocationTargetException e) {
			fail();
		}
		File profileFolder = new File(folder, getName() + ".profile");
		File[] filesFound = profileFolder.listFiles((FileFilter) pathname -> pathname.getName().endsWith(".profile"));
		assertEquals(1, filesFound.length);
		filesFound = profileFolder.listFiles((FileFilter) pathname -> pathname.getName().endsWith(".profile.gz"));
		assertEquals(0, filesFound.length);
	}

	/**
	 * Asserts that the profile registry persistence honours the system property for controlling
	 * the profile format. See bug 285774.
	 */
	public void testPersistenceFormatOverride() {
		try {
			IInstallableUnit engineIU = createEclipseIU("org.eclipse.equinox.p2.engine", Version.create("55.2"));
			final String[] values = new String[] {"", "blort", null, EngineActivator.PROFILE_FORMAT_UNCOMPRESSED};
			for (String currentValue : values) {
				if (currentValue == null)
					System.getProperties().remove(EngineActivator.PROP_PROFILE_FORMAT);
				else
					System.getProperties().put(EngineActivator.PROP_PROFILE_FORMAT, currentValue);
				File folder = getTempFolder();
				folder.mkdirs();
				SimpleProfileRegistry profileRegistry = new SimpleProfileRegistry(getAgent(), folder, null, false);
				Profile profile = new Profile(getAgent(), getName(), null, null);
				profile.addInstallableUnit(engineIU);
				Method saveMethod;
				try {
					saveMethod = registry.getClass().getDeclaredMethod("saveProfile", Profile.class);
					saveMethod.setAccessible(true);
					saveMethod.invoke(profileRegistry, profile);
				} catch (SecurityException e) {
					fail("1.0", e);
				} catch (NoSuchMethodException e) {
					fail("1.1", e);
				} catch (IllegalArgumentException e) {
					fail("1.2", e);
				} catch (IllegalAccessException e) {
					fail("1.3", e);
				} catch (InvocationTargetException e) {
					fail("1.4", e);
				}
				File profileFolder = new File(folder, getName() + ".profile");
				profileFolder.listFiles((FileFilter) pathname -> {
					if (pathname.getName().endsWith(".profile"))
						assertEquals("2.0." + currentValue, EngineActivator.PROFILE_FORMAT_UNCOMPRESSED, currentValue);
					else if (pathname.getName().endsWith(".profile.gz"))
						assertFalse("2.1." + currentValue, EngineActivator.PROFILE_FORMAT_UNCOMPRESSED.equals(currentValue));
					return false;
				});
			}
		} finally {
			System.getProperties().remove(EngineActivator.PROP_PROFILE_FORMAT);
		}
	}

	public void testPersistenceFormatGzipped() {
		//in a profile with an engine version from 3.5.1 or later, we gzip the profile registry
		IInstallableUnit engineIU = createEclipseIU("org.eclipse.equinox.p2.engine", Version.create("1.0.101"));
		File folder = getTempFolder();
		folder.mkdirs();
		SimpleProfileRegistry profileRegistry = new SimpleProfileRegistry(getAgent(), folder, null, false);
		Profile profile = new Profile(getAgent(), getName(), null, null);
		profile.addInstallableUnit(engineIU);
		Method saveMethod;
		try {
			saveMethod = registry.getClass().getDeclaredMethod("saveProfile", Profile.class);
			saveMethod.setAccessible(true);
			saveMethod.invoke(profileRegistry, profile);
		} catch (SecurityException e) {
			fail();
		} catch (NoSuchMethodException e) {
			fail();
		} catch (IllegalArgumentException e) {
			fail();
		} catch (IllegalAccessException e) {
			fail();
		} catch (InvocationTargetException e) {
			fail();
		}
		File profileFolder = new File(folder, getName() + ".profile");
		File[] filesFound = profileFolder.listFiles((FileFilter) pathname -> pathname.getName().endsWith(".profile.gz"));
		assertEquals(1, filesFound.length);
	}

	public void testRemoveProfileTimestamps() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Map<String, String> properties = new HashMap<>();
		properties.put("test", "test");
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME, properties);
		assertTrue(profile.getProperties().containsKey("test"));
		long[] timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(1, timestamps.length);

		assertTrue(profile.getProperties().containsKey("test"));
		profile.removeProperty("test");
		assertNull(profile.getProperty("test"));
		saveProfile(registry, profile);
		timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(2, timestamps.length);

		profile.setProperty("test2", "test2");
		saveProfile(registry, profile);
		timestamps = registry.listProfileTimestamps(PROFILE_NAME);

		// We have three timestamps and should be able to remove two
		// of them (but not the current)
		assertEquals(3, timestamps.length);
		int fail = 0;

		for (long timestamp : timestamps) {
			try {
				registry.removeProfile(PROFILE_NAME, timestamp);
			} catch (ProvisionException e) {
				fail++;
			}
		}
		timestamps = registry.listProfileTimestamps(PROFILE_NAME);
		assertEquals(1, timestamps.length);
		assertEquals(1, fail);
	}

	public void testSetProfileStateProperties() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);
		long[] states = registry.listProfileTimestamps(profile.getProfileId());
		long goodTimestamp = states[0];
		long badTimestamp = goodTimestamp + 1;
		Map<String, String> properties = new HashMap<>();
		properties.put("foo", "bar");

		// Test invalid arguments handled as per contract
		try {
			registry.setProfileStateProperties(null, goodTimestamp, properties);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		try {
			registry.setProfileStateProperties(profile.getProfileId(), goodTimestamp, null);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		assertNotOK(registry.setProfileStateProperties(profile.getProfileId(), badTimestamp, properties));

		// Test single set.
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), goodTimestamp, properties));

		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), goodTimestamp);
		assertEquals(1, result.size());
		assertEquals("bar", result.get("foo"));

		// Test reset and multiple set
		properties.put("foo", "newBar");
		properties.put("two", "three");

		assertOK(registry.setProfileStateProperties(profile.getProfileId(), goodTimestamp, properties));
		result = registry.getProfileStateProperties(profile.getProfileId(), goodTimestamp);
		assertEquals(2, result.size());
		assertEquals("newBar", result.get("foo"));
		assertEquals("three", result.get("two"));
	}

	public void testSetProfileStateProperty() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);
		long[] states = registry.listProfileTimestamps(profile.getProfileId());
		long goodTimestamp = states[0];
		long badTimestamp = goodTimestamp + 1;

		// Test invalid arguments handled as per contract
		try {
			registry.setProfileStateProperty(null, goodTimestamp, "foo", "bar");
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		try {
			registry.setProfileStateProperty(profile.getProfileId(), goodTimestamp, null, "bar");
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		try {
			registry.setProfileStateProperty(profile.getProfileId(), goodTimestamp, "foo", null);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		assertNotOK(registry.setProfileStateProperty(profile.getProfileId(), badTimestamp, "foo", "bar"));

		// Test single set.
		assertOK(registry.setProfileStateProperty(profile.getProfileId(), goodTimestamp, "foo", "bar"));

		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), goodTimestamp);
		assertEquals(1, result.size());
		assertEquals("bar", result.get("foo"));

		// Test reset and multiple set
		assertOK(registry.setProfileStateProperty(profile.getProfileId(), goodTimestamp, "foo", "newBar"));
		assertOK(registry.setProfileStateProperty(profile.getProfileId(), goodTimestamp, "two", "three"));
		result = registry.getProfileStateProperties(profile.getProfileId(), goodTimestamp);
		assertEquals(2, result.size());
		assertEquals("newBar", result.get("foo"));
		assertEquals("three", result.get("two"));
	}

	public void testPruneProfileStateProperties() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);

		Map<String, String> profileProperties = new HashMap<>();
		profileProperties.put("profileFoo", "profileBar");

		profile.addProperties(profileProperties);

		saveProfile(registry, profile);

		long[] states = registry.listProfileTimestamps(profile.getProfileId());
		assertEquals(2, registry.listProfileTimestamps(profile.getProfileId()).length);

		Map<String, String> stateProperties1 = new HashMap<>();
		Map<String, String> stateProperties2 = new HashMap<>();

		stateProperties1.put("one", "two");
		stateProperties1.put("a", "b");
		stateProperties1.put("z", "y");

		stateProperties2.put("one", "three");
		stateProperties2.put("a", "c");
		stateProperties2.put("zz", "yy");

		// Check regular states are what we expect.
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[0], stateProperties1));
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties2));

		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(3, result.size());
		assertEquals("two", result.get("one"));
		assertEquals("b", result.get("a"));
		assertEquals("y", result.get("z"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1] + 1);
		assertEquals(0, result.size());

		// Remove a profile
		registry.removeProfile(PROFILE_NAME, states[0]);
		assertEquals(1, registry.listProfileTimestamps(profile.getProfileId()).length);

		// Ensure all properties for the first state were removed when we removed the state
		assertTrue(registry.getProfileStateProperties(profile.getProfileId(), states[0]).isEmpty());

		// Ensure we still have properties for the other state.
		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1] + 1);
		assertEquals(0, result.size());

		// Force the states to be pruned by causing a new state to be written.
		Map<String, String> stateProperties3 = new HashMap<>();
		stateProperties3.put("AmIPruned", "yes");
		registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties3);

		// Check results.  We expect that state properties for the first state are now gone.
		result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(0, result.size());

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(4, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));
		assertEquals("yes", result.get("AmIPruned"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1] + 1);
		assertEquals(0, result.size());

	}

	public void testGetProfileStateProperties() throws ProvisionException {
		assertNull(registry.getProfile(PROFILE_NAME));
		Profile profile = (Profile) registry.addProfile(PROFILE_NAME);

		Map<String, String> profileProperties = new HashMap<>();
		profileProperties.put("profileFoo", "profileBar");

		profile.addProperties(profileProperties);

		saveProfile(registry, profile);

		long[] states = registry.listProfileTimestamps(profile.getProfileId());

		Map<String, String> stateProperties1 = new HashMap<>();
		Map<String, String> stateProperties2 = new HashMap<>();

		stateProperties1.put("one", "two");
		stateProperties1.put("a", "b");
		stateProperties1.put("z", "y");

		stateProperties2.put("one", "three");
		stateProperties2.put("a", "c");
		stateProperties2.put("zz", "yy");

		// Test invalid arguments handled as per contract
		try {
			registry.getProfileStateProperties(null, states[0]);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		// Test getting before any sets. (I.E. file does not exist)
		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(0, result.size());

		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[0], stateProperties1));
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties2));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(3, result.size());
		assertEquals("two", result.get("one"));
		assertEquals("b", result.get("a"));
		assertEquals("y", result.get("z"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1] + 1);
		assertEquals(0, result.size());
	}

	public void testGetProfileStateProperties2() throws ProvisionException {
		assertNull(registry.getProfile(getName()));
		Profile profile = (Profile) registry.addProfile(getName());

		Map<String, String> profileProperties = new HashMap<>();
		profileProperties.put("profileFoo", "profileBar");

		profile.addProperties(profileProperties);

		saveProfile(registry, profile);

		long[] states = registry.listProfileTimestamps(profile.getProfileId());

		Map<String, String> stateProperties1 = new HashMap<>();
		Map<String, String> stateProperties2 = new HashMap<>();

		stateProperties1.put("one", "two");
		stateProperties1.put("a", "b");
		stateProperties1.put("z", "y");

		stateProperties2.put("one", "three");
		stateProperties2.put("a", "c");
		stateProperties2.put("zz", "yy");

		// Test invalid arguments handled as per contract
		try {
			registry.getProfileStateProperties(null, "foo");
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		try {
			registry.getProfileStateProperties(profile.getProfileId(), null);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		// Test getting before any sets. (I.E. file does not exist)
		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), "one");
		assertEquals(0, result.size());

		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[0], stateProperties1));
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties2));

		result = registry.getProfileStateProperties(profile.getProfileId(), "one");
		assertEquals(2, result.size());
		assertEquals("two", result.get(String.valueOf(states[0])));
		assertEquals("three", result.get(String.valueOf(states[1])));

		result = registry.getProfileStateProperties(profile.getProfileId(), "a");
		assertEquals(2, result.size());
		assertEquals("b", result.get(String.valueOf(states[0])));
		assertEquals("c", result.get(String.valueOf(states[1])));

		result = registry.getProfileStateProperties(profile.getProfileId(), "z");
		assertEquals(1, result.size());
		assertEquals("y", result.get(String.valueOf(states[0])));

		result = registry.getProfileStateProperties(profile.getProfileId(), "zz");
		assertEquals(1, result.size());
		assertEquals("yy", result.get(String.valueOf(states[1])));

		result = registry.getProfileStateProperties(profile.getProfileId(), "none");
		assertEquals(0, result.size());
	}

	public void testRemoveProfileStateProperties() throws ProvisionException {
		assertNull(registry.getProfile(getName()));
		Profile profile = (Profile) registry.addProfile(getName());

		Map<String, String> profileProperties = new HashMap<>();
		profileProperties.put("profileFoo", "profileBar");

		profile.addProperties(profileProperties);

		saveProfile(registry, profile);

		long[] states = registry.listProfileTimestamps(profile.getProfileId());

		Map<String, String> stateProperties1 = new HashMap<>();
		Map<String, String> stateProperties2 = new HashMap<>();

		stateProperties1.put("one", "two");
		stateProperties1.put("a", "b");
		stateProperties1.put("z", "y");

		stateProperties2.put("one", "three");
		stateProperties2.put("a", "c");
		stateProperties2.put("zz", "yy");

		List<String> keys = Arrays.asList(new String[] {"one", "a", "none"});

		// Test removing before any sets. (I.E. file does not exist)
		assertOK(registry.removeProfileStateProperties(profile.getProfileId(), 1, keys));

		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[0], stateProperties1));
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties2));

		// Test invalid arguments handled as per contract
		try {
			registry.removeProfileStateProperties(null, states[0], keys);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		// Remove from one state
		assertOK(registry.removeProfileStateProperties(profile.getProfileId(), states[0], keys));
		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(1, result.size());
		assertEquals("y", result.get("z"));

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));
	}

	public void testRemoveProfileStateProperties2() throws ProvisionException {
		assertNull(registry.getProfile(getName()));
		Profile profile = (Profile) registry.addProfile(getName());

		Map<String, String> profileProperties = new HashMap<>();
		profileProperties.put("profileFoo", "profileBar");

		profile.addProperties(profileProperties);

		saveProfile(registry, profile);

		long[] states = registry.listProfileTimestamps(profile.getProfileId());

		Map<String, String> stateProperties1 = new HashMap<>();
		Map<String, String> stateProperties2 = new HashMap<>();

		stateProperties1.put("one", "two");
		stateProperties1.put("a", "b");
		stateProperties1.put("z", "y");

		stateProperties2.put("one", "three");
		stateProperties2.put("a", "c");
		stateProperties2.put("zz", "yy");

		// Test removing before any sets. (I.E. file does not exist)
		assertOK(registry.removeProfileStateProperties(profile.getProfileId(), 1, null));

		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[0], stateProperties1));
		assertOK(registry.setProfileStateProperties(profile.getProfileId(), states[1], stateProperties2));

		// ensure everything was set correctly
		Map<String, String> result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(3, result.size());
		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());

		// Test invalid arguments handled as per contract
		try {
			registry.removeProfileStateProperties(null, states[0], null);
			fail("Expected a NullPointerException.");
		} catch (NullPointerException e) {
			// expected
		}

		// Remove from one state
		assertOK(registry.removeProfileStateProperties(profile.getProfileId(), states[0], null));
		result = registry.getProfileStateProperties(profile.getProfileId(), states[0]);
		assertEquals(0, result.size());

		result = registry.getProfileStateProperties(profile.getProfileId(), states[1]);
		assertEquals(3, result.size());
		assertEquals("three", result.get("one"));
		assertEquals("c", result.get("a"));
		assertEquals("yy", result.get("zz"));
	}
}
