/*******************************************************************************
 *  Copyright (c) 2012, 2017 Wind River 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:
 *     Wind River - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.p2.tests.engine;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.internal.registry.ExtensionRegistry;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.spi.RegistryContributor;
import org.eclipse.equinox.internal.p2.artifact.repository.MirrorEvent;
import org.eclipse.equinox.internal.p2.engine.*;
import org.eclipse.equinox.internal.p2.metadata.TouchpointData;
import org.eclipse.equinox.internal.p2.metadata.TouchpointInstruction;
import org.eclipse.equinox.internal.p2.touchpoint.eclipse.Util;
import org.eclipse.equinox.internal.p2.touchpoint.eclipse.actions.ActionConstants;
import org.eclipse.equinox.internal.p2.touchpoint.eclipse.actions.RemoveRepositoryAction;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.ProvisioningListener;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.engine.*;
import org.eclipse.equinox.p2.engine.spi.ProvisioningAction;
import org.eclipse.equinox.p2.metadata.*;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.equinox.p2.repository.IRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository;
import org.eclipse.equinox.p2.repository.artifact.IArtifactRequest;
import org.eclipse.equinox.p2.tests.AbstractProvisioningTest;
import org.eclipse.equinox.p2.tests.TestActivator;
import org.junit.*;

public class ProvisioningEventTest extends AbstractProvisioningTest {
	private IEngine engine;
	private File testProvisioning;

	@Override
	@Before
	public void setUp() throws Exception {
		engine = getEngine();
		testProvisioning = new File(System.getProperty("java.io.tmpdir"), "testProvisioning");
		delete(testProvisioning);
		testProvisioning.mkdir();
	}

	@Override
	@After
	public void tearDown() throws Exception {
		engine = null;
		delete(testProvisioning);
	}

	@Test
	public void testCollectEvent() throws ProvisionException, OperationCanceledException, InterruptedException {
		class ProvTestListener implements ProvisioningListener {
			int requestsNumber = 0;
			boolean called = false;
			boolean mirrorEevent = false;
			CountDownLatch latch = new CountDownLatch(1);

			@Override
			public void notify(EventObject o) {
				if (o instanceof CollectEvent) {
					if (((CollectEvent) o).getType() == CollectEvent.TYPE_OVERALL_START && ((CollectEvent) o).getRepository() == null) {
						called = true;
						IArtifactRequest[] requests = ((CollectEvent) o).getDownloadRequests();
						requestsNumber = requests.length;
					}
				} else if (o instanceof MirrorEvent) {
					mirrorEevent = true;
					System.out.println(((MirrorEvent) o).getDownloadStatus());
				} else if (o instanceof CommitOperationEvent || o instanceof RollbackOperationEvent)
					latch.countDown();
			}
		}
		final ProvTestListener listener = new ProvTestListener();
		getEventBus().addListener(listener);
		try {
			URI repoLoc = getTestData("Load test data.", "/testData/testRepos/updateSite").toURI();
			IProfile profile = createProfile("test");
			// clean possible cached artifacts
			IArtifactRepository bundlePool = Util.getBundlePoolRepository(getAgent(), profile);
			bundlePool.removeAll(new NullProgressMonitor());
			ProvisioningContext context = new ProvisioningContext(getAgent());
			context.setArtifactRepositories(new URI[] {repoLoc});
			context.setMetadataRepositories(new URI[] {repoLoc});
			IProvisioningPlan plan = engine.createPlan(profile, context);
			IQueryResult<IInstallableUnit> allIUs = getMetadataRepositoryManager().loadRepository(repoLoc, null).query(QueryUtil.ALL_UNITS, null);
			for (IInstallableUnit iu : allIUs.toSet()) {
				plan.addInstallableUnit(iu);
			}
			IStatus status = engine.perform(plan, new NullProgressMonitor());
			assertTrue("Provisioning was failed.", status.isOK());
			//			// make sure the listener handles all event already that are dispatched asynchronously
			listener.latch.await(10, TimeUnit.SECONDS);
			assertTrue("Collect event wasn't dispatched.", listener.called);
			assertEquals("Collect event didn't report expected artifacts to be downloaded.", 19, listener.requestsNumber);
			assertTrue("Mirror event wasn't dispatched.", listener.mirrorEevent);
		} finally {
			getEventBus().removeListener(listener);
		}
	}

	@Test
	public void testPhaseEvent() throws ProvisionException, OperationCanceledException, InterruptedException {
		final String[] phaseSets = new String[] {PhaseSetFactory.PHASE_COLLECT, PhaseSetFactory.PHASE_CHECK_TRUST, PhaseSetFactory.PHASE_INSTALL, PhaseSetFactory.PHASE_CONFIGURE};

		class ProvTestListener implements ProvisioningListener {
			String publishUnWantedPhaseEvent = null;
			int publishUnWantedPhaseType = 0;
			List<String> phaseStartEventToBePublised = new ArrayList<>(Arrays.asList(phaseSets));
			List<String> phaseEndEventToBePublised = new ArrayList<>(Arrays.asList(phaseSets));
			CountDownLatch latch = new CountDownLatch(1);

			@Override
			public void notify(EventObject o) {
				if (o instanceof PhaseEvent) {
					PhaseEvent event = (PhaseEvent) o;
					if (event.getType() == PhaseEvent.TYPE_START) {
						if (!phaseStartEventToBePublised.remove(event.getPhaseId()))
							publishUnWantedPhaseEvent = event.getPhaseId();
					} else if (event.getType() == PhaseEvent.TYPE_END) {
						if (!phaseEndEventToBePublised.remove(event.getPhaseId()))
							publishUnWantedPhaseEvent = event.getPhaseId();
					} else
						publishUnWantedPhaseType = event.getType();
				} else if (o instanceof CommitOperationEvent || o instanceof RollbackOperationEvent)
					latch.countDown();
			}
		}
		final ProvTestListener listener = new ProvTestListener();
		getEventBus().addListener(listener);
		try {
			URI repoLoc = getTestData("Load test data.", "/testData/testRepos/updateSite").toURI();
			IProfile profile = createProfile("test");
			ProvisioningContext context = new ProvisioningContext(getAgent());
			context.setArtifactRepositories(new URI[] {repoLoc});
			context.setMetadataRepositories(new URI[] {repoLoc});
			IProvisioningPlan plan = engine.createPlan(profile, context);
			IQueryResult<IInstallableUnit> allIUs = getMetadataRepositoryManager().loadRepository(repoLoc, null).query(QueryUtil.ALL_UNITS, null);
			for (IInstallableUnit iu : allIUs.toSet()) {
				plan.addInstallableUnit(iu);
			}
			IStatus status = engine.perform(plan, PhaseSetFactory.createPhaseSetIncluding(phaseSets), new NullProgressMonitor());
			assertTrue("Provisioning was failed.", status.isOK());
			// make sure the listener handles all event already that are dispatched asynchronously
			listener.latch.await(10, TimeUnit.SECONDS);
			assertNull("Published phase event with unwanted phase id.", listener.publishUnWantedPhaseEvent);
			assertEquals("Published unwanted type of phase event.", 0, listener.publishUnWantedPhaseType);
			assertEquals("Expected Phase start event is not published.", new ArrayList<>(0), listener.phaseStartEventToBePublised);
			assertEquals("Expected Phase end event is not published.", new ArrayList<>(0), listener.phaseEndEventToBePublised);
		} finally {
			getEventBus().removeListener(listener);
		}
	}

	@Test
	public void testConfigureUnConfigureEvent() throws InterruptedException {
		final String iuId = "test";
		class ProvTestListener implements ProvisioningListener {
			int preConfigureEvent = 0;
			int postConfigureEvent = 0;
			int preUnConfigureEvent = 0;
			int postUnConfigureEvent = 0;
			CountDownLatch latch = new CountDownLatch(2);

			@Override
			public void notify(EventObject o) {
				if (o instanceof InstallableUnitEvent) {
					InstallableUnitEvent event = (InstallableUnitEvent) o;
					if (event.getPhase().equals(PhaseSetFactory.PHASE_CONFIGURE) && event.isConfigure() && event.getInstallableUnit().getId().equals(iuId)) {
						if (event.isPre())
							preConfigureEvent++;
						else if (event.isPost())
							postConfigureEvent++;
					} else if (event.getPhase().equals(PhaseSetFactory.PHASE_UNCONFIGURE) && event.isUnConfigure() && event.getInstallableUnit().getId().equals(iuId)) {
						if (event.isPre())
							preUnConfigureEvent++;
						else if (event.isPost())
							postUnConfigureEvent++;
					}
				} else if (o instanceof CommitOperationEvent || o instanceof RollbackOperationEvent)
					latch.countDown();
			}
		}
		final ProvTestListener listener = new ProvTestListener();
		getEventBus().addListener(listener);

		try {
			IProfile profile = createProfile("testConfigureEvent");
			IProvisioningPlan plan = engine.createPlan(profile, null);

			final String testLocation = "http://download.eclipse.org/releases/juno";
			// remove the existing location in case it has
			Map<String, Object> args = new HashMap<>();
			args.put(ActionConstants.PARM_AGENT, getAgent());
			args.put("location", testLocation);
			args.put("type", Integer.toString(IRepository.TYPE_ARTIFACT));
			args.put("enabled", "true");
			new RemoveRepositoryAction().execute(args);

			Map<String, ITouchpointInstruction> data = new HashMap<>();
			Map<String, String> parameters = new HashMap<>();
			parameters.put("location", testLocation);
			parameters.put("type", Integer.toString(IRepository.TYPE_ARTIFACT));
			parameters.put("name", "Juno");
			parameters.put("enabled", "true");
			data.put(PhaseSetFactory.PHASE_CONFIGURE, new TouchpointInstruction(TouchpointInstruction.encodeAction("addRepository", parameters), null));
			IInstallableUnit testIU = createResolvedIU(createEclipseIU(iuId, Version.create("1.0.0"), new IRequirement[0], new TouchpointData(data)));
			plan.addInstallableUnit(testIU);
			IStatus result = engine.perform(plan, PhaseSetFactory.createDefaultPhaseSet(), new NullProgressMonitor());
			assertTrue("0.2", result.isOK());
			Set<IInstallableUnit> installedIUs = profile.available(QueryUtil.ALL_UNITS, null).toUnmodifiableSet();
			assertEquals("0.3", 1, installedIUs.size());
			plan = engine.createPlan(profile, null);
			plan.removeInstallableUnit(testIU);
			result = engine.perform(plan, PhaseSetFactory.createDefaultPhaseSet(), new NullProgressMonitor());
			assertTrue("0.4", result.isOK());
			// make sure the listener handles all event already that are dispatched asynchronously
			listener.latch.await(10, TimeUnit.SECONDS);
			assertEquals("0.5", 1, listener.preConfigureEvent);
			assertEquals("0.6", 1, listener.postConfigureEvent);
			assertEquals("0.7", 1, listener.preUnConfigureEvent);
			assertEquals("0.8", 1, listener.postUnConfigureEvent);
		} finally {
			getEventBus().removeListener(listener);
		}
	}

	public static class AlwaysFail extends ProvisioningAction {

		@Override
		public IStatus execute(Map<String, Object> parameters) {
			int a = 1;
			if (a == 1)
				throw new NullPointerException("no reason");
			return null;
		}

		@Override
		public IStatus undo(Map<String, Object> parameters) {
			return null;
		}

	}

	@Test
	public void testConfigureUndoEvent() throws InterruptedException {
		final String iuId = "test";
		final String failureIU = "alwaysFail";
		class ProvTestListener implements ProvisioningListener {
			int preConfigureEvent = 0;
			int postConfigureEvent = 0;
			int preUnConfigureEventForUndo = 0;
			int postUnConfigureEventForUndo = 0;
			CountDownLatch latch = new CountDownLatch(1);

			@Override
			public void notify(EventObject o) {
				if (o instanceof InstallableUnitEvent) {
					InstallableUnitEvent event = (InstallableUnitEvent) o;
					if (event.getPhase().equals(PhaseSetFactory.PHASE_CONFIGURE) && event.getInstallableUnit().getId().equals(iuId)) {
						if (event.isConfigure() && event.isPre())
							preConfigureEvent++;
						else if (event.isConfigure() && event.isPost())
							postConfigureEvent++;
						else if (event.isUnConfigure() && event.isPre())
							preUnConfigureEventForUndo++;
						else if (event.isUnConfigure() && event.isPost())
							postUnConfigureEventForUndo++;
					} else if (event.getPhase().equals(PhaseSetFactory.PHASE_CONFIGURE) && event.getInstallableUnit().getId().equals(failureIU)) {
						if (event.isConfigure() && event.isPre()) {
							preConfigureEvent++;
						} else if (event.isConfigure() && event.isPost())
							postConfigureEvent++;
						else if (event.isUnConfigure() && event.isPre())
							preUnConfigureEventForUndo++;
						else if (event.isUnConfigure() && event.isPost())
							postUnConfigureEventForUndo++;
					}
				} else if (o instanceof CommitOperationEvent || o instanceof RollbackOperationEvent)
					latch.countDown();
			}
		}
		final ProvTestListener listener = new ProvTestListener();
		getEventBus().addListener(listener);

		try {
			final String customTouchPoint = "<extension point=\"org.eclipse.equinox.p2.engine.actions\"> <action class=\"org.eclipse.equinox.p2.tests.engine.ProvisioningEventTest.AlwaysFail\" name=\"alwaysFail\" touchpointType=\"org.eclipse.equinox.p2.osgi\" touchpointVersion=\"1.0.0\" version=\"1.0.0\"></action></extension>";
			ByteArrayInputStream input = new ByteArrayInputStream(customTouchPoint.getBytes());
			IExtensionRegistry registry = Platform.getExtensionRegistry();
			registry.addContribution(input, new RegistryContributor(TestActivator.PI_PROV_TESTS, "p2 tests", null, null), false, "Always Fail TouchPoint Action", null, ((ExtensionRegistry) registry).getTemporaryUserToken());

			IProfile profile = createProfile("testConfigureEvent");
			IProvisioningPlan plan = engine.createPlan(profile, null);
			Map<String, ITouchpointInstruction> data = new HashMap<>();
			data.put(PhaseSetFactory.PHASE_CONFIGURE, MetadataFactory.createTouchpointInstruction("instructionparsertest.goodAction()", null));
			IInstallableUnit testIU = createResolvedIU(createIU(iuId, Version.create("1.0.0"), null, new IRequirement[0], BUNDLE_CAPABILITY, NO_PROPERTIES, ITouchpointType.NONE, new TouchpointData(data), false));
			plan.addInstallableUnit(testIU);
			data = new HashMap<>(1);
			data.put(PhaseSetFactory.PHASE_CONFIGURE, new TouchpointInstruction("alwaysFail();", null));
			plan.addInstallableUnit(createResolvedIU(createEclipseIU(failureIU, Version.create("1.0.0"), new IRequirement[0], new TouchpointData(data))));
			IStatus result = engine.perform(plan, PhaseSetFactory.createDefaultPhaseSet(), new NullProgressMonitor());
			assertFalse(result.isOK());
			// make sure the listener handles all event already that are dispatched asynchronously
			listener.latch.await(10, TimeUnit.SECONDS);
			assertEquals("0.5", 2, listener.preConfigureEvent);
			assertEquals("0.6", 1, listener.postConfigureEvent);
			assertEquals("0.7", 1, listener.preUnConfigureEventForUndo);
			assertEquals("0.8", 2, listener.postUnConfigureEventForUndo);
		} finally {
			getEventBus().removeListener(listener);
		}
	}
}