/*******************************************************************************
 * Copyright (c) 2005, 2006 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.operations;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.AbstractOperation;
import org.eclipse.core.commands.operations.DefaultOperationHistory;
import org.eclipse.core.commands.operations.ICompositeOperation;
import org.eclipse.core.commands.operations.IOperationApprover;
import org.eclipse.core.commands.operations.IOperationApprover2;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.LinearUndoEnforcer;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.OperationHistoryFactory;
import org.eclipse.core.commands.operations.OperationStatus;
import org.eclipse.core.commands.operations.TriggeredOperations;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.ui.tests.internal.ForcedException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests the Operations Framework API.
 *
 * @since 3.1
 */
public class OperationsAPITest {

	// number of operations to perform a stress test
	static int STRESS_NUM = 5000;

	ObjectUndoContext contextA, contextB, contextC, contextW;
	IOperationHistory history;

	IUndoableOperation op1, op2, op3, op4, op5, op6, localA, localB, localC;
	ICompositeOperation refactor;

	int preExec, postExec, preUndo, postUndo, preRedo, postRedo, add, remove, notOK, changed = 0;
	IOperationHistoryListener listener;

	public OperationsAPITest() {
		super();
	}

	@Before
	public void setUp() throws Exception {
		history = new DefaultOperationHistory();
		contextA = new ObjectUndoContext("A");
		contextB = new ObjectUndoContext("B");
		contextC = new ObjectUndoContext("C");
		op1 = new TestOperation("op1");
		op1.addContext(contextA);
		op2 = new TestOperation("op2");
		op2.addContext(contextB);
		op2.addContext(contextC);
		op3 = new TestOperation("op3");
		op3.addContext(contextC);
		op4 = new TestOperation("op4");
		op4.addContext(contextA);
		op5 = new TestOperation("op5");
		op5.addContext(contextB);
		op6 = new TestOperation("op6");
		op6.addContext(contextC);
		op6.addContext(contextA);
		history.execute(op1, null, null);
		history.execute(op2, null, null);
		history.execute(op3, null, null);
		history.execute(op4, null, null);
		history.execute(op5, null, null);
		history.execute(op6, null, null);
		preExec = 0; postExec = 0;
		preUndo = 0; postUndo = 0;
		preRedo = 0; postRedo = 0;
		add = 0; remove = 0; notOK = 0;
		listener = event -> {
			switch (event.getEventType()) {
			case OperationHistoryEvent.ABOUT_TO_EXECUTE:
				preExec++;
				break;
			case OperationHistoryEvent.ABOUT_TO_UNDO:
				preUndo++;
				break;
			case OperationHistoryEvent.ABOUT_TO_REDO:
				preRedo++;
				break;
			case OperationHistoryEvent.DONE:
				postExec++;
				break;
			case OperationHistoryEvent.UNDONE:
				postUndo++;
				break;
			case OperationHistoryEvent.REDONE:
				postRedo++;
				break;
			case OperationHistoryEvent.OPERATION_ADDED:
				add++;
				break;
			case OperationHistoryEvent.OPERATION_REMOVED:
				remove++;
				break;
			case OperationHistoryEvent.OPERATION_NOT_OK:
				notOK++;
				break;
			case OperationHistoryEvent.OPERATION_CHANGED:
				changed++;
				break;
			}
		};
		history.addOperationHistoryListener(listener);

	}

	@After
	public void tearDown() throws Exception {
		history.removeOperationHistoryListener(listener);
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
	}

	@Test
	public void testContextDispose() throws ExecutionException {
		assertSame(history.getUndoOperation(contextA), op6);
		assertSame(history.getUndoOperation(contextC), op6);
		history.dispose(contextA, true, true, false);
		assertSame(history.getUndoOperation(contextC), op6);
		assertFalse(op6.hasContext(contextA));
		history.undo(contextC, null, null);
		history.dispose(contextC, true, false, false);
		assertFalse(history.canUndo(contextC));
		assertTrue(history.canRedo(contextC));
		history.redo(contextC, null, null);
		IUndoableOperation[] ops = history.getUndoHistory(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertEquals(ops.length, 3);
		ops = history.getUndoHistory(contextC);
		assertEquals(ops.length, 1);
		ops = history.getUndoHistory(contextB);
		assertEquals(ops.length, 2);
	}

	@Test
	public void testContextHistories() throws ExecutionException {
		assertSame(history.getUndoOperation(contextA), op6);
		assertSame(history.getUndoOperation(contextB), op5);
		assertSame(history.getUndoOperation(contextC), op6);
		IStatus status = history.undo(contextC, null, null);
		assertTrue("Status should be ok", status.isOK());
		assertSame(history.getRedoOperation(contextC), op6);
		assertSame(history.getUndoOperation(contextC), op3);
		assertTrue("Should be able to redo in c3", history.canRedo(contextC));
		assertTrue("Should be able to redo in c1", history.canRedo(contextA));
		history.redo(contextA, null, null);
		assertSame(history.getUndoOperation(contextC), op6);
		assertSame(history.getUndoOperation(contextA), op6);
	}

	@Test
	public void testHistoryLimit() throws ExecutionException {
		history.setLimit(contextA, 2);
		assertTrue(history.getUndoHistory(contextA).length == 2);
		history.add(op1);
		assertTrue(history.getUndoHistory(contextA).length == 2);
		history.setLimit(contextB, 1);
		assertTrue(history.getUndoHistory(contextB).length == 1);
		assertFalse(op2.hasContext(contextB));
		history.undo(contextB, null, null);
		assertTrue(history.getRedoHistory(contextB).length == 1);
		assertTrue(history.getUndoHistory(contextB).length == 0);
		history.redo(contextB, null, null);
		assertTrue(history.getRedoHistory(contextB).length == 0);
		assertTrue(history.getUndoHistory(contextB).length == 1);
	}

	@Test
	public void testLocalHistoryLimits() throws ExecutionException {
		history.setLimit(contextC, 2);
		assertTrue(history.getUndoHistory(contextC).length == 2);
		// op2 should have context c3 removed as part of forcing the limit
		assertFalse(op2.hasContext(contextC));
		assertTrue(history.getUndoHistory(contextB).length == 2);

		history.setLimit(contextB, 1);
		assertTrue(history.getUndoHistory(contextB).length == 1);
		history.undo(contextB, null, null);
		op2.addContext(contextC);
		history.add(op2);
		assertSame(history.getUndoOperation(contextB), op2);
		assertTrue(history.getUndoHistory(contextB).length == 1);

		history.setLimit(contextA, 0);
		assertTrue(history.getUndoHistory(contextA).length == 0);
		history.add(op1);
		assertTrue(history.getUndoHistory(contextA).length == 0);
	}

	@Test
	public void testOpenOperation() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation batch = new TriggeredOperations(op1, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("no operations should be in history yet", op == null);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should be batching", op == batch);
		op.removeContext(contextB);
		assertFalse("Operation should not have context", op.hasContext(contextB));
	}

	@Test
	public void testExceptionDuringOpenOperation() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		IUndoableOperation op = new AbstractOperation("Operation with Exception") {
			@Override
			public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) {
				return Status.OK_STATUS;
			}
			@Override
			public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
				throw new ForcedException("Forced during undo");
			}
			@Override
			public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
				throw new ForcedException("Forced during redo");
			}
		};

		ICompositeOperation batch = new TriggeredOperations(op, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op.execute(null, null);
		op1.execute(null, null);
		history.add(op1);
		history.execute(op2, null, null);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		// when we undo the batch operation, the triggering op will throw the
		// ForcedException.  This is expected.
		try {
			batch.undo(null, null);
		} catch (ForcedException e) {
			// expected, no cause for panic.
		}

		// See bug #134238.  Before this bug was fixed, we would get an
		// IllegalStateException upon trying to open a composite.  If cleanup
		// after the above exception is done, then we shouldn't get an
		// IllegalStateException.
		try {
			history.openOperation(new TriggeredOperations(op3, history), IOperationHistory.EXECUTE);
			history.closeOperation(true, true, IOperationHistory.EXECUTE);
		} catch (IllegalStateException e) {
			assertTrue("IllegalStateException - trying to open an operation before a close", false);
		}
	}

	@Test
	public void test94459() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		op2.execute(null, null);
		ICompositeOperation batch = new TriggeredOperations(op2, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		history.setLimit(contextA, 0);
		op1.execute(null, null);
		history.add(op1);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should be batching", op == batch);
		assertFalse("Operation should not have context", op.hasContext(contextA));
	}

	@Test
	public void test94459AllContextsEmpty() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		op2.execute(null, null);
		ICompositeOperation batch = new TriggeredOperations(op2, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		history.setLimit(contextA, 0);
		history.setLimit(contextB, 0);
		history.setLimit(contextC, 0);
		op1.execute(null, null);
		history.add(op1);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should not have been added", op == null);
	}

	/*
	 * Test updated for 3.2 in light of
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=123316
	 * The expected behavior has changed.
	 */
	@Test
	public void test94400() throws ExecutionException {
		UnredoableTestOperation op = new UnredoableTestOperation("troubled op");
		op.addContext(contextA);
		history.execute(op, null, null);
		assertTrue("Operation should be undoable", history.canUndo(contextA));
		history.undo(contextA, null, null);
		assertTrue("Operation should still be in redo history", history.getRedoOperation(contextA) == op);
		assertFalse("Operation should not be disposed", op.disposed);
	}

	/*
	 * Similar to the test above, except that we are going to change the
	 * operation history limit and check that we disposed the operation properly.
	 */
	@Test
	public void test123316() throws ExecutionException {
		UnredoableTestOperation op = new UnredoableTestOperation("troubled op");
		op.addContext(contextA);
		history.setLimit(contextA, 0);
		history.execute(op, null, null);
		assertFalse("Should be nothing to undo", history.canUndo(contextA));
		assertTrue("Operation should be disposed", op.disposed);
	}


	@Test
	public void testUnsuccessfulOpenOperation() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation batch = new TriggeredOperations(op1, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("no operations should be in history yet", op == null);
		history.closeOperation(false, true, IOperationHistory.EXECUTE);
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertNull("Unsuccessful operation should not be added to history", op);
		assertTrue("NOT_OK notification should have been received", notOK == 1);
		assertTrue("DONE should not be sent while batching", postExec == 0);
		assertTrue("ADDED should not have been sent while batching", add == 0);
	}

	@Test
	public void testNotAddedOpenOperation() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation batch = new TriggeredOperations(op1, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("no operations should be in history yet", op == null);
		history.closeOperation(true, false, IOperationHistory.EXECUTE);
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertNull("Operation should not be added to history", op);
		assertTrue("DONE notification should have been received", postExec == 1);
		assertTrue("ADDED should not have occurred or be sent while batching", add == 0);
	}

	@Test
	public void testMultipleOpenOperation() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		boolean failure = false;
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation comp1 = new TriggeredOperations(op1, history);
		history.openOperation(comp1, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		ICompositeOperation comp2 = new TriggeredOperations(op4, history);
		try {
			history.openOperation(comp2, IOperationHistory.EXECUTE);
		} catch (IllegalStateException e) {
			failure = true;
		}
		assertTrue("Exception should have been thrown for second open operation", failure);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertNull("Unexpected nested open should not add original", op);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertSame("First operation should be closed", op, comp1);
	}

	@Test
	public void testAbortedOpenOperation() throws ExecutionException {
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		history.openOperation(new TriggeredOperations(op1, history), IOperationHistory.EXECUTE);
		op1.execute(null, null);
		history.execute(op2, null, null);
		// flush history while operation is open
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		// op3 should be added as its own op since we flushed while open
		history.add(op3);
		// should really have no effect
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Open operation should be flushed", op == op3);
	}

	@Test
	public void testOperationApproval() throws ExecutionException {
		history.addOperationApprover(new LinearUndoEnforcer());
		// the first undo should be fine
		IStatus status = history.undo(contextB, null, null);
		assertTrue(status.isOK());

		// the second causes a linear violation on C
		assertTrue(history.canUndo(contextB));
		status = history.undo(contextB, null, null);
		assertFalse(status.isOK());

		// undo the newer C items
		status = history.undo(contextC, null, null);
		assertTrue(status.isOK());
		status = history.undo(contextC, null, null);
		assertTrue(status.isOK());

		// now we should be okay in B
		status = history.undo(contextB, null, null);
		assertTrue(status.isOK());

		history.addOperationApprover(new IOperationApprover() {

			@Override
			public IStatus proceedRedoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				return Status.CANCEL_STATUS;
			}
			@Override
			public IStatus proceedUndoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				return Status.CANCEL_STATUS;
			}
		});
		// everything should fail now
		assertFalse(history.redo(contextB, null, null).isOK());
		assertFalse(history.redo(contextC, null, null).isOK());
		assertFalse(history.undo(contextA, null, null).isOK());
		assertFalse(history.undo(contextB, null, null).isOK());
		assertFalse(history.undo(contextC, null, null).isOK());
	}

	@Test
	public void testOperationFailure() throws ExecutionException {
		history.addOperationApprover(new IOperationApprover() {

			@Override
			public IStatus proceedRedoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				return Status.OK_STATUS;
			}
			@Override
			public IStatus proceedUndoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				if (o == op6) {
					return Status.CANCEL_STATUS;
				}
				if (o == op5) {
					return new OperationStatus(IStatus.ERROR, "org.eclipse.ui.tests", 0, "Error", null);
				}
				return Status.OK_STATUS;
			}
		});

		// should fail but still keep op6 on the stack since it's cancelled
		IStatus status = history.undo(contextC, null, null);
		assertFalse(status.isOK());
		assertSame(history.getUndoOperation(contextC), op6);

		// should fail since it's an error
		status = history.undo(contextB, null, null);
		assertFalse(status.isOK());

		// operation remains on stack (see bug#92506)
		assertSame(history.getUndoOperation(contextB), op5);
	}

	@Test
	public void testOperationRedo() throws ExecutionException {
		history.undo(contextB, null, null);
		history.undo(contextB, null, null);
		history.undo(contextC, null, null);
		history.undo(contextC, null, null);
		assertSame(history.getRedoOperation(contextB), op2);
		assertSame(history.getUndoOperation(contextA), op4);
		assertTrue(history.canUndo(contextA));
		assertFalse(history.canUndo(contextB));
		assertFalse(history.canUndo(contextC));
		assertTrue(preUndo == 4);
		assertTrue(postUndo == 4);
		history.redo(contextB, null, null);
		assertTrue(postRedo == 1);
		assertTrue(history.canUndo(contextB));
		assertTrue(history.canUndo(contextC));
	}

	@Test
	public void testOperationUndo() throws ExecutionException {
		history.undo(contextA, null, null);
		history.undo(contextA, null, null);
		assertSame(history.getRedoOperation(contextA), op4);
		assertSame(history.getUndoOperation(contextA), op1);
		history.undo(contextA, null, null);
		assertTrue(preUndo == 3);
		assertTrue(postUndo == 3);
		assertFalse("Shouldn't be able to undo in c1", history.canUndo(contextA));
		assertTrue("Should be able to undo in c2", history.canUndo(contextB));
		assertTrue("Should be able to undo in c3", history.canUndo(contextC));
	}

	@Test
	public void testHistoryFactory() {
		IOperationHistory anotherHistory = OperationHistoryFactory.getOperationHistory();
		assertNotNull(anotherHistory);
	}

	@Test
	public void testOperationChanged() {
		history.operationChanged(op1);
		history.operationChanged(op2);
		history.operationChanged(new TestOperation("New op"));
		assertTrue("should not notify about changes if not in the history", changed == 2);
	}

	// the setup for the infamous (local conflict on top of composite and composite gets pruned) case
	private void setup87675() throws ExecutionException {
		// clear everything out.  special setup for this test case
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		contextA = new ObjectUndoContext("A");
		contextB = new ObjectUndoContext("B");
		contextC = new ObjectUndoContext("C");
		contextW = new ObjectUndoContext("W");
		history.addOperationApprover(new LinearUndoEnforcer());

		// local edits on A, B, C are added first
		IUndoableOperation op = new TestOperation("op1a");
		op.addContext(contextA);
		history.execute(op, null, null);
		op = new TestOperation("op1b");
		op.addContext(contextB);
		history.execute(op, null, null);
		op = new TestOperation("op1c");
		op.addContext(contextC);
		history.execute(op, null, null);

		// now we create the "refactoring op" which touches them all
		op = new TestOperation("Refactoring");
		op.addContext(contextW);
		op.execute(null, null);
		refactor = new TriggeredOperations(op, history);
		history.openOperation(refactor, IOperationHistory.EXECUTE);
		localA = new TestOperation("op2a");
		localA.addContext(contextA);
		history.execute(localA, null, null);
		localB = new TestOperation("op2b");
		localB.addContext(contextB);
		history.execute(localB, null, null);
		localC = new TestOperation("op2c");
		localC.addContext(contextC);
		history.execute(localC, null, null);

		// close off the composite
		history.closeOperation(true, true, IOperationHistory.EXECUTE);

		// subsequent local edit to C
		op = new TestOperation("op3c");
		op.addContext(contextC);
		history.execute(op, null, null);
	}

	@Test
	public void test87675_split() throws ExecutionException {
		setup87675();
		IUndoableOperation op;

		// check setup
		op = history.getUndoOperation(contextA);
		assertTrue("Refactoring should be next op for context A", op == refactor);
		op = history.getUndoOperation(contextB);
		assertTrue("Refactoring should be next op for context B", op == refactor);
		op = history.getUndoOperation(contextW);
		assertTrue("Refactoring should be next op for context W", op == refactor);
		op = history.getUndoOperation(contextC);
		assertFalse("Refactoring should not be next op for context C", op == refactor);

		// try a bogus undo
		IStatus status = history.undo(contextW, null, null);
		assertFalse("Undo should not be permitted due to linear conflict", status.isOK());

		// prune the history for contextW
		history.dispose(contextW, true, true, false);

		// refactoring op should have been broken up into pieces
		op = history.getUndoOperation(contextA);
		assertTrue("Local edit A should be atomic", op == localA);
		op = history.getUndoOperation(contextB);
		assertTrue("Local edit B should be atomic", op == localB);
		op = history.getUndoOperation(contextC);
		assertFalse("Local edit C should not be refactoring edit", op == localC);

		// now the refactoring C edit should be the next one
		history.undo(contextC, null, null);
		op = history.getUndoOperation(contextC);
		assertTrue("Local edit C should be refactoring edit", op == localC);
	}

	@Test
	public void test87675_undoredo() throws ExecutionException {
		setup87675();
		IUndoableOperation op;

		// undo the local edit to C
		history.undo(contextC, null, null);

		// undo the refactoring operation via context C
		history.undo(contextC, null, null);

		// check that there are no new operations in the undo list for A, B, C
		op = history.getUndoOperation(contextC);
		assertTrue("Local edit C should be original edit", op.getLabel().equals("op1c"));

		op = history.getUndoOperation(contextB);
		assertTrue("Local edit B should be original edit", op.getLabel().equals("op1b"));

		op = history.getUndoOperation(contextA);
		assertTrue("Local edit A should be original edit", op.getLabel().equals("op1a"));

		// test that the redo operation has all contexts
		op = history.getRedoOperation(contextW);
		assertTrue("operation should have context A", op.hasContext(contextA));
		assertTrue("operation should have context B", op.hasContext(contextB));
		assertTrue("operation should have context C", op.hasContext(contextC));

		// now redo the operation
		history.redo(contextA, null, null);

		// test that the next undo is our refactoring operation
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("operation should have context W", op.hasContext(contextW));

		// undo again and check that no side effect ops were left on the undo stack
		history.undo(contextW, null, null);

		op = history.getUndoOperation(contextC);
		assertTrue("Local edit C should be original edit", op.getLabel().equals("op1c"));

		op = history.getUndoOperation(contextB);
		assertTrue("Local edit B should be original edit", op.getLabel().equals("op1b"));

		op = history.getUndoOperation(contextA);
		assertTrue("Local edit A should be original edit", op.getLabel().equals("op1a"));

	}

	@Test
	public void testOperationApprover2() throws ExecutionException {
		// clear out the history
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);

		history.addOperationApprover(new IOperationApprover2() {

			@Override
			public IStatus proceedRedoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				return Status.OK_STATUS;
			}
			@Override
			public IStatus proceedExecuting(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				if (o == op6) {
					return Status.CANCEL_STATUS;
				}
				return Status.OK_STATUS;
			}
			@Override
			public IStatus proceedUndoing(IUndoableOperation o, IOperationHistory h, IAdaptable a) {
				return Status.OK_STATUS;
			}
		});
		IStatus status = history.execute(op1, null, null);
		assertTrue(status.isOK());
		assertTrue(preExec == 1 && postExec == 1);

		status = history.execute(op6, null, null);
		assertFalse(status.isOK());
		// listener counts should not have changed
		assertTrue(preExec == 1 && postExec == 1);
	}

	@Test
	public void testReplaceContext() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		TriggeredOperations batch = new TriggeredOperations(op1, history);
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("no operations should be in history yet", op == null);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should be batching", op == batch);
		IUndoContext contextD = new ObjectUndoContext("D");
		batch.replaceContext(contextC, contextD);
		assertFalse("Operation should not have context", batch.hasContext(contextC));
		assertFalse("Operation should not have context", op1.hasContext(contextC));
		assertFalse("Operation should not have context", op2.hasContext(contextC));
		assertFalse("Operation should not have context", op3.hasContext(contextC));
		batch.replaceContext(contextD, contextC);
		assertTrue("Operation should have context", batch.hasContext(contextC));
		assertFalse("Operation should not have context", op1.hasContext(contextC));
		assertTrue("Operation should have context", op2.hasContext(contextC));
		assertTrue("Operation should have context", op3.hasContext(contextC));

	}

	// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=128117
	// Test that context is removed from a triggered operations.
	@Test
	public void test128117simple() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation batch = new TriggeredOperations(op1, history);
		IUndoContext context = new ObjectUndoContext("test");
		batch.addContext(context);
		assertTrue("Operation should have newly added context", batch.hasContext(context));
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should be the composite", op == batch);
		assertTrue("Operation should have top level context", op.hasContext(context));
		op.removeContext(context);
		assertFalse("Operation should have removed top level context", op.hasContext(context));
	}

	// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=128117
	// Test that context is removed from a triggered operations after recompute of contexts.
	@Test
	public void test128117complex() throws ExecutionException {
		// clear out history which will also reset operation execution counts
		history.dispose(IOperationHistory.GLOBAL_UNDO_CONTEXT, true, true, false);
		ICompositeOperation batch = new TriggeredOperations(op1, history);
		IUndoContext context = new ObjectUndoContext("test");
		batch.addContext(context);
		assertTrue("Operation should have top level context", batch.hasContext(context));
		history.openOperation(batch, IOperationHistory.EXECUTE);
		op1.execute(null, null);
		op2.execute(null, null);
		history.add(op2);
		history.execute(op3, null, null);
		history.closeOperation(true, true, IOperationHistory.EXECUTE);
		IUndoableOperation op = history.getUndoOperation(IOperationHistory.GLOBAL_UNDO_CONTEXT);
		assertTrue("Operation should be the composite", op == batch);
		op.removeContext(contextB);
		assertFalse("Operation should have removed child context", op.hasContext(contextB));
		assertTrue("Operation should have top level context", op.hasContext(context));
		op.removeContext(context);
		assertFalse("Operation should have removed top level context", op.hasContext(context));
	}

	@Test
	public void testStressTestAPI() throws ExecutionException {
		history.setLimit(contextA, STRESS_NUM);
		for (int i=0; i < STRESS_NUM; i++) {
			IUndoableOperation op = new TestOperation("test");
			op.addContext(contextA);
			if (i%3 == 0) {
				op.addContext(contextB);
			}
			history.execute(op, null, null);
		}
		for (int i=0; i < STRESS_NUM; i++) {
			if (i%2 == 0) {
				history.undo(contextA, null, null);
			}
			if (i%5 == 0) {
				history.redo(contextA, null, null);
			}
		}
	}

	@Test
	public void test159305() throws ExecutionException {
		final int [] approvalCount = new int[1];
		IOperationApprover approver;
		approver = new IOperationApprover() {
			@Override
			public IStatus proceedUndoing(IUndoableOperation op, IOperationHistory history, IAdaptable uiInfo) {
				approvalCount[0]++;
				return Status.OK_STATUS;
			}
			@Override
			public IStatus proceedRedoing(IUndoableOperation op, IOperationHistory history, IAdaptable uiInfo) {
				approvalCount[0]--;
				return Status.OK_STATUS;
			}
		};
		history.addOperationApprover(approver);
		history.undo(contextB, null, null);
		// approval should have only run once for linear undo
		assertTrue("Operation approver should run only once for linear undo", approvalCount[0] == 1);
		history.redo(contextB, null, null);
		assertTrue("Operation approver should run only once for linear redo", approvalCount[0] == 0);

		// approval should have only run once for direct undo
		history.undoOperation(op5, null, null);
		assertTrue("Operation approver should run only once for direct undo", approvalCount[0]== 1);
		history.redoOperation(op5, null, null);
		assertTrue("Operation approver should run only once for direct redo", approvalCount[0]== 0);

		// cleanup
		history.removeOperationApprover(approver);
	}

}
