/*******************************************************************************
 * Copyright (c) 2006, 2021 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
 *     David Saff (saff@mit.edu) - initial API and implementation
 *             (bug 102632: [JUnit] Support for JUnit 4.)
 *******************************************************************************/

package org.eclipse.jdt.internal.junit.runner.junit3;

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

import org.eclipse.jdt.internal.junit.runner.FailuresFirstPrioritizer;
import org.eclipse.jdt.internal.junit.runner.ITestLoader;
import org.eclipse.jdt.internal.junit.runner.ITestPrioritizer;
import org.eclipse.jdt.internal.junit.runner.ITestReference;
import org.eclipse.jdt.internal.junit.runner.JUnitMessages;
import org.eclipse.jdt.internal.junit.runner.NullPrioritizer;
import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class JUnit3TestLoader implements ITestLoader {
	private static final String SUITE_METHODNAME= "suite"; //$NON-NLS-1$
	public static final String SET_UP_TEST_METHOD_NAME = "setUpTest"; //$NON-NLS-1$

	// WANT: give test loaders a schema

	@Override
	public ITestReference[] loadTests(Class<?>[] testClasses, String testName, String[] failureNames, String[] packages, String[][] includeExcludeTags, String uniqueId, RemoteTestRunner listener) {
		// instantiate all tests
		ITestReference[] suites= new ITestReference[testClasses.length];
		ITestPrioritizer prioritizer;

		if (failureNames != null)
			prioritizer= new FailuresFirstPrioritizer(failureNames);
		else
			prioritizer= new NullPrioritizer();

		for (int i= 0; i < suites.length; i++) {
			Class<?> testClassName= testClasses[i];
			Test test= getTest(testClassName, testName, listener);
			prioritizer.prioritize(test);
			suites[i]= new JUnit3TestReference(test);
		}

		return suites;
	}

	private Test createTest(String testName, Class<?> testClass) {
		Class<?>[] classArgs= { String.class };
		Test test;
		Constructor<?> constructor= null;
		try {
			try {
				constructor= testClass.getConstructor(classArgs);
				test= (Test) constructor.newInstance(testName);
			} catch (NoSuchMethodException e) {
				// try the no arg constructor supported in 3.8.1
				constructor= testClass.getConstructor(new Class[0]);
				test= (Test) constructor.newInstance(new Object[0]);
				if (test instanceof TestCase)
					((TestCase) test).setName(testName);
			}
			if (test != null)
				return test;
		} catch (InstantiationException e) {
			// do nothing
		} catch (IllegalAccessException e) {
			// do nothing
		} catch (InvocationTargetException e) {
			// do nothing
		} catch (NoSuchMethodException e) {
			// do nothing
		} catch (ClassCastException e) {
			// do nothing
		}
		return error(testName, "Could not create test \'" + testName + "\' "); //$NON-NLS-1$ //$NON-NLS-2$
	}

	public Test getTest(Class<?> testClass, String testName, RemoteTestRunner failureListener) {
		if (testName != null) {
			return setupTest(testClass, createTest(testName, testClass), testName);
		}
		Method suiteMethod= null;
		try {
			suiteMethod= testClass.getMethod(JUnit3TestLoader.SUITE_METHODNAME, new Class[0]);
		} catch (Exception e) {
			// try to extract a test suite automatically
			return new TestSuite(testClass);
		}
		if (!Modifier.isStatic(suiteMethod.getModifiers())) {
			return error(JUnitMessages.getString("RemoteTestRunner.error"), JUnitMessages.getString("RemoteTestRunner.error.suite.notstatic"));//$NON-NLS-1$ //$NON-NLS-2$
		}
		try {
			Test test= (Test) suiteMethod.invoke(null, new Object[0]); // static
			if (test != null) {
				return test;
			}
			return error(JUnitMessages.getString("RemoteTestRunner.error"), JUnitMessages.getString("RemoteTestRunner.error.suite.nullreturn")); //$NON-NLS-1$ //$NON-NLS-2$
		} catch (InvocationTargetException e) {
			String message= JUnitMessages.getFormattedString("RemoteTestRunner.error.invoke", e.getTargetException().toString()); //$NON-NLS-1$
			failureListener.runFailed(message, e);
			return new TestSuite(testClass);
		} catch (IllegalAccessException e) {
			String message= JUnitMessages.getFormattedString("RemoteTestRunner.error.invoke", e.toString()); //$NON-NLS-1$
			failureListener.runFailed(message, e);
			return new TestSuite(testClass);
		}
	}

	/**
	 * Prepare a single test to be run standalone. If the test case class
	 * provides a static method Test setUpTest(Test test) then this method will
	 * be invoked. Instead of calling the test method directly the "decorated"
	 * test returned from setUpTest will be called. The purpose of this
	 * mechanism is to enable tests which requires a set-up to be run
	 * individually.
	 *
	 * @param reloadedTestClass test class
	 * @param reloadedTest test instance
	 * @param testName test name
	 * @return the reloaded test, or the test wrapped with setUpTest(..) if available
	 */
	private Test setupTest(Class<?> reloadedTestClass, Test reloadedTest, String testName) {
		if (reloadedTestClass == null)
			return reloadedTest;

		Method setup= null;
		try {
			setup= reloadedTestClass.getMethod(SET_UP_TEST_METHOD_NAME, Test.class);
		} catch (SecurityException e1) {
			return reloadedTest;
		} catch (NoSuchMethodException e) {
			return reloadedTest;
		}
		if (setup.getReturnType() != Test.class)
			return error(testName, JUnitMessages.getString("RemoteTestRunner.error.notestreturn")); //$NON-NLS-1$
		if (!Modifier.isPublic(setup.getModifiers()))
			return error(testName, JUnitMessages.getString("RemoteTestRunner.error.shouldbepublic")); //$NON-NLS-1$
		if (!Modifier.isStatic(setup.getModifiers()))
			return error(testName, JUnitMessages.getString("RemoteTestRunner.error.shouldbestatic")); //$NON-NLS-1$
		try {
			Test test= (Test) setup.invoke(null, reloadedTest);
			if (test == null)
				return error(testName, JUnitMessages.getString("RemoteTestRunner.error.nullreturn")); //$NON-NLS-1$
			return test;
		} catch (IllegalArgumentException e) {
			return error(testName, JUnitMessages.getFormattedString("RemoteTestRunner.error.couldnotinvoke", e)); //$NON-NLS-1$
		} catch (IllegalAccessException e) {
			return error(testName, JUnitMessages.getFormattedString("RemoteTestRunner.error.couldnotinvoke", e)); //$NON-NLS-1$
		} catch (InvocationTargetException e) {
			return error(testName, JUnitMessages.getFormattedString("RemoteTestRunner.error.invocationexception", e.getTargetException())); //$NON-NLS-1$
		}
	}

	/**
	 * @param testName test name
	 * @param message error message
	 * @return a test which will fail and log an error message.
	 */
	private Test error(String testName, final String message) {
		return new TestCase(testName) {
			@Override
			protected void runTest() {
				fail(message);
			}
		};
	}
}
