/*******************************************************************************
 * Copyright (c) 2000, 2012 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.debug.jdi.tests;

import com.sun.jdi.IncompatibleThreadStateException;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.event.ClassPrepareEvent;
import com.sun.jdi.event.ClassUnloadEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.StepEvent;
import com.sun.jdi.request.ClassPrepareRequest;
import com.sun.jdi.request.ClassUnloadRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.StepRequest;

/**
 * Tests for the hot code replacement JDI extension.
 */
public class HotCodeReplacementTest extends AbstractJDITest {
	/**
	 * Creates a new test.
	 */
	public HotCodeReplacementTest() {
		super();
	}
	private void dropTopFrame(
		ThreadReference thread,
		org.eclipse.jdi.hcr.ThreadReference hcrThread) {
		// Get stack size
		int stackSize = 0;
		try {
			stackSize = thread.frames().size();
		} catch (IncompatibleThreadStateException e) {
			fail("dropTopFrame.1");
		}

		// Create and install step out request
		StepRequest request =
			fVM.eventRequestManager().createStepRequest(
				thread,
				StepRequest.STEP_MIN,
				StepRequest.STEP_OUT);
		request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		request.enable();

		// Prepare to receive the event
		EventWaiter waiter = new EventWaiter(request, false);
		fEventReader.addEventListener(waiter);

		// Do return
		boolean finallyBlocksSkipped = hcrThread.doReturn(null, true);
		assertFalse("dropTopFrame.2", finallyBlocksSkipped);

		// Wait for the event to come in
		Event event = waitForEvent(waiter, 10000); // Wait 10s max
		assertNotNull("dropTopFrame.3", event);
		fEventReader.removeEventListener(waiter);
		fVM.eventRequestManager().deleteEventRequest(request);

		// Check thread has dropped top frame
		assertTrue("dropTopFrame.4", thread.isSuspended());
		int newStackSize = 0;
		try {
			newStackSize = thread.frames().size();
		} catch (IncompatibleThreadStateException e) {
			fail("dropTopFrame.5");
		}
		assertEquals("dropTopFrame.6", stackSize - 1, newStackSize);
	}

	/**
	 * Init the fields that are used by this test only.
	 */
	@Override
	public void localSetUp() {
		waitUntilReady();
	}
	/**
	 * Run all tests and output to standard output.
	 * @param args
	 */
	public static void main(String[] args) {
		new HotCodeReplacementTest().runSuite(args);
	}
	/**
	 * Gets the name of the test case.
	 * @see junit.framework.TestCase#getName()
	 */
	@Override
	public String getName() {
		return "Hot code replacement extension to JDI (org.eclipse.jdi.hcr) tests";
	}
	private void reenterOnExit(ThreadReference thread) {
		// Get top frame's location
		Location location = null;
		try {
			StackFrame frame = thread.frames(0, 1).get(0);
			location = frame.location();
		} catch (IncompatibleThreadStateException e) {
			fail("reenterOnExit.1");
		}

		// Create and install reenter step request
		org.eclipse.jdi.hcr.EventRequestManager eventRequestManager =
			(org.eclipse.jdi.hcr.EventRequestManager) fVM.eventRequestManager();
		org.eclipse.jdi.hcr.ReenterStepRequest request =
			eventRequestManager.createReenterStepRequest(thread);
		request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		request.enable();

		// Prepare to receive the step event
		EventWaiter waiter = new EventWaiter(request, false);
		fEventReader.addEventListener(waiter);

		// Resume thread with a doReturn so that the frame is reentered right away
		 ((org.eclipse.jdi.hcr.ThreadReference) thread).doReturn(null, false);

		// Wait for the step event to come in
		StepEvent event = (StepEvent) waitForEvent(waiter, 10000); // Wait 10s max
		assertNotNull("reenterOnExit.2", event);
		fEventReader.removeEventListener(waiter);
		fVM.eventRequestManager().deleteEventRequest(request);

		// Check that the top frame location is as expected
		Location newLocation = null;
		try {
			StackFrame frame = thread.frames(0, 1).get(0);
			newLocation = frame.location();
		} catch (IncompatibleThreadStateException e) {
			fail("reenterOnExit.3");
		}
		assertFalse("reenterOnExit.4", newLocation.equals(location));
		assertTrue("reenterOnExit.5", newLocation.codeIndex() <= location.codeIndex());

	}
	private void reloadClasses() {
		// Gets the old class
		ReferenceType oldType = getMainClass();

		// Create and install class unload and class prepare event requests
		ClassUnloadRequest unloadRequest =
			fVM.eventRequestManager().createClassUnloadRequest();
		unloadRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE);
		unloadRequest.enable();
		ClassPrepareRequest loadRequest =
			fVM.eventRequestManager().createClassPrepareRequest();
		loadRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		loadRequest.enable();

		// Prepare to receive the class unload event
		EventWaiter unloadEventWaiter = new EventWaiter(unloadRequest, true);
		fEventReader.addEventListener(unloadEventWaiter);

		// Prepare to receive the class prepare event
		EventWaiter loadEventWaiter = new EventWaiter(loadRequest, true);
		fEventReader.addEventListener(loadEventWaiter);

		// Reload classes
		org.eclipse.jdi.hcr.VirtualMachine vm =
			(org.eclipse.jdi.hcr.VirtualMachine) fVM;
		int result =
			vm.classesHaveChanged(
				new String[] { "org.eclipse.debug.jdi.tests.program.MainClass" });
		assertEquals("reloadClasses.1", org.eclipse.jdi.hcr.VirtualMachine.RELOAD_SUCCESS, result);

		// Wait for the class unload event to come in
		ClassUnloadEvent unloadEvent =
			(ClassUnloadEvent) waitForEvent(unloadEventWaiter, 10000);
		// Wait 10s max
		assertNotNull("reloadClasses.2", unloadEvent);
		fEventReader.removeEventListener(unloadEventWaiter);
		fVM.eventRequestManager().deleteEventRequest(unloadRequest);
		assertEquals(
			"reloadClasses.3",
			"org.eclipse.debug.jdi.tests.program.MainClass",
			unloadEvent.className());

		// Wait for the class prepare event to come in
		ClassPrepareEvent loadEvent =
			(ClassPrepareEvent) waitForEvent(loadEventWaiter, 10000);
		// Wait 10s max
		assertNotNull("reloadClasses.4", loadEvent);
		fEventReader.removeEventListener(loadEventWaiter);
		fVM.eventRequestManager().deleteEventRequest(loadRequest);
		ReferenceType newType = loadEvent.referenceType();
		assertEquals(
			"reloadClasses.5",
			"org.eclipse.debug.jdi.tests.program.MainClass",
			newType.name());
		assertFalse("reloadClasses.6", oldType.equals(newType));
	}
	/**
	 * Use case 1:
	 * . get a thread and suspend it
	 * . get hot code replacement capabilities
	 * . drop the top a frame
	 * . reload some classes
	 * . request reeenter on exit
	 * . resume thread
	 * . get step event
	 * . get class file version for some classes
	 */
	public void testJDIUseCase1() {
		// Get the suspended thread
		ThreadReference thread = getThread();
		assertTrue("1", thread.isSuspended());
		assertEquals("2", 1, thread.suspendCount());
		org.eclipse.jdi.hcr.ThreadReference hcrThread =
			(org.eclipse.jdi.hcr.ThreadReference) thread;

		org.eclipse.jdi.hcr.VirtualMachine vm =
			(org.eclipse.jdi.hcr.VirtualMachine) fVM;


		// Drop the top a frame
		try {
			if (vm.canDoReturn()) {
				dropTopFrame(thread, hcrThread);
			}
		} catch (UnsupportedOperationException e) {
			//not using a VM that supports the extension
			return;
		}

		// Reload classes
		if (vm.canReloadClasses()) {
			reloadClasses();
		}

		// Reenter on exit
		if (vm.canReenterOnExit()) {
			reenterOnExit(thread);
		}
	}
	/**
	 * Make sure the test leaves the VM in the same state it found it.
	 */
	@Override
	public void localTearDown() {
		waitUntilReady();
	}
}
