/*******************************************************************************
 *  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.net.URI;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest;
import org.eclipse.equinox.internal.p2.engine.BeginOperationEvent;
import org.eclipse.equinox.internal.p2.engine.CommitOperationEvent;
import org.eclipse.equinox.internal.p2.engine.EngineSession;
import org.eclipse.equinox.internal.p2.engine.InstallableUnitOperand;
import org.eclipse.equinox.internal.p2.engine.Phase;
import org.eclipse.equinox.internal.p2.engine.PhaseEvent;
import org.eclipse.equinox.internal.p2.engine.PhaseSet;
import org.eclipse.equinox.internal.p2.engine.ProfileEvent;
import org.eclipse.equinox.internal.p2.engine.RollbackOperationEvent;
import org.eclipse.equinox.internal.p2.repository.DownloadProgressEvent;
import org.eclipse.equinox.internal.p2.touchpoint.natives.Util;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.ProvisioningListener;
import org.eclipse.equinox.internal.provisional.p2.repository.RepositoryEvent;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.engine.IEngine;
import org.eclipse.equinox.p2.engine.IPhaseSet;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProvisioningPlan;
import org.eclipse.equinox.p2.engine.PhaseSetFactory;
import org.eclipse.equinox.p2.engine.ProvisioningContext;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.query.IQuery;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
import org.eclipse.equinox.p2.tests.TestActivator;
import org.junit.Test;

/**
 * Simple test of the engine API.
 */
public class PhaseSetTest extends AbstractProvisioningTest {
	PauseJob pause = null;

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

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

	public void testNullPhases() {
		try {
			new PhaseSet(null) {
				// empty PhaseSet
			};
		} catch (IllegalArgumentException exepcted) {
			return;
		}
		fail();
	}

	public void testNoTrustCheck() {
		IPhaseSet set1 = PhaseSetFactory.createDefaultPhaseSet();
		IPhaseSet set2 = PhaseSetFactory.createDefaultPhaseSetExcluding(new String[] {PhaseSetFactory.PHASE_CHECK_TRUST});
		assertTrue("1.0", !set1.equals(set2));
	}

	public void testEmptyPhases() {
		IProfile profile = createProfile("PhaseSetTest");
		PhaseSet phaseSet = new PhaseSet(new Phase[] {}) {
			// empty PhaseSet
		};
		InstallableUnitOperand op = new InstallableUnitOperand(createResolvedIU(createIU("iu")), null);
		InstallableUnitOperand[] operands = new InstallableUnitOperand[] {op};

		ProvisioningContext context = new ProvisioningContext(getAgent());
		IStatus result = phaseSet.perform(new EngineSession(null, profile, context), operands, new NullProgressMonitor());
		assertTrue(result.isOK());
	}

	@Test
	public void testPauseNotRunningPhaseSet() {
		PhaseSet set = (PhaseSet) PhaseSetFactory.createDefaultPhaseSet();
		assertFalse("Can pause not running phaseset.", set.pause());
	}

	@Test
	public void testResumeNotPausedPhaseSet() {
		PhaseSet set = (PhaseSet) PhaseSetFactory.createDefaultPhaseSet();
		assertFalse("Can resume not phaused phaseset.", set.resume());
	}

	abstract class PauseJob extends Job {
		public PauseJob(String name) {
			super(name);
		}

		private boolean isPaused = false;
		Job resume = null;

		public boolean isPaused() {
			return isPaused;
		}

		public void setPause(boolean paused) {
			isPaused = paused;
		}
	}

	@Test
	public void testPauseAndResume() throws ProvisionException, OperationCanceledException, InterruptedException {
		URI repoLoc = getTestData("Load test data.", "/testData/pausefeature").toURI();
		final PhaseSet phaseSet = (PhaseSet) PhaseSetFactory.createDefaultPhaseSet();
		pause = new PauseJob("pause") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				if (!phaseSet.pause())
					return new Status(IStatus.ERROR, TestActivator.PI_PROV_TESTS, "pause() failed.");
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				setPause(true);
				resume = new Job("resume") {
					@Override
					protected IStatus run(IProgressMonitor monitor1) {
						pause.setPause(false);
						if (!phaseSet.resume())
							return new Status(IStatus.ERROR, TestActivator.PI_PROV_TESTS, "resume() failed.");
						return Status.OK_STATUS;
					}
				};
				resume.schedule(10000);
				return Status.OK_STATUS;
			}
		};
		basicTest(repoLoc, phaseSet, pause, QueryUtil.createIUQuery("org.eclipse.equinox.launcher"), IStatus.OK, null);
		assertTrue("Pause job is failed.", pause.getResult().isOK());
		pause.resume.join();
		assertTrue("Resume job is failed.", pause.resume.getResult().isOK());
	}

	private void basicTest(URI repoURI, PhaseSet phaseSet, final PauseJob pauseJob, IQuery<IInstallableUnit> query, int expectedCode, IProgressMonitor monitor) throws ProvisionException, InterruptedException {
		class ProvTestListener implements ProvisioningListener {
			boolean hasProvisioningEventAfterPaused = false;
			CountDownLatch latch = new CountDownLatch(1);
			boolean canStart = false;

			@Override
			public void notify(EventObject o) {
				if (o instanceof BeginOperationEvent) {
					canStart = true;
				}
				if (o instanceof RepositoryEvent || o instanceof ProfileEvent)
					return;
				if (canStart && o instanceof DownloadProgressEvent) {
					// make sure to pause downloading after it has started
					pauseJob.schedule();
					canStart = false;
					return;
				}
				if (o instanceof CommitOperationEvent || o instanceof RollbackOperationEvent) {
					latch.countDown();
					pauseJob.cancel();
					return;
				}
				if (pauseJob.isPaused() && !(o instanceof PhaseEvent)) {
					hasProvisioningEventAfterPaused = true;
				}
			}
		}

		ProvTestListener listener = new ProvTestListener();
		getEventBus().addListener(listener);
		try {
			getMetadataRepositoryManager().loadRepository(repoURI, null);
			getArtifactRepositoryManager().loadRepository(repoURI, null);
			doProvisioning(repoURI, phaseSet, query, expectedCode, monitor);
			// make sure the listener handles all event already that are dispatched asynchronously
			listener.latch.await(15, TimeUnit.SECONDS);
			assertFalse("Engine still do provisioning after pausing.", listener.hasProvisioningEventAfterPaused);
			pauseJob.join();
		} finally {
			getEventBus().removeListener(listener);
		}
	}

	private void doProvisioning(URI repoLoc, final PhaseSet phaseSet, IQuery<IInstallableUnit> query, int expectedResult, IProgressMonitor monitor) throws ProvisionException {
		File testFolder = new File(System.getProperty("java.io.tmpdir"), "testProvisioning");
		delete(testFolder);
		testFolder.mkdir();
		final String profileId = "test";
		try {
			ProvisioningContext context = new ProvisioningContext(getAgent());
			context.setArtifactRepositories(new URI[] {repoLoc});
			context.setMetadataRepositories(new URI[] {repoLoc});
			IEngine engine = getEngine();
			// restrict the installation to 'linux & gtk & x86' to match the test repo
			Map<String, String> props = new HashMap<>();
			props.put(IProfile.PROP_ENVIRONMENTS, "osgi.ws=gtk,osgi.arch=x86,osgi.os=linux");
			props.put(IProfile.PROP_INSTALL_FOLDER, testFolder.getAbsolutePath());
			IProfile profile = createProfile(profileId, props);
			// clean cached artifacts
			IArtifactRepository artifactRepo = org.eclipse.equinox.internal.p2.touchpoint.eclipse.Util.getBundlePoolRepository(getAgent(), profile);
			artifactRepo.removeAll(new NullProgressMonitor());
			ProfileChangeRequest request = ProfileChangeRequest.createByProfileId(getAgent(), profile.getProfileId());
			IQueryResult<IInstallableUnit> toBeInstalledIUs = getMetadataRepositoryManager().loadRepository(repoLoc, null).query(query, null);
			assertFalse("Test case has problem to find IU to be installed.", toBeInstalledIUs.isEmpty());
			request.addAll(toBeInstalledIUs.toSet());
			IProvisioningPlan plan = getPlanner(getAgent()).getProvisioningPlan(request, context, null);
			assertTrue("Provisioning plan can't be resolved.", plan.getStatus().isOK());
			IStatus status = engine.perform(plan, phaseSet, monitor);
			assertEquals("The reture code of provisioning is not expected.", expectedResult, status.getSeverity());
		} finally {
			delete(testFolder);
			getProfileRegistry().removeProfile(profileId);
			Util.getDownloadCacheRepo(getAgent()).removeAll(new NullProgressMonitor());
		}
	}

	@Test
	public void testPauseAndResumeMoreThanOnce() throws ProvisionException, InterruptedException {
		//		URI repoLoc = URI.create("http://download.eclipse.org/releases/indigo");
		URI repoLoc = getTestData("Load test data.", "/testData/pausefeature").toURI();
		final PhaseSet phaseSet = (PhaseSet) PhaseSetFactory.createDefaultPhaseSet();
		final int threhold = 3;
		class ResumeJob extends Job {

			private PauseJob pauseJob;
			private int count = 0;

			public ResumeJob(String name, PauseJob pauseJob) {
				super(name);
				this.pauseJob = pauseJob;
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				pauseJob.setPause(false);
				if (!phaseSet.resume())
					return new Status(IStatus.INFO, TestActivator.PI_PROV_TESTS, "resume() failed.");
				if (count++ < threhold)
					pauseJob.schedule(10000);
				return Status.OK_STATUS;
			}
		}
		pause = new PauseJob("pause") {
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				if (!phaseSet.pause())
					return new Status(IStatus.INFO, TestActivator.PI_PROV_TESTS, "pause() failed.");
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				setPause(true);
				if (resume == null)
					resume = new ResumeJob("resume", this);
				resume.schedule(10000);
				return Status.OK_STATUS;
			}
		};

		basicTest(repoLoc, phaseSet, pause, QueryUtil.createLatestQuery(QueryUtil.createIUQuery("org.eclipse.equinox.executable.feature.group")), IStatus.OK, null);
	}

	@Test
	public void testCancelPausedProvisioing() throws ProvisionException, InterruptedException {
		URI repoLoc = getTestData("Load test data.", "/testData/pausefeature").toURI();
		final PhaseSet phaseSet = (PhaseSet) PhaseSetFactory.createDefaultPhaseSet();
		class ProvListener implements ProvisioningListener {
			boolean hasDownloadEvent = false;

			@Override
			public void notify(EventObject o) {
				if (o instanceof DownloadProgressEvent)
					hasDownloadEvent = true;
			}

		}
		final ProvListener listener = new ProvListener();
		getEventBus().addListener(listener);
		try {

			pause = new PauseJob("pause") {
				@Override
				protected IStatus run(IProgressMonitor monitor) {
					while (!listener.hasDownloadEvent) {
						try {
							Thread.sleep(10);
						} catch (InterruptedException e1) {
							e1.printStackTrace();
						}
					}
					if (!phaseSet.pause())
						return new Status(IStatus.ERROR, TestActivator.PI_PROV_TESTS, "pause() failed.");
					try {
						Thread.sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// wait seconds
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						//
					}
					setPause(true);
					return Status.OK_STATUS;
				}
			};

			basicTest(repoLoc, phaseSet, pause, QueryUtil.createLatestQuery(QueryUtil.createIUQuery("org.eclipse.equinox.executable.feature.group")), IStatus.CANCEL, new NullProgressMonitor() {
				@Override
				public boolean isCanceled() {
					return pause.isPaused();
				}
			});
		} finally {
			getEventBus().removeListener(listener);
		}
	}
}
