package org.powermock.modules.junit4.legacy.internal.impl.testcaseworkaround;

import junit.framework.TestCase;
import org.junit.internal.runners.BeforeAndAfterRunner;
import org.junit.internal.runners.TestIntrospector;
import org.junit.internal.runners.TestMethodRunner;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.powermock.reflect.Whitebox;
import org.powermock.tests.utils.PowerMockTestNotifier;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * This class is needed because the test method runner creates a new instance of
 * a {@link TestIntrospector} in its constructor. The TestIntrospector needs to
 * be changed in order to support methods not annotated with Test to avoid
 * NPE's.
 * <p>
 * This class also executes the setUp and tearDown methods if the test case
 * extends TestCase. Another thing it does is to invoke all PowerMock test
 * listeners for events.
 */
public class PowerMockJUnit4LegacyTestMethodRunner extends TestMethodRunner {

	private final PowerMockJUnit4LegacyTestIntrospector testIntrospector;

	private final Method method;
	private final Description description;
	private final RunNotifier notifier;

	private final PowerMockTestNotifier powerMockTestNotifier;

	public PowerMockJUnit4LegacyTestMethodRunner(Object test, Method method, RunNotifier notifier,
			Description description, PowerMockTestNotifier powerMockTestNotifier) {
		super(test, method, notifier, description);
		this.method = method;
		this.description = description;
		this.notifier = notifier;
		this.powerMockTestNotifier = powerMockTestNotifier;
		testIntrospector = new PowerMockJUnit4LegacyTestIntrospector(test.getClass());
		Whitebox.setInternalState(this, "fTestIntrospector", testIntrospector, TestMethodRunner.class);
		Whitebox.setInternalState(this, "fTestIntrospector", testIntrospector, BeforeAndAfterRunner.class);
	}

	@Override
	public void run() {
		if (testIntrospector.isIgnored(method)) {
			notifier.fireTestIgnored(description);
			return;
		}
		notifier.fireTestStarted(description);
		try {
			powerMockTestNotifier.notifyBeforeTestMethod(Whitebox.getInternalState(this, "fTest"), method,
					new Object[0]);
			long timeout = testIntrospector.getTimeout(method);

			// Execute the the setUp method if needed
			executeMethodInTestInstance("setUp");
			if (timeout > 0) {
				// The runWithTimeout method is private in the super class,
				// invoke it using reflection.
				Whitebox.invokeMethod(this, TestMethodRunner.class, "runWithTimeout", timeout);
			} else {
				Whitebox.invokeMethod(this, TestMethodRunner.class, "runMethod");
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			try {
				executeMethodInTestInstance("tearDown");
			} finally {
				notifier.fireTestFinished(description);
			}
		}
	}

	/**
	 * This method takes care of executing a method in the test object <i>if</i>
	 * this object extends from {@link TestCase}. It can be used to execute the
	 * setUp and tearDown methods for example.
	 */
	private void executeMethodInTestInstance(String methodName) {
		if (TestCase.class.isAssignableFrom(Whitebox.getInternalState(this, "fTest").getClass())) {
			Object object = Whitebox.getInternalState(this, "fTest");
			try {
				if (object != null) {
					Whitebox.invokeMethod(object, methodName);
				}
			} catch (Throwable e) {
				if (e instanceof RuntimeException) {
					throw (RuntimeException) e;
				}
				throw new RuntimeException(e);
			}
		}
	}

	@Override
	protected void runUnprotected() {
		try {
			executeMethodBody();
			if (expectsException())
				addFailure(new AssertionError("Expected exception: " + expectedException().getName()));
		} catch (InvocationTargetException e) {
			Throwable actual = e.getTargetException();
			if (!expectsException())
				addFailure(actual);
			else if (isUnexpected(actual)) {
				String message = "Unexpected exception, expected<" + expectedException().getName() + "> but was<"
						+ actual.getClass().getName() + ">";
				addFailure(new Exception(message, actual));
			}
		} catch (Throwable e) {
			addFailure(e);
		}
	}

	private boolean isUnexpected(Throwable exception) {
		return !expectedException().isAssignableFrom(exception.getClass());
	}

	private boolean expectsException() {
		return expectedException() != null;
	}

	private Class<? extends Throwable> expectedException() {
		return testIntrospector.expectedException(method);
	}

}
