/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springsource.loaded.testgen;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Assert;
import org.junit.runner.RunWith;
import org.springsource.loaded.ri.ReflectiveInterceptor;
import org.springsource.loaded.test.infra.Result;


/**
 * This class is intended to be subclassed to create 'generated' sprongloaded tests. It needs to be run with the
 * {@link ExploreAllChoicesRunner} test runner, using the {@link RunWith} annotation.
 * <p>
 * To create a generative test two things come together:
 * 
 * <ul>
 * <li>A mechanism to create different test configurations based on 'random' choices. These random choices are made by the test's
 * 'setup' method calling the provided 'choice' methods.
 * 
 * <li>A mechanism to run the same test twice in two different execution contexts. It is the responsibility of the test subclass.
 * One context uses an ordinary Java classloader to obtain Class objects by loading a the 'final' version of the class. The other
 * uses SpringLoaded infrastructure, and reloads a class's successive version up to the 'final' version.
 * </ul>
 * 
 * On a first run, the test runner will provide a 'recording' random choices generator and an 'ordinary Java context'. The test is
 * run multiple times until all possible choices have been explored. for each test run the choices are recorded together with the
 * observed test result. This used to populate the test tree.
 * <p>
 * Then the tests are run again replaying the recorded choices, in a SpringLoaded context. The result is compared with the result
 * from the first run. The test fails if the results are not 'equals'.
 * 
 * @author kdvolder
 */
public abstract class GenerativeSpringLoadedTest extends GenerativeTest {

	/**
	 * Provides the execution context where we can load class versions, so that we can then uses these classes to execute reflective
	 * calls on them.
	 * <p>
	 * During 'generative' setup, an execution context with a standard java class loader is used to 'predict' the expected test
	 * result. During 'replay' a SpringLoaded based implementation is used instead.
	 */
	protected IClassProvider classProvider = null;

	/**
	 * To have 'nice' toString value and display name. While generating test parameters, add some text to this buffer to describe
	 * the selected parameter. Method in this class that are called 'targetXXX' generally will add some text to this buffer.
	 */
	protected StringBuffer toStringValue = new StringBuffer();

	/**
	 * Typically, each test has a particular 'target package' which is a package in the test data project that contains the
	 * reloadable classes that this test is operating on. This method must be implemented by the subclass to provide a suitable
	 * value.
	 */
	protected abstract String getTargetPackage();

	/**
	 * Loads up a given version of a given type.
	 * <p>
	 * In 'JustJava' mode a standard Java classloader is used to immediately load the stipulated version.
	 * <p>
	 * In 'SpringLoaded' mode the original version of the type is loaded first. Then successive version are reloaded until the
	 * stipulated version number is reached.
	 * <p>
	 * This method should only be called to load a class that has not been loaded yet or an error will occur.
	 * <p>
	 * Since loading a class may trigger loading of dependent classes, it is important to call this method on classes in the correct
	 * order.
	 * 
	 * @param typeName Dotted name of the type to load.
	 * @param version One of "", "002", "003", ...
	 */
	protected Class<?> loadClassVersion(String typeName, String version) {
		return classProvider.loadClassVersion(typeName, version);
	}

	/**
	 * Get a type from the classloader. Use this to get references to already loaded classes, or to get classes that fall outside
	 * the reloadable types universe.
	 */
	public Class<?> classForName(String className) throws ClassNotFoundException {
		return classProvider.classForName(className);
	}

	@Override
	public void setup() throws Exception, RejectedChoice {
		super.setup();
		if (generative) {
			classProvider = new JustJavaClassProvider();
		} else {
			classProvider = new SpringLoadedClassProvider(getReloableTypeConfig());
		}
		chooseTestParameters();
	}

	/**
	 * This method should be overridden in order for a test to choose its test parameters. If a test throws RejectedChoice
	 * exception, this test configuration will be silently ignored. Any other exceptions raised will result in an error initialising
	 * the test suite.
	 * 
	 * @throws RejectedChoice
	 * @throws Exception
	 */
	protected abstract void chooseTestParameters() throws RejectedChoice, Exception;

	/**
	 * Override this in your own test class to configure SpringLoaded type registry.
	 */
	protected String getReloableTypeConfig() {
		String targetPackage = getTargetPackage();
		Assert.assertNotNull(targetPackage);
		return targetPackage + "..*";
	}

	/**
	 * Select a method from given class's declared methods as a test target.
	 * 
	 * @throws RejectedChoice If class has no methods to choose from.
	 */
	protected Method targetMethodFrom(Class<?> targetClass) throws RejectedChoice {
		Method[] methods = ReflectiveInterceptor.jlClassGetDeclaredMethods(targetClass);

		//To be deterministic we must sort these methods in a predictable fashion! Otherwise the test
		//may compare results from one method in the first run with those of another method in the second
		//run and fail.
		sort(methods);
		// Arrays.sort(methods, new ToStringComparator());
		Method method = choice(methods);
		toStringValue.append(method);
		return method;
	}

	/**
	 * Select a field from given class's declared field as a test target.
	 * 
	 * @throws RejectedChoice If class has no fields to choose from.
	 */
	protected Field targetFieldFrom(Class<?> clazz, FieldGetMethod howToGet) throws RejectedChoice {
		Field[] fields = null;
		switch (howToGet) {
		case GET_DECLARED_FIELD:
		case GET_DECLARED_FIELDS:
			fields = ReflectiveInterceptor.jlClassGetDeclaredFields(clazz);
			break;
		case GET_FIELD:
		case GET_FIELDS:
			fields = ReflectiveInterceptor.jlClassGetFields(clazz);
			break;
		}
		//To be deterministic we must sort these in a predictable fashion! 
//		Arrays.sort(fields, new ToStringComparator());
		sort(fields);
		Field f = choice(fields);
		toStringValue.append(f.getName());
		try {
			switch (howToGet) {
			case GET_DECLARED_FIELDS:
			case GET_FIELDS:
				return f;
			case GET_DECLARED_FIELD:
				return ReflectiveInterceptor.jlClassGetDeclaredField(clazz, f.getName());
			case GET_FIELD:
				return ReflectiveInterceptor.jlClassGetField(clazz, f.getName());
			}
		} catch (Exception e) {
			throw new Error(e);
		}
		return f;
	}

	/**
	 * Select a Constructor from given class's declared constructors as a test target.
	 * 
	 * @throws RejectedChoice If class has no Constructors to choose from.
	 */
	protected Constructor<?> targetConstructorFrom(Class<?> clazz) throws RejectedChoice {
		Constructor<?>[] constructors = ReflectiveInterceptor.jlClassGetDeclaredConstructors(clazz);
		//To be deterministic we must sort these methods in a predictable fashion! 
		//Arrays.sort(constructors, new ToStringComparator());
		sort(constructors);
		Constructor<?> c = choice(constructors);
		toStringValue.append(c);
		return c;
	}

	/**
	 * Load and selected a target type for testing. This adds the name of the class to the 'toStringValue' making it part of the
	 * test description.
	 */
	protected Class<?> targetClass(String typeName, String version) {
		toStringValue.append(typeName + version + " ");
		return loadClassVersion(getTargetPackage() + "." + typeName, version);
	}

	/**
	 * Similar to other targetClass method, but for getting an unversioned, non-reloadable type.
	 * <p>
	 * Typically this class will <b>not</b> be in the target package (since it is non-reloadable) so you must explicitly include the
	 * package name in the type name.
	 */
	protected Class<?> targetClass(String fullyQuallifiedName) throws ClassNotFoundException {
		if (fullyQuallifiedName == null) {
			toStringValue.append("null");
			return null;
		} else {
			Class<?> clazz = classForName(fullyQuallifiedName);
			toStringValue.append(clazz.getSimpleName() + " ");
			return clazz;
		}
	}

	@Override
	public String getConfigDescription() {
		return toStringValue.toString();
	}

	@Override
	public String toString() {
		return this.getClass().getSimpleName() + " " + getConfigDescription();
	}

	/**
	 * Most of the stuff we are interested in (Methods, Classes, Annotations, will not be 'equals') when they are executed in a
	 * different classloader. So we approximate this by just calling 'toString' on returned objects and comparing those.
	 */
	@Override
	protected void assertEqualResults(Result expected, Result actual) {
		Assert.assertEquals("" + expected.returnValue, "" + actual.returnValue);
	}

	/**
	 * If your test is expected to return a list of stuff that may not be 'equals' to each other because
	 * <ul>
	 * <li>objects come from different classloaders and are not equals
	 * <li>the order of the objects may vary
	 * </ul>
	 * Then, assuming the objects have a reasonable toString implementation, you can use this method as an implementation of
	 * assertEqualResults. Simply call this method from your assertEqualResults method.
	 */
	protected void assertEqualUnorderedToStringLists(Result _expected, Result _actual) {
		List<String> expected = toStringList((List<?>) _expected.returnValue);
		List<String> actual = toStringList((List<?>) _actual.returnValue);

		StringBuffer msg = new StringBuffer("Actual " + actual + " don't match expected " + expected + "\n");

		List<String> extra = new ArrayList<String>(actual);
		extra.removeAll(expected);
		if (!extra.isEmpty()) {
			msg.append("extra: \n");
			for (String string : extra) {
				msg.append("   " + string + "\n");
			}
		}

		List<String> missing = new ArrayList<String>(expected);
		missing.removeAll(actual);
		if (!missing.isEmpty()) {
			msg.append("missing: \n");
			for (String string : missing) {
				msg.append("   " + string + "\n");
			}
		}
		Assert.assertTrue(msg.toString(), missing.isEmpty() && extra.isEmpty());
		Assert.assertEquals("Duplicates in result?", expected.size(), actual.size());
	}

	/**
	 * Converts a list of any type of object into a list of Strings by calling the toString method on each object.
	 */
	protected List<String> toStringList(List<?> list) {
		List<String> result = new ArrayList<String>();
		for (Object obj : list) {
			result.add("" + obj);
		}
		return result;
	}

	protected void sort(Object[] os) {
		for (int i = 0; i < os.length; i++) {
		    for (int x = 1; x < os.length - i; x++) {
		        if (os[x - 1].toString().compareTo(os[x].toString())>0) {
		            Object temp = os[x - 1];
		            os[x - 1] = os[x];
		            os[x] = temp;

		        }
		    }
		  }
	}

}
