/*******************************************************************************
* Copyright (c) 2009, 2017 EclipseSource 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:
*   EclipseSource - initial API and implementation
******************************************************************************/
package org.eclipse.equinox.p2.tests.metadata.repository;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.metadata.repository.LocalMetadataRepository;
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.repository.IRunnableWithProgress;
import org.eclipse.equinox.p2.repository.metadata.IMetadataRepositoryManager;
import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;

/**
 * This tests Batch Execution for Metadata Repositories
 */
public class BatchExecuteMetadataRepositoryTest extends AbstractProvisioningTest {

	//metadata repository to remove on tear down
	private File repositoryFile = null;
	private URI repositoryURI = null;

	protected Collection<IInstallableUnit> createIUInCollection(String id, Version version) {
		IInstallableUnit iu = createIU(id, version);
		Collection<IInstallableUnit> result = new ArrayList<>(1);
		result.add(iu);
		return result;
	}

	boolean fileContainsString(URI location, String string) throws IOException {
		StringBuilder buffer = new StringBuilder();
		try (BufferedReader reader = new BufferedReader(new FileReader(new File(location)));) {
			while (reader.ready())
				buffer.append(reader.readLine());
			return buffer.toString().contains(string);
		}
	}

	/*
	 * This tests that in the normal case, the batch process runs fine
	 */
	public void testAdd() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			final URI contentXML = new URI(repositoryFile.toURI().toString() + "/content.xml");
			IStatus status = repo.executeBatch(monitor -> {
				repo.addInstallableUnits(createIUInCollection("foo", Version.emptyVersion));
				try {
					assertFalse("1.0", fileContainsString(contentXML, "foo"));
				} catch (IOException e) {
					fail("0.99");
				}
			}, new NullProgressMonitor());
			assertTrue(status.isOK());
			assertTrue("1.0", fileContainsString(contentXML, "foo"));
			assertEquals("2.0", 1, repo.query(QueryUtil.createIUQuery("foo"), new NullProgressMonitor()).toSet().size());
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests 3 adds
	 */
	public void testMultiAdd() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			final URI contentXML = new URI(repositoryFile.toURI().toString() + "/content.xml");
			IStatus status = repo.executeBatch(monitor -> {
				repo.addInstallableUnits(createIUInCollection("foo", Version.emptyVersion));
				repo.addInstallableUnits(createIUInCollection("bar", Version.emptyVersion));
				repo.addInstallableUnits(createIUInCollection("baz", Version.emptyVersion));
				try {
					assertFalse("1.0", fileContainsString(contentXML, "foo"));
					assertFalse("1.0", fileContainsString(contentXML, "bar"));
					assertFalse("1.0", fileContainsString(contentXML, "baz"));
				} catch (IOException e) {
					fail("0.99");
				}
			}, new NullProgressMonitor());
			assertTrue(status.isOK());
			assertEquals("1.0", 1, repo.query(QueryUtil.createIUQuery("foo"), new NullProgressMonitor()).toSet().size());
			assertEquals("1.1", 1, repo.query(QueryUtil.createIUQuery("bar"), new NullProgressMonitor()).toSet().size());
			assertEquals("1.2", 1, repo.query(QueryUtil.createIUQuery("baz"), new NullProgressMonitor()).toSet().size());
			assertTrue("2.0", fileContainsString(contentXML, "foo"));
			assertTrue("2.1", fileContainsString(contentXML, "bar"));
			assertTrue("2.2", fileContainsString(contentXML, "baz"));
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests multiple adds with an exception thrown. Makes sure that the descriptors are added
	 */
	public void testMultiAddWithException() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			final URI contentXML = new URI(repositoryFile.toURI().toString() + "/content.xml");
			IStatus status = repo.executeBatch(monitor -> {
				repo.addInstallableUnits(createIUInCollection("foo", Version.emptyVersion));
				repo.addInstallableUnits(createIUInCollection("bar", Version.emptyVersion));
				throw new RuntimeException();
			}, new NullProgressMonitor());
			assertFalse(status.isOK());
			assertEquals("1.0", 1, repo.query(QueryUtil.createIUQuery("foo"), new NullProgressMonitor()).toSet().size());
			assertEquals("1.1", 1, repo.query(QueryUtil.createIUQuery("bar"), new NullProgressMonitor()).toSet().size());
			assertTrue("2.0", fileContainsString(contentXML, "foo"));
			assertTrue("2.1", fileContainsString(contentXML, "bar"));
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests multiple adds with an exception thrown. Makes sure that the descriptors are added
	 */
	public void testAddAndRemove() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			final URI contentXML = new URI(repositoryFile.toURI().toString() + "/content.xml");
			IStatus status = repo.executeBatch(monitor -> {
				Collection<IInstallableUnit> foo = createIUInCollection("foo", Version.emptyVersion);
				repo.addInstallableUnits(foo);
				repo.addInstallableUnits(createIUInCollection("bar", Version.emptyVersion));
				repo.removeInstallableUnits(foo);
			}, new NullProgressMonitor());
			assertTrue(status.isOK());
			assertEquals("1.0", 0, repo.query(QueryUtil.createIUQuery("foo"), new NullProgressMonitor()).toSet().size());
			assertEquals("1.1", 1, repo.query(QueryUtil.createIUQuery("bar"), new NullProgressMonitor()).toSet().size());
			assertFalse("2.0", fileContainsString(contentXML, "foo"));
			assertTrue("2.1", fileContainsString(contentXML, "bar"));
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests multiple adds with an exception thrown. Makes sure that the descriptors are added
	 */
	public void testMultiAddAndRemove() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			final URI contentXML = new URI(repositoryFile.toURI().toString() + "/content.xml");
			IStatus status = repo.executeBatch(monitor -> {
				repo.addInstallableUnits(createIUInCollection("1", Version.emptyVersion));
				repo.addInstallableUnits(createIUInCollection("2", Version.emptyVersion));
				repo.addInstallableUnits(createIUInCollection("3", Version.emptyVersion));
				Collection<IInstallableUnit> foo = createIUInCollection("foo", Version.emptyVersion);
				repo.addInstallableUnits(foo);
				repo.addInstallableUnits(createIUInCollection("bar", Version.emptyVersion));
				repo.removeInstallableUnits(foo);
			}, new NullProgressMonitor());
			assertTrue(status.isOK());
			assertEquals("1.0", 4, repo.query(QueryUtil.createIUAnyQuery(), new NullProgressMonitor()).toSet().size());
			assertFalse("2.0", fileContainsString(contentXML, "foo"));
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests that in the normal case, the batch process runs fine
	 */
	public void testBatchProcessingOK() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			IStatus status = repo.executeBatch(monitor -> {
				// empty
			}, new NullProgressMonitor());
			assertTrue(status.isOK());
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	class FailingSimpleMetadataRepository extends LocalMetadataRepository {
		boolean executeBatch = false;

		/**
		 * @param repositoryName
		 * @param location
		 * @param properties
		 */
		public FailingSimpleMetadataRepository() {
			super(getAgent());
		}

		@Override
		public IStatus executeBatch(IRunnableWithProgress runnable, IProgressMonitor monitor) {
			executeBatch = true;
			return super.executeBatch(runnable, monitor);
		}

		@Override
		public void save() {
			if (executeBatch)
				throw new RuntimeException("foo");
		}
	}

	/*
	 * This tests that exceptions are properly propagated for a SimpleMetadataRepository
	 */
	public void testBatchProcessingExceptionsSimple() {
		try {
			LocalMetadataRepository simpleMetadataRepository = new FailingSimpleMetadataRepository();

			IStatus status = simpleMetadataRepository.executeBatch(monitor -> {
				throw new RuntimeException("bar");
			}, new NullProgressMonitor());
			assertFalse(status.isOK());
			assertEquals("foo", status.getException().getMessage());
			assertEquals(1, status.getChildren().length);
			assertEquals("bar", status.getChildren()[0].getMessage());
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This tests that exceptions are properly propagated for a SimpleMetadataRepository
	 */
	public void testBatchProcessingSaveExceptionSimple() {
		try {
			LocalMetadataRepository simpleMetadataRepository = new FailingSimpleMetadataRepository();

			IStatus status = simpleMetadataRepository.executeBatch(monitor -> {
				// empty
			}, new NullProgressMonitor());
			assertFalse(status.isOK());
			assertEquals("foo", status.getException().getMessage());
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	class TrackSavignSimpleMetadataRepository extends LocalMetadataRepository {
		boolean executeBatch = false;
		public boolean didSave = false;

		/**
		 * @param repositoryName
		 * @param location
		 * @param properties
		 */
		public TrackSavignSimpleMetadataRepository() {
			super(getAgent());
		}

		@Override
		public IStatus executeBatch(IRunnableWithProgress runnable, IProgressMonitor monitor) {
			executeBatch = true;
			return super.executeBatch(runnable, monitor);
		}

		@Override
		public void save() {
			if (executeBatch)
				didSave = true;
		}
	}

	/*
	 * This test ensure that a repository did in fact save the results,
	 * if there was no exception
	 */
	public void testBatchProcessingTrackSaving() {
		try {
			TrackSavignSimpleMetadataRepository simpleMetadataRepository = new TrackSavignSimpleMetadataRepository();
			simpleMetadataRepository.executeBatch(monitor -> {
				//do nothing;
			}, new NullProgressMonitor());
			assertTrue(simpleMetadataRepository.didSave);
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This test ensures that a repository did in fact save the results, even
	 * if there was an exception
	 */
	public void testBatchProcessingTrackSavingException() {
		try {
			TrackSavignSimpleMetadataRepository simpleMetadataRepository = new TrackSavignSimpleMetadataRepository();
			simpleMetadataRepository.executeBatch(monitor -> {
				throw new RuntimeException();
			}, new NullProgressMonitor());
			assertTrue(simpleMetadataRepository.didSave);
		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This test ensures that the simple repository resets the disableSave flag
	 * even if there is an exception
	 */
	public void testDisableSaveFlagResetSimple() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			repo.executeBatch(monitor -> {
				throw new RuntimeException();
			}, new NullProgressMonitor());
			Field field = LocalMetadataRepository.class.getDeclaredField("disableSave");
			field.setAccessible(true);
			boolean disableSave = field.getBoolean(repo);
			assertFalse("1.0", disableSave);

		} catch (Exception e) {
			fail("Test failed", e);
		}
	}

	/*
	 * This test ensure that the simple metadata repository disables the
	 * save flag during the batch process
	 */
	public void testDisableSaveFlagDuringExecutionSimple() {
		try {
			repositoryFile = getTempFolder();
			repositoryURI = repositoryFile.toURI();
			Map<String, String> properties = new HashMap<>();
			final LocalMetadataRepository repo = (LocalMetadataRepository) getMetadataRepositoryManager().createRepository(repositoryURI, "My Repo", IMetadataRepositoryManager.TYPE_SIMPLE_REPOSITORY, properties);
			repo.executeBatch(monitor -> {
				Field field;
				try {
					field = LocalMetadataRepository.class.getDeclaredField("disableSave");
					field.setAccessible(true);
					boolean disableSave = field.getBoolean(repo);
					assertTrue("1.0", disableSave);
				} catch (SecurityException e1) {
					fail("1.1" + e1.getMessage());
				} catch (NoSuchFieldException e2) {
					// TODO Auto-generated catch block
					fail("1.2" + e2.getMessage());
				} catch (IllegalArgumentException e3) {
					fail("1.2" + e3.getMessage());
				} catch (IllegalAccessException e4) {
					fail("1.2" + e4.getMessage());
				}
			}, new NullProgressMonitor());

		} catch (Exception e) {
			fail("Test failed", e);
		}
	}
}
