/*******************************************************************************
 * Copyright (c) 2000, 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.ui.tests.api;

import static org.junit.Assert.assertArrayEquals;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.ui.IAggregateWorkingSet;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.XMLMemento;
import org.eclipse.ui.internal.AbstractWorkingSet;
import org.eclipse.ui.internal.AbstractWorkingSetManager;
import org.eclipse.ui.internal.AggregateWorkingSet;
import org.eclipse.ui.internal.IWorkbenchConstants;
import org.eclipse.ui.tests.harness.util.ArrayUtil;
import org.eclipse.ui.tests.harness.util.UITestCase;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class IAggregateWorkingSetTest extends UITestCase {

	static final String WORKING_SET_NAME = "testws";
	static final String AGGREGATE_WORKING_SET_NAME_ = "testaggregatews";
	static final String WSET_PAGE_ID="org.eclipse.ui.resourceWorkingSetPage";
	IWorkspace fWorkspace;

	IWorkingSet[] components;
	List<IWorkingSet> backup;
	IAggregateWorkingSet fWorkingSet;

	public IAggregateWorkingSetTest() {
		super(IAggregateWorkingSetTest.class.getSimpleName());
	}

	@Override
	protected void doSetUp() throws Exception {
		super.doSetUp();
		IWorkingSetManager workingSetManager = fWorkbench
		.getWorkingSetManager();
		backup = Arrays.asList(workingSetManager.getAllWorkingSets());

		fWorkspace = ResourcesPlugin.getWorkspace();
		components = new IWorkingSet[4];
		for (int i = 0; i < 4; i++) {
			components[i] = workingSetManager.createWorkingSet(WORKING_SET_NAME
					+ i, new IAdaptable[] {});
			workingSetManager.addWorkingSet(components[i]);
		}
		fWorkingSet = (IAggregateWorkingSet) workingSetManager
		.createAggregateWorkingSet(AGGREGATE_WORKING_SET_NAME_,
				AGGREGATE_WORKING_SET_NAME_, components);

		workingSetManager.addWorkingSet(fWorkingSet);
	}
	@Override
	protected void doTearDown() throws Exception {
		IWorkingSetManager workingSetManager = fWorkbench.getWorkingSetManager();
		workingSetManager.removeWorkingSet(fWorkingSet);
		for (IWorkingSet component : components) {
			workingSetManager.removeWorkingSet(component);
		}
		IWorkingSet[] sets = workingSetManager.getAllWorkingSets();
		for (IWorkingSet wset : sets) {
			if (!backup.contains(wset)) {
				workingSetManager.removeWorkingSet(wset);
			}
		}
		super.doTearDown();
	}

	@Test
	public void testSaveWSet() throws Throwable {
		//<possible client code>
		IWorkingSetManager workingSetManager = fWorkbench
		.getWorkingSetManager();
		IWorkingSet set=workingSetManager.getWorkingSet(AGGREGATE_WORKING_SET_NAME_);
		if(set.isAggregateWorkingSet()){
			IWorkingSet[] sets=((IAggregateWorkingSet)set).getComponents();
			if(sets.length>=1){
				sets[0]=null; //client fails to pay enough attention to specs or unknowingly does this
			}
		}
		//</possible client code>
		//error makes it look like it comes from workingsets api, with no clue about the actual culprit
		IMemento memento=XMLMemento.createWriteRoot(IWorkbenchConstants.TAG_WORKING_SET);
		set.saveState(memento);
	}
	@Test
	public void testGetElemets() throws Throwable {
		//<possible client code>
		IWorkingSetManager workingSetManager = fWorkbench
		.getWorkingSetManager();
		IWorkingSet set=workingSetManager.getWorkingSet(AGGREGATE_WORKING_SET_NAME_);
		if(set.isAggregateWorkingSet()){
			IWorkingSet[] sets=((IAggregateWorkingSet)set).getComponents();
			if(sets.length>1){
				//code 2 fails to pay enough attention to specs or unknowingly does this
				sets[0]=workingSetManager.createWorkingSet(WORKING_SET_NAME, new IAdaptable[] { fWorkspace.getRoot() });
				//code 1 part  removes a workingset
				workingSetManager.removeWorkingSet(sets[1]);
			}
		}
		//</possible client code>

		//unexpected
		assertTrue(ArrayUtil.equals(
				new IAdaptable[] {},
				fWorkingSet.getElements()));
	}

	/**
	 * Core of the problem: while Eclipse is running, name collisions among working sets
	 * don't matter. However, on save and restart names will be used to identify working
	 * sets, which could possibly lead to cycles in aggregate working sets.
	 *
	 * Bottom line: if there are multiple aggregate working sets with the same name, expect
	 * trouble on restart.
	 *
	 * To create a cycle we have to be creative:
	 * - create an aggregate1 with an ID = "testCycle"
	 * - create an aggregate2 with an ID = "testCycle" containing aggregate1
	 * - save it into IMemento
	 *
	 * Now the IMememnto creates a self reference:
	 *
	 * <pre>
	 * &lt;workingSet name="testCycle" label="testCycle" aggregate="true"&gt;
	 * 	&lt;workingSet IMemento.internal.id="testCycle" /&gt;
	 * &lt;/workingSet&gt;
	 * </pre>
	 *
	 * All we have to do to emulate stack overflow is to create a working set based on this IMemento.
	 *
	 * @throws Throwable
	 */
	@Test
	public void testWorkingSetCycle() throws Throwable {
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();

		// create an IMemento with a cycle in it
		IAggregateWorkingSet aggregate = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle","testCycle", new IWorkingSet[0]);

		IAggregateWorkingSet aggregate2 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle","testCycle", new IWorkingSet[] {aggregate});

		IMemento memento=XMLMemento.createWriteRoot(IWorkbenchConstants.TAG_WORKING_SET);
		aggregate2.saveState(memento);

		// load the IMemento
		IAggregateWorkingSet aggregateReloaded = null;
		try {
			aggregateReloaded = (IAggregateWorkingSet) manager.createWorkingSet(memento);
			manager.addWorkingSet(aggregateReloaded);
			aggregateReloaded.getComponents();
		} catch (StackOverflowError e) {
			e.printStackTrace();
			fail("Stack overflow for self-referenced aggregate working set", e);
		} finally {
			if (aggregateReloaded != null) {
				manager.removeWorkingSet(aggregateReloaded);
			}
		}
	}

	/**
	 * Tests cleanup of the cycle from an aggregate working set.
	 * @throws Throwable
	 */
	@Test
	public void testCycleCleanup() throws Throwable {
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();

		// create an IMemento with a cycle in it: { good, good, cycle, good, good }
		IAggregateWorkingSet aggregateSub0 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle0","testCycle0", new IWorkingSet[0]);

		IAggregateWorkingSet aggregateSub1 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle1","testCycle1", new IWorkingSet[0]);

		IAggregateWorkingSet aggregateSub2 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle","testCycle", new IWorkingSet[0]); // cycle

		IAggregateWorkingSet aggregateSub3 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle3","testCycle3", new IWorkingSet[0]);

		IAggregateWorkingSet aggregateSub4 = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle4","testCycle4", new IWorkingSet[0]);


		IAggregateWorkingSet aggregate = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet("testCycle","testCycle", new IWorkingSet[] {aggregateSub0,
				aggregateSub1, aggregateSub2, aggregateSub3, aggregateSub4});

		manager.addWorkingSet(aggregateSub0);
		manager.addWorkingSet(aggregateSub1);
		manager.addWorkingSet(aggregateSub3);
		manager.addWorkingSet(aggregateSub4);

		IMemento memento=XMLMemento.createWriteRoot(IWorkbenchConstants.TAG_WORKING_SET);
		aggregate.saveState(memento);

		// load the IMemento
		IAggregateWorkingSet aggregateReloaded = null;
		try {
			aggregateReloaded = (IAggregateWorkingSet) manager.createWorkingSet(memento);
			manager.addWorkingSet(aggregateReloaded);
			IWorkingSet[] aggregates = aggregateReloaded.getComponents();
			assertNotNull(aggregates);
			assertEquals(4, aggregates.length);
			for (IWorkingSet aggregate2 : aggregates) {
				assertFalse("testCycle".equals(aggregate2.getName()));
			}
		} catch (StackOverflowError e) {
			e.printStackTrace();
			fail("Stack overflow for self-referenced aggregate working set", e);
		} finally {
			if (aggregateReloaded != null) {
				manager.removeWorkingSet(aggregateReloaded);
			}
		}
	}

	/*
	 * Test related to Bug 217955.The initial fix made changes that caused
	 * save/restore to fail due to early restore and forward reference in
	 * memento of aggregates
	 */
	/* TODO test must be enabled after bug 479217 is fixed */
	@Test
	@Ignore("Bug 479217")
	public void XXXtestWorkingSetSaveRestoreAggregates() throws Throwable {
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();
		String nameA = "A";
		String nameB = "B";
		String nameC = "C";

		IWorkingSet wSetA = manager
				.createWorkingSet(nameA, new IAdaptable[] {});
		manager.addWorkingSet(wSetA);

		IAggregateWorkingSet wSetB = (IAggregateWorkingSet) manager
		.createAggregateWorkingSet(nameB, nameB, new IWorkingSet[] {});
		manager.addWorkingSet(wSetB);

		IAggregateWorkingSet wSetC = (IAggregateWorkingSet) manager
				.createAggregateWorkingSet(nameC, nameC, new IWorkingSet[0]);
		manager.addWorkingSet(wSetC);

		try {
			assertEquals("Failed to add workingset" + nameA, wSetA, manager
					.getWorkingSet(nameA));

			assertEquals("Failed to add workingset" + nameC, wSetC, manager
					.getWorkingSet(nameC));

			assertEquals("Failed to add workingset" + nameB, wSetB, manager
					.getWorkingSet(nameB));

			assertEquals(0, wSetB.getComponents().length);

			invokeMethod(AggregateWorkingSet.class, "setComponents", wSetB,
					new Object[] { new IWorkingSet[] {
							wSetA, wSetC } },
					new Class[] { new IWorkingSet[] {}.getClass() });

			assertArrayEquals(new IWorkingSet[] { wSetA, wSetC }, wSetB.getComponents());

			IMemento workingSets = saveAndRemoveWorkingSets(wSetA, wSetB, wSetC);
			processEvents();
			waitForJobs(500, 3000);

			assertNull(manager.getWorkingSet(nameA));
			assertNull(manager.getWorkingSet(nameB));
			assertNull(manager.getWorkingSet(nameC));

			restoreWorkingSetManager(workingSets);
			processEvents();
			waitForJobs(500, 3000);

			IWorkingSet restoredA = manager.getWorkingSet(nameA);
			assertNotNull("Unable to save/restore correctly", restoredA);

			IAggregateWorkingSet restoredB = (IAggregateWorkingSet) manager.getWorkingSet(nameB);

			IAggregateWorkingSet restoredC = (IAggregateWorkingSet) manager.getWorkingSet(nameC);

			assertNotNull("Unable to save/restore correctly", restoredC);
			assertNotNull("Unable to save/restore correctly", restoredB);

			IWorkingSet[] componenents1 = wSetB.getComponents();
			IWorkingSet[] componenents2 = restoredB.getComponents();

			if (componenents1.length != componenents2.length) {
				assertArrayEquals(nameB + " has lost data in the process of save/restore: " + restoredB,
						wSetB.getComponents(), restoredB.getComponents());
			} else {
				for (int i = 0; i < componenents1.length; i++) {
					if (!componenents1[i].equals(componenents2[i])) {
						assertEquals(nameB + " has lost data in the process of save/restore: " + restoredB,
								componenents1[i].toString(), componenents2[i].toString());
						fail("equals() and toString() do not match for: " + componenents1[i] + " and "
								+ componenents2[i]);
					}
				}
			}

		} finally {
			// restore
			IWorkingSet set = manager.getWorkingSet(nameA);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
			set = manager.getWorkingSet(nameB);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
			set = manager.getWorkingSet(nameC);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
		}
	}

	/* test which passes as long as bug 479217 is not fixed */
	@Test
	public void testWorkingSetSaveNeverRestoresAggregate() throws Throwable {
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();
		String nameA = "A";
		String nameB = "B";
		String nameC = "C";

		IWorkingSet wSetA = manager.createWorkingSet(nameA, new IAdaptable[] {});
		manager.addWorkingSet(wSetA);

		IAggregateWorkingSet wSetB = (IAggregateWorkingSet) manager.createAggregateWorkingSet(nameB, nameB,
				new IWorkingSet[] {});
		manager.addWorkingSet(wSetB);

		IAggregateWorkingSet wSetC = (IAggregateWorkingSet) manager.createAggregateWorkingSet(nameC, nameC,
				new IWorkingSet[0]);
		manager.addWorkingSet(wSetC);

		try {
			assertEquals("Failed to add workingset" + nameA, wSetA, manager.getWorkingSet(nameA));

			assertEquals("Failed to add workingset" + nameC, wSetC, manager.getWorkingSet(nameC));

			assertEquals("Failed to add workingset" + nameB, wSetB, manager.getWorkingSet(nameB));

			assertEquals(0, wSetB.getComponents().length);

			invokeMethod(AggregateWorkingSet.class, "setComponents", wSetB,
					new Object[] { new IWorkingSet[] { wSetA, wSetC } },
					new Class[] { new IWorkingSet[] {}.getClass() });

			assertArrayEquals(new IWorkingSet[] { wSetA, wSetC }, wSetB.getComponents());

			IMemento workingSets = saveAndRemoveWorkingSets(wSetA, wSetB, wSetC);
			processEvents();
			waitForJobs(500, 3000);

			assertNull(manager.getWorkingSet(nameA));
			assertNull(manager.getWorkingSet(nameB));
			assertNull(manager.getWorkingSet(nameC));

			final AtomicReference<String> error = new AtomicReference<>();

			// Exploit the bug 479217 in
			// AbstractWorkingSetManager.restoreWorkingSetState():
			// every client which wants to see components of the working set
			// *before* the restoreWorkingSetState() is done, can silently (!!!)
			// damage the AggregateWorkingSet being restored
			IPropertyChangeListener badListener = event -> {
				if (event.getProperty() != IWorkingSetManager.CHANGE_WORKING_SET_ADD) {
					return;
				}
				// simply resolve the working set before the manager creates
				// another one
				Object ws = event.getNewValue();
				if (!(ws instanceof AggregateWorkingSet)) {
					return;
				}
				AggregateWorkingSet aws = (AggregateWorkingSet) ws;
				IMemento m = readField(AbstractWorkingSet.class, "workingSetMemento", IMemento.class, aws);
				IWorkingSet[] sets = aws.getComponents();
				if (m != null) {
					IMemento[] msets = m.getChildren(IWorkbenchConstants.TAG_WORKING_SET);
					if (msets.length != sets.length) {
						// KABOOM!
						error.set("Working set lost due the bad listener! " + "restored: " + Arrays.toString(sets)
								+ ", expected: " + Arrays.toString(msets));
					}
				} else {
					if (nameB.equals(aws.getName()) && sets.length != 2) {
						// someone was faster
						error.set("Working set lost due the bad listener! " + "restored: " + Arrays.toString(sets));
					}
				}
			};
			try {
				manager.addPropertyChangeListener(badListener);

				restoreWorkingSetManager(workingSets);
				processEvents();

				IWorkingSet restoredA = manager.getWorkingSet(nameA);
				assertNotNull("Unable to save/restore correctly", restoredA);

				IAggregateWorkingSet restoredB = (IAggregateWorkingSet) manager.getWorkingSet(nameB);
				assertNotNull("Unable to save/restore correctly", restoredB);

				IAggregateWorkingSet restoredC = (IAggregateWorkingSet) manager.getWorkingSet(nameC);
				assertNotNull("Unable to save/restore correctly", restoredC);

				IWorkingSet[] componenents1 = wSetB.getComponents();
				IWorkingSet[] componenents2 = restoredB.getComponents();
				assertEquals(2, componenents1.length);
				// this is the bug 479217: we should see 2 elements, and not 1!
				assertEquals(1, componenents2.length);
				// if the bug is fixed, the error must be null
				assertNotNull(error.get());
			} finally {
				manager.removePropertyChangeListener(badListener);
			}

		} finally {
			// restore
			IWorkingSet set = manager.getWorkingSet(nameA);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
			set = manager.getWorkingSet(nameB);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
			set = manager.getWorkingSet(nameC);
			if (set != null) {
				manager.removeWorkingSet(set);
			}
		}
	}

	private IMemento saveAndRemoveWorkingSets(IWorkingSet... sets) {
		IMemento managerMemento = XMLMemento
				.createWriteRoot(IWorkbenchConstants.TAG_WORKING_SET_MANAGER);
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();
		for (IWorkingSet set : sets) {
			if(set.getId()==null){
				//set default id as set by factory
				set.setId(WSET_PAGE_ID);
			}
		}
		invokeMethod(AbstractWorkingSetManager.class, "saveWorkingSetState",
				manager, new Object[] { managerMemento },
				new Class[] { IMemento.class });
		invokeMethod(AbstractWorkingSetManager.class, "saveMruList", manager,
				new Object[] { managerMemento }, new Class[] { IMemento.class });
		for (IWorkingSet set : sets) {
			((AbstractWorkingSet) set).disconnect();
		}
		for (IWorkingSet set : sets) {
			manager.removeWorkingSet(set);
		}
		//manager.dispose(); //not needed, also cause problems
		return managerMemento;
	}

	private void restoreWorkingSetManager(IMemento managerMemento) {
		IWorkingSetManager manager = fWorkbench.getWorkingSetManager();

		invokeMethod(AbstractWorkingSetManager.class, "restoreWorkingSetState",
				manager, new Object[] { managerMemento },
				new Class[] { IMemento.class });
		invokeMethod(AbstractWorkingSetManager.class, "restoreMruList",
				manager, new Object[] { managerMemento },
				new Class[] { IMemento.class });
	}

	private Object invokeMethod(Class<?> clazz, String methodName, Object instance, Object[] args,
			Class<?>[] argsClasses) {
		try {
			Method method = clazz.getDeclaredMethod(methodName, argsClasses);
			method.setAccessible(true);
			return method.invoke(instance, args);
		} catch (Exception e) {
			fail("Failure in invoking " + clazz.getName() + methodName, e);
		}
		return null;
	}

	private <T> T readField(Class<?> clazz, String filedName, Class<T> type, Object instance) {
		try {
			Field field = clazz.getDeclaredField(filedName);
			field.setAccessible(true);
			return type.cast(field.get(instance));
		} catch (Exception e) {
			fail("Failure in reading " + clazz.getName() + filedName, e);
		}
		return null;
	}
}
