/*
 * 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.test;

import static org.junit.Assert.fail;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodNode;
import org.springsource.loaded.ClassRenamer;
import org.springsource.loaded.Constants;
import org.springsource.loaded.ISMgr;
import org.springsource.loaded.MethodMember;
import org.springsource.loaded.NameRegistry;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.SSMgr;
import org.springsource.loaded.TypeDescriptor;
import org.springsource.loaded.TypeRegistry;
import org.springsource.loaded.Utils;
import org.springsource.loaded.agent.SpringLoadedPreProcessor;
import org.springsource.loaded.test.ReloadingJVM.JVMOutput;
import org.springsource.loaded.test.infra.ClassPrinter;
import org.springsource.loaded.test.infra.MethodPrinter;
import org.springsource.loaded.test.infra.Result;
import org.springsource.loaded.test.infra.ResultException;
import org.springsource.loaded.test.infra.TestClassLoader;


/**
 * Abstract root test class containing helper functions.
 * 
 * @author Andy Clement
 * @since 1.0
 */
public abstract class SpringLoadedTests implements Constants {

	/**
	 * Classloader that can be used to see things in the bin directory, it is initialised ready for each test to use.
	 */
	protected ClassLoader binLoader;

	protected String TestDataPath = TestUtils.getPathToClasses("../testdata");
	protected String TestDataAspectJPath = TestUtils.getPathToClasses("../testdata-aspectj");
	protected String GroovyTestDataPath = TestUtils.getPathToClasses("../testdata-groovy");
	protected String AspectjrtJar = "../testdata/aspectjrt.jar";
	protected String CodeJar = "../testdata/code.jar";
	// TODO [java8] replace this with project dependency when Java8 is out
	protected String Java8CodeJar = findJar("../testdata-java8/build/libs","testdata-java8");
	protected String GroovyrtJar = "../testdata-groovy/groovy-all-1.8.6.jar";
	protected Result result;
	protected TypeRegistry registry;

	
	@Before
	public void setup() throws Exception {
		SpringLoadedPreProcessor.disabled = true;
		NameRegistry.reset();
		binLoader = new TestClassLoader(toURLs(TestDataPath, TestDataAspectJPath, AspectjrtJar, CodeJar, Java8CodeJar), this.getClass().getClassLoader());
	}

	@After
	public void teardown() throws Exception {
		SpringLoadedPreProcessor.disabled = false;
	}

	public void switchToGroovy() {
		binLoader = new TestClassLoader(toURLs(GroovyTestDataPath, GroovyrtJar), this.getClass().getClassLoader());
		//		Thread.currentThread().setContextClassLoader(binLoader);
	}

	/**
	 * Convert an array of string paths to an array of URLs
	 * 
	 * @param paths the string paths
	 * @return the converted URLs
	 */
	public URL[] toURLs(String... paths) {
		URL[] urls = new URL[paths.length];
		int i = 0;
		for (String path : paths) {
			try {
				urls[i++] = new File(path).toURI().toURL();
			} catch (MalformedURLException e) {
				Assert.fail(e.toString());
			}
		}
		return urls;
	}

	public Object run(Class<?> clazz, String methodname, Object... params) {
		try {
			//			System.out.println("Calling method " + methodname + " on " + clazz.getName());
			Object o = clazz.newInstance();
			Method m = null;
			Method[] ms = clazz.getMethods();
			for (Method mm : ms) {
				if (mm.getName().equals(methodname)) {
					m = mm;
					break;
				}
			}
			// Method m = clazz.getDeclaredMethod(methodname);
			return m.invoke(o, params);
		} catch (Exception e) {
			e.printStackTrace();
			Assert.fail(e.toString());
			return null;
		}
	}

	public Object run(Class<?> clazz, Object o, String methodname, Object... params) {
		try {
			System.out.println("Calling method " + methodname + " on " + clazz.getName());
			Method m = null;
			Method[] ms = clazz.getMethods();
			for (Method mm : ms) {
				if (mm.getName().equals(methodname)) {
					m = mm;
					break;
				}
			}
			// Method m = clazz.getDeclaredMethod(methodname);
			return m.invoke(o, params);
		} catch (Exception e) {
			e.printStackTrace();
			Assert.fail(e.toString());
			return null;
		}
	}

	public static boolean capture = true;

	public Result runUnguardedWithCCL(Class<?> clazz, ClassLoader ccl, String methodname, Object... params)
			throws InstantiationException, IllegalAccessException, SecurityException, NoSuchMethodException,
			IllegalArgumentException, InvocationTargetException {
		ClassLoader oldCCL = Thread.currentThread().getContextClassLoader();
		try {
			Thread.currentThread().setContextClassLoader(ccl);
			return runUnguarded(clazz, methodname, params);
		} finally {
			Thread.currentThread().setContextClassLoader(oldCCL);
		}
	}

	public Result runUnguarded(Class<?> clazz, String methodname, Object... params) throws InstantiationException,
			IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

		PrintStream oldo = System.out;
		PrintStream olde = System.err;
		Object result = null;
		ByteArrayOutputStream oso = new ByteArrayOutputStream();
		ByteArrayOutputStream ose = new ByteArrayOutputStream();
		try {
			if (capture) {
				System.setOut(new PrintStream(oso));
				System.setErr(new PrintStream(ose));
			}

			Object o = clazz.newInstance();
			Method m = null;
			Method[] ms = clazz.getMethods();
			for (Method mm : ms) {
				if (mm.getName().equals(methodname)) {
					m = mm;
					break;
				}
			}
			if (m == null) {
				Assert.fail("Invocation failure: could not find method '" + methodname + "' on type '" + clazz.getName());
			}
			m.setAccessible(true);
			result = m.invoke(o, params);
		} finally {
			if (capture) {
				System.setOut(oldo);
				System.setErr(olde);
			}
		}
		return new Result(result, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
	}

	public Result runStaticUnguarded(Class<?> clazz, String methodname, Object... params) throws InstantiationException,
			IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

		PrintStream oldo = System.out;
		PrintStream olde = System.err;
		Object result = null;
		ByteArrayOutputStream oso = new ByteArrayOutputStream();
		ByteArrayOutputStream ose = new ByteArrayOutputStream();
		try {
			if (capture) {
				System.setOut(new PrintStream(oso));
				System.setErr(new PrintStream(ose));
			}

			Method m = null;
			Method[] ms = clazz.getMethods();
			for (Method mm : ms) {
				if (mm.getName().equals(methodname)) {
					m = mm;
					break;
				}
			}
			if (m == null) {
				Assert.fail("Invocation failure: could not find method '" + methodname + "' on type '" + clazz.getName());
			}
			m.setAccessible(true);
			result = m.invoke(null, params);
		} finally {
			if (capture) {
				System.setOut(oldo);
				System.setErr(olde);
			}
		}
		return new Result(result, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
	}

	public Result runConstructor(Class<?> clazz, int whichConstructor, Object... params) throws InstantiationException,
			IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

		PrintStream oldo = System.out;
		PrintStream olde = System.err;
		Object result = null;
		ByteArrayOutputStream oso = new ByteArrayOutputStream();
		ByteArrayOutputStream ose = new ByteArrayOutputStream();
		Constructor<?> c = null;
		try {
			if (capture) {
				System.setOut(new PrintStream(oso));
				System.setErr(new PrintStream(ose));
			}

			Constructor<?>[] cs = clazz.getConstructors();
			c = cs[whichConstructor];
			System.out.println(c);
			if (c == null) {
				Assert.fail("Invocation failure: could not find constructor " + whichConstructor + " on type '" + clazz.getName());
			}
			c.setAccessible(true);
			result = c.newInstance(params);
		} finally {
			if (capture) {
				System.setOut(oldo);
				System.setErr(olde);
			}
		}
		return new Result(result, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
	}

	public Result runConstructor(Class<?> clazz, String paramDescriptor, Object... params) throws InstantiationException,
			IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {

		PrintStream oldo = System.out;
		PrintStream olde = System.err;
		Object result = null;
		ByteArrayOutputStream oso = new ByteArrayOutputStream();
		ByteArrayOutputStream ose = new ByteArrayOutputStream();
		try {
			if (capture) {
				System.setOut(new PrintStream(oso));
				System.setErr(new PrintStream(ose));
			}

			Constructor<?>[] cs = clazz.getConstructors();
			Constructor<?> c = null;
			for (Constructor<?> ctor : cs) {
				Class<?>[] paramClazzes = ctor.getParameterTypes();
				String toParamDescriptorString = toParamDescriptorString(paramClazzes);
				//				System.out.println(toParamDescriptorString + "<<");
				if (paramDescriptor.equals(toParamDescriptorString)) {
					c = ctor;
					break;
				}
			}

			if (c == null) {
				Assert.fail("Invocation failure: could not find constructor with param descriptor " + paramDescriptor
						+ " on type '" + clazz.getName());
			}
			c.setAccessible(true);
			result = c.newInstance(params);
		} finally {
			if (capture) {
				System.setOut(oldo);
				System.setErr(olde);
			}
		}
		return new Result(result, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
	}

	private String toParamDescriptorString(Class<?>[] paramClazzes) {
		if (paramClazzes == null) {
			return "";
		}
		StringBuilder s = new StringBuilder();
		for (Class<?> c : paramClazzes) {
			s.append(c.getName());
			s.append(" ");
		}
		return s.toString().trim();
	}

	public void runExpectNoSuchMethodException(Class<?> clazz, String methodname, Object... params) throws InstantiationException,
			IllegalAccessException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
		try {
			runUnguarded(clazz, methodname, params);
			Assert.fail("should not work, NSME should occur for " + methodname);
		} catch (InvocationTargetException ite) {
			String cause = ite.getCause().toString();
			if (!cause.startsWith("java.lang.NoSuchMethodError")) {
				ite.printStackTrace();
				Assert.fail("Should be a NoSuchMethodError, but got " + ite);
			}
		}
	}

	public static boolean printOutput = false;

	/**
	 * Proposed alternate version of runOnInstance that produces a wrapper Exception object similar to the Result object, but where
	 * the "result" is an exception. This is done so as not to lose the grabbed output when exception is raised by the test case.
	 */
	public static Result runOnInstance(Class<?> clazz, Object instance, String methodname, Object... params) throws ResultException {

		PrintStream oldo = System.out;
		PrintStream olde = System.err;
		Object result = null;
		Throwable exception = null;
		ByteArrayOutputStream oso = new ByteArrayOutputStream();
		ByteArrayOutputStream ose = new ByteArrayOutputStream();
		try {
			if (capture) {
				System.setOut(new PrintStream(oso));
				System.setErr(new PrintStream(ose));
			}

			Method m = null;
			Method[] ms = clazz.getMethods();
			for (Method mm : ms) {
				if (mm.getName().equals(methodname)) {
					m = mm;
					break;
				}
			}
			if (m == null) {
				throw new IllegalStateException("No method called " + methodname + " to call on type "
						+ (instance == null ? "null" : instance.getClass().getName()));
			}
			try {
				result = m.invoke(instance, params);
			} catch (Throwable e) {
				exception = e;
			}

		} finally {
			System.setOut(oldo);
			System.setErr(olde);
		}
		if (printOutput) {
			System.out.println("Collected output running: " + methodname);
			System.out.println(oso.toString());
			System.out.println(ose.toString());
		}
		if (exception != null) {
			throw new ResultException(exception, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
		} else {
			return new Result(result, oso.toString().replace("\r", ""), ose.toString().replace("\r", ""));
		}
	}

	// attempt definition - kind of a lightweight way to see if it is OK
	public Class<?> loadit(String name, byte[] bytes) {
		try {
			return ((TestClassLoader) binLoader).defineTheClass(name, bytes);
		} catch (RuntimeException t) {
			ClassPrinter.print(bytes);
			t.printStackTrace();
			throw t;
		}
	}

	public Class<?> loadClass(String name) {
		return loadit(name, loadBytesForClass(name));
	}

	protected byte[] retrieveRename(String newName, String name) {
		return ClassRenamer.rename(newName, loadBytesForClass(name));
	}

	/**
	 * retargets are "from.this.thing:to.this.thing"
	 * 
	 * @param newName
	 * @param name
	 * @param retargets of the form "this.from:this.to"
	 * @return
	 */
	protected byte[] retrieveRename(String newName, String name, String... retargets) {
		return ClassRenamer.rename(newName, loadBytesForClass(name), retargets);
	}

	protected byte[] loadBytesForClass(String dottedClassName) {
		byte[] data = Utils.loadDottedClassAsBytes(binLoader, dottedClassName);
		Assert.assertNotNull(data);
		Assert.assertNotSame(0, data.length);
		return data;
	}

	protected String getStamp(String classname) {
		return getStamp(binLoader, classname);
	}

	public static byte[] retrieveClass(ClassLoader loader, String classname) {
		byte[] data = Utils.loadDottedClassAsBytes(loader, classname);
		Assert.assertNotNull(data);
		Assert.assertNotSame(0, data.length);
		return data;
	}

	protected void print(byte[] classdata) {
		ClassReader reader = new ClassReader(classdata);
		reader.accept(new ClassPrinter(System.out), 0);
	}

	protected String printItAndReturnIt(byte[] classdata) {
		return printItAndReturnIt(classdata, false);
	}

	protected String printItAndReturnIt(byte[] classdata, boolean quoted) {
		OutputStream os = new SimpleOutputStream();
		ClassReader reader = new ClassReader(classdata);
		reader.accept(new ClassPrinter(new PrintStream(os)), 0);
		StringBuffer sb = new StringBuffer(os.toString().replace("\r", ""));
		if (!quoted) {
			return sb.toString();
		}
		for (int i = 0; i < sb.length(); i++) {
			if (sb.charAt(i) == '\n') {
				sb.insert(i + 1, "\"");
				sb.insert(i, "\\n\"+");
				i += 4;
			}
		}
		sb.delete(sb.length() - 3, sb.length());
		sb.insert(0, "\"");
		return sb.toString();
	}

	@SuppressWarnings("unchecked")
	private MethodNode getMethod(byte[] classbytes, String methodName) {
		ClassReader super_cr = new ClassReader(classbytes);
		ClassNode cn = new ClassNode();
		super_cr.accept(cn, 0);
		List<MethodNode> methods = cn.methods;
		if (methods != null) {
			for (MethodNode mn : methods) {
				if (mn.name.equals(methodName)) {
					return mn;
				}
			}
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private FieldNode getField(byte[] classbytes, String fieldName) {
		ClassReader super_cr = new ClassReader(classbytes);
		ClassNode cn = new ClassNode();
		super_cr.accept(cn, 0);
		List<FieldNode> fields = cn.fields;
		if (fields != null) {
			for (FieldNode fn : fields) {
				if (fn.name.equals(fieldName)) {
					return fn;
				}
			}
		}
		return null;
	}

	protected ClassNode getClassNode(byte[] classdata) {
		ClassNode cn = new ClassNode();
		ClassReader cr = new ClassReader(classdata);
		cr.accept(cn, 0);	
		return cn;
	}

	@SuppressWarnings("unchecked")
	protected List<MethodNode> getMethods(byte[] classdata) {
		return getClassNode(classdata).methods;
	}
	
	protected int countMethods(byte[] classdata) {
		ClassNode cn = getClassNode(classdata);
		return cn.methods==null?0:cn.methods.size();
	}	

	protected List<MethodNode> filter(List<MethodNode> methods, String nameSubstring) {
		if (methods == null) { return Collections.<MethodNode>emptyList(); }
		List<MethodNode> subset = new ArrayList<MethodNode>();
		for (MethodNode methodNode: methods) {
			if (methodNode.name.contains(nameSubstring)) {
				subset.add(methodNode);
			}
		}
		return subset;
	}
	
	protected String toStringClass(byte[] classdata) {
		return toStringClass(classdata, false, false);
	}

	protected String toStringClass(byte[] classdata, boolean includeBytecode) {
		return toStringClass(classdata, includeBytecode, false);
	}

	protected String toStringClass(byte[] classdata, boolean includeBytecode, boolean quoted) {
		OutputStream os = new SimpleOutputStream();
		ClassPrinter.print(new PrintStream(os), classdata, includeBytecode);
		String s = os.toString();
		StringBuffer sb = new StringBuffer(s.replaceAll("\r", ""));
		if (!quoted) {
			return sb.toString();
		}
		for (int i = 0; i < sb.length(); i++) {
			if (sb.charAt(i) == '\n') {
				sb.insert(i + 1, "\"");
				sb.insert(i, "\\n\"+");
				i += 4;
			}
		}
		sb.insert(0, "\"");
		sb.delete(sb.length() - 3, sb.length());
		return sb.toString();
	}

	protected String toStringMethod(byte[] classdata, String methodname, boolean quoted) {
		OutputStream os = new SimpleOutputStream();
		// ClassReader reader = new ClassReader(classdata);
		MethodNode one = getMethod(classdata, methodname);
		one.instructions.accept(new MethodPrinter(new PrintStream(os)));
		String s = os.toString();
		StringBuffer sb = new StringBuffer(s.replaceAll("\r", ""));
		if (!quoted) {
			return sb.toString();
		}
		for (int i = 0; i < sb.length(); i++) {
			if (sb.charAt(i) == '\n') {
				sb.insert(i + 1, "\"");
				sb.insert(i, "\\n\"+");
				i += 4;
			}
		}
		sb.insert(0, "Method is " + methodname + "\n\"");
		sb.delete(sb.length() - 3, sb.length());
		return sb.toString();
	}

	public static String findJar(String whereToLook, String jarPrefix) {
		File dir = new File(whereToLook);
		File[] fs = dir.listFiles();
		for (File f: fs) {
			if (f.getName().startsWith(jarPrefix)) {
				return f.toString();
			}
		}
		return null;
	}
	
	@SuppressWarnings("unchecked")
	protected String toStringField(byte[] classdata, String fieldname) {
		StringBuilder sb = new StringBuilder();
		FieldNode fieldNode = getField(classdata, fieldname);
		if (fieldNode == null) {
			return null;
		}
		List<AnnotationNode> annos = fieldNode.visibleAnnotations;
		if (annos != null) {
			sb.append("vis(").append(toStringAnnotations(annos)).append(") ");
		}
		annos = fieldNode.invisibleAnnotations;
		if (annos != null) {
			sb.append("invis(").append(toStringAnnotations(annos)).append(") ");
		}
		// will need implementing at some point:
		//		List<Attribute> attrs = fieldNode.attrs;
		//		if (attrs = !null) {
		//			sb.append("attrs(").append(toStringAttributes(attrs)).append(") ");
		//		}
		sb.append("0x").append(Integer.toHexString(fieldNode.access)).append("(")
				.append(ClassPrinter.toAccessForMember(fieldNode.access)).append(") ");
		sb.append(fieldNode.name).append(" ");
		sb.append(fieldNode.desc).append(" ");
		if (fieldNode.signature != null) {
			sb.append(fieldNode.signature).append(" ");
		}
		if (fieldNode.value != null) {
			sb.append(fieldNode.value).append(" ");
		}
		return sb.toString().trim();
	}

	private String toStringAnnotations(List<AnnotationNode> annos) {
		StringBuilder sb = new StringBuilder();
		for (AnnotationNode anno : annos) {
			sb.append(toStringAnnotation(anno)).append(" ");
		}
		return sb.toString().trim();
	}

	private String toStringAnnotation(AnnotationNode anno) {
		StringBuilder sb = new StringBuilder();
		sb.append(anno.desc);
		if (anno.values != null) {
			for (int i = 0; i < anno.values.size(); i = i + 2) {
				if (i > 0) {
					sb.append(" ");
				}
				sb.append(toStringAnnotationValue((String) anno.values.get(i), anno.values.get(i + 1)));
			}
		}

		return sb.toString().trim();
	}

	/**
	 * From asm:
	 * 
	 * The name value pairs of this annotation. Each name value pair is stored as two consecutive elements in the list. The name is
	 * a {@link String}, and the value may be a {@link Byte}, {@link Boolean}, {@link Character}, {@link Short}, {@link Integer},
	 * {@link Long}, {@link Float}, {@link Double}, {@link String} or {@link org.objectweb.asm.Type}, or an two elements String
	 * array (for enumeration values), a {@link AnnotationNode}, or a {@link List} of values of one of the preceding types. The list
	 * may be <tt>null</tt> if there is no name value pair.
	 */

	private String toStringAnnotationValue(String name, Object value) {
		StringBuilder sb = new StringBuilder();
		sb.append(name).append("=");
		if (value instanceof Byte) {
			sb.append(((Byte) value).byteValue());
		} else if (value instanceof Boolean) {
			sb.append(((Boolean) value).booleanValue());
		} else if (value instanceof Character) {
			sb.append(((Character) value).charValue());
		} else if (value instanceof Short) {
			sb.append(((Short) value).shortValue());
		} else if (value instanceof Integer) {
			sb.append(((Integer) value).intValue());
		} else if (value instanceof Long) {
			sb.append(((Long) value).longValue());
		} else if (value instanceof Float) {
			sb.append(((Float) value).floatValue());
		} else if (value instanceof Double) {
			sb.append(((Double) value).doubleValue());
		} else if (value instanceof String) {
			sb.append(((String) value));
		} else if (value instanceof Type) {
			sb.append(((Type) value).getClassName());
		} else if (value instanceof String[]) {
			String[] ss = (String[]) value;
			sb.append(ss[0]).append(ss[1]);
		} else if (value instanceof AnnotationNode) {
			sb.append(toStringAnnotation((AnnotationNode) value));
		} else if (value instanceof List) {
			throw new IllegalStateException("nyi");
		}
		return sb.toString().trim();
	}

	private static class SimpleOutputStream extends OutputStream {

		StringBuilder sb = new StringBuilder();

		@Override
		public void write(int b) throws IOException {
			sb.append((char) b);
		}

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

	}

	protected static String getStamp(ClassLoader loader, String classname) {
		Assert.assertFalse(classname.endsWith(".class"));
		Assert.assertEquals(-1, classname.indexOf('/'));
		URL resourceURL = loader.getResource(classname.replace('.', '/') + ".class");
		System.out.println(resourceURL.getFile());
		return null;
	}

	/**
	 * Look for a <name>.print file and check the printout of the bytes matches it, unless regenerate is true in which case the
	 * print out is recorded in that file.
	 */
	protected void checkIt(String name, byte[] bytes) {
		checkIt(name, bytes, shouldRegenerate());
	}

	/**
	 * Look for a <name>.print file and check the printout of the bytes matches it, unless regenerate is true in which case the
	 * print out is recorded in that file.
	 */
	protected void checkIt(String name, byte[] bytes, boolean regenerate) {
		String filename = "src/test/java/" + name.replace('.', '/') + ".print";
		try {
			if (regenerate) {
				// create the file
				System.out.println("creating " + filename);
				File f = new File(filename);
				FileWriter fos = new FileWriter(f);
				BufferedWriter dos = new BufferedWriter(fos);
				dos.write(printItAndReturnIt(bytes));
				dos.flush();
				fos.close();
			} else {
				// compare the files
				List<String> expectedLines = new ArrayList<String>();
				File f = new File(filename);
				if (!f.exists()) {
					Assert.fail("Must run with renegerate on once to create the expected output for '" + name + "'");
				}
				FileInputStream fis = new FileInputStream(f);
				BufferedReader dis = new BufferedReader(new FileReader(new File(filename)));
				String line = null;
				while ((line = dis.readLine()) != null) {
					if (line.length() != 0) {
						expectedLines.add(line);
					}
				}
				dis.close();
				fis.close();
				List<String> actualLines = toLines(printItAndReturnIt(bytes));
				if (actualLines.size() != expectedLines.size()) {
					System.out.println("actual lines=" + actualLines.size());
					System.out.println("   exp lines=" + expectedLines.size());
					Assert.assertEquals(expectedLines, actualLines);
				}
				for (int ln = 0; ln < expectedLines.size(); ln++) {
					if (!expectedLines.get(ln).equals(actualLines.get(ln))) {
						String expLine = (ln + 1) + " " + expectedLines.get(ln);
						String actLine = (ln + 1) + " " + actualLines.get(ln);
						Assert.assertEquals(expLine, actLine);
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	protected List<String> toLines(String input) {
		StringTokenizer tokenizer = new StringTokenizer(input, "\n\r");
		List<String> output = new ArrayList<String>();
		while (tokenizer.hasMoreElements()) {
			output.add(tokenizer.nextToken());
		}
		return output;
	}

	protected boolean shouldRegenerate() {
		return true;
	}

	protected void copyFile(File from, File to) {
		try {
			FileInputStream fis = new FileInputStream(from);
			FileOutputStream fos = new FileOutputStream(to);
			BufferedInputStream bis = new BufferedInputStream(fis);
			BufferedOutputStream bos = new BufferedOutputStream(fos);
			byte[] buffer = new byte[4096];
			int len;
			while ((len = bis.read(buffer, 0, 4096)) != -1) {
				bos.write(buffer, 0, len);
			}
			bis.close();
			bos.close();
		} catch (IOException ioe) {
			throw new RuntimeException("Copy file failed", ioe);
		}
	}

	protected void checkMethod(byte[] bytes, String name, String expected) {
		String actual = toStringMethod(bytes, name, false);
		if (!actual.equals(expected)) {
			// print it out for inclusion in the testcode
			System.out.println(toStringMethod(bytes, name, true));
		}
		Assert.assertEquals(expected, actual);
	}

	protected void checkType(byte[] bytes, String expected) {
		String actual = printItAndReturnIt(bytes, false);
		if (!actual.equals(expected)) {
			// print it out for inclusion in the testcode
			System.out.println(printItAndReturnIt(bytes, true));
		}
		Assert.assertEquals(expected, actual);
	}

	// ---

	// private void loadNewVersion(ReloadableType rtype, String version) throws InstantiationException,
	// IllegalAccessException {
	// loadNewVersion(rtype, version, true);
	// }
	//
	// /**
	// * Loads a new version of a type, given the name of the type, the version (suffix) and the name.
	// */
	// private void loadNewVersion(ReloadableType rtype, String version, boolean log) throws InstantiationException,
	// IllegalAccessException {
	// String name = rtype.getTypeName();
	// byte[] newclassbytes = retrieveClass(name + version);
	// newclassbytes = ClassRenamer.rename(name.replace('.', '/'), newclassbytes);
	// byte[] newdispatcher = DispatcherCreator.createFor(rtype, version);
	// if (log) {
	// System.out.println("DISPATCHER:");
	// print(newdispatcher);
	// }
	// Class<?> newdispatcherclass = loadit(name + "$D$" + version, newdispatcher);
	// byte[] newexecutorbytes = ExecutorCreator.createFor(rtype, version, newclassbytes);
	// if (log) {
	// System.out.println("EXECUTOR:");
	// print(newexecutorbytes);
	// }
	// loadit(name + "$E$" + version, newexecutorbytes);
	// TypeRegistry.poke(name.replace('.', '/'), newdispatcherclass.newInstance());
	// }
	//
	// private void checkvalue(Class<?> clazz, String methodname, Object expectedOutput) {
	// Object value = run(clazz, methodname);
	// if (!value.equals(expectedOutput)) {
	// Assert.fail("Expected " + expectedOutput + ", not " + value);
	// }
	// }

	protected void configureForTesting(TypeRegistry typeRegistry, String... includePatterns) {
		if (includePatterns != null) {
			Properties p = new Properties();
			StringBuilder s = new StringBuilder();
			for (int i = 0; i < includePatterns.length; i++) {
				if (i > 0) {
					s.append(',');
				}
				s.append(includePatterns[i]);
			}
			p.setProperty(TypeRegistry.Key_Inclusions, s.toString());
			typeRegistry.configure(p);
		}
	}

	protected void checkCause(Exception e, Class<? extends Throwable> expectedCause) {
		Throwable t = e.getCause();
		if (t == null || !t.getClass().equals(expectedCause)) {
			e.printStackTrace();
			Assert.fail("Expected cause of " + expectedCause + " but it was " + (t == null ? "null" : t.getClass()));
		}
	}

	public MethodMember grabFrom(List<MethodMember> ms, String name) {
		for (MethodMember m : ms) {
			if (m.getName().equals(name)) {
				return m;
			}
		}
		return null;
	}

	public Method grabFrom(Method[] ms, String name) {
		for (Method m : ms) {
			if (m.getName().equals(name)) {
				return m;
			}
		}
		return null;
	}

	public Field grabFrom(Field[] fs, String name) {
		for (Field f : fs) {
			if (f.getName().equals(name)) {
				return f;
			}
		}
		return null;
	}

	protected MethodMember findMethod(String toSearchFor, TypeDescriptor typeDescriptor) {
		for (MethodMember method : typeDescriptor.getMethods()) {
			if (method.toString().equals(toSearchFor)) {
				return method;
			}
		}
		return null;
	}

	/**
	 * Create a type registry, configure it with the specified reloadable type/packages and return it.
	 * 
	 * @return new TypeRegistry
	 */
	protected TypeRegistry getTypeRegistry(String includePatterns) {
		TypeRegistry.reinitialize();
		TypeRegistry tr = TypeRegistry.getTypeRegistryFor(binLoader);
		Properties p = new Properties();
		if (includePatterns != null) {
			p.setProperty(TypeRegistry.Key_Inclusions, includePatterns);
		}
		if (tr == null) {
			throw new IllegalStateException(
					"maybe you need to run with: -Dspringloaded=limit=false -Xmx512M -XX:MaxPermSize=256m -noverify");
		}
		tr.configure(p);
		return tr;
	}

	//	protected TypeRegistry getTypeRegistry(String... includePatterns) {
	//		StringBuilder s = new StringBuilder();
	//		for (int i = 0; i < includePatterns.length; i++) {
	//			if (i > 0) {
	//				s.append(',');
	//			}
	//			s.append(includePatterns[i]);
	//		}
	//		return getTypeRegistry(s.toString());
	//	}

	protected TypeRegistry getTypeRegistry() {
		return getTypeRegistry(null);
	}

	/**
	 * Make a type reload itself - this does trigger creation of the dispatcher/executor.
	 */
	protected void reload(ReloadableType reloadableType, String versionstamp) {
		reloadableType.loadNewVersion(versionstamp, reloadableType.bytesInitial);
	}

	@SuppressWarnings("unchecked")
	protected void checkLocalVariables(byte[] bytes, String methodNameAndDescriptor, String... expected) {
		ClassNode cn = new ClassNode();
		ClassReader cr = new ClassReader(bytes);
		cr.accept(cn, 0);

		boolean checked = false;
		List<MethodNode> methods = cn.methods;
		for (MethodNode mn : methods) {
			if (methodNameAndDescriptor.equals(mn.name + mn.desc)) {
				List<LocalVariableNode> localVariables = mn.localVariables;
				Assert.assertEquals(expected.length, localVariables.size());
				for (int i = 0; i < expected.length; i++) {
					StringTokenizer tokenizer = new StringTokenizer(expected[i], ":");
					String expectedName = tokenizer.nextToken();
					String expectedDesc = tokenizer.nextToken();
					LocalVariableNode localVariable = localVariables.get(i);
					Assert.assertEquals(i, localVariable.index);
					Assert.assertEquals(expectedName, localVariable.name);
					Assert.assertEquals(expectedDesc, localVariable.desc);
				}
				checked = true;
			}
		}
		if (!checked) {
			for (MethodNode mn : methods) {
				System.out.println(mn.name + mn.desc);
			}
			Assert.fail("Unable to find method " + methodNameAndDescriptor);
		}
	}

	@SuppressWarnings("unchecked")
	protected void checkAnnotations(byte[] bytes, String methodNameAndDescriptor, String... expected) {
		ClassNode cn = new ClassNode();
		ClassReader cr = new ClassReader(bytes);
		cr.accept(cn, 0);
		if (expected == null) {
			expected = new String[0];
		}

		boolean checked = false;
		List<MethodNode> methods = cn.methods;
		for (MethodNode mn : methods) {
			if (methodNameAndDescriptor.equals(mn.name + mn.desc)) {
				List<AnnotationNode> annotations = mn.visibleAnnotations;
				if (annotations == null) {
					annotations = Collections.emptyList();
				}
				Assert.assertEquals(expected.length, annotations.size());
				for (int i = 0; i < expected.length; i++) {
					//					StringTokenizer tokenizer = new StringTokenizer(expected[i], ":");
					//					String expectedName = tokenizer.nextToken();
					//					String expectedDesc = tokenizer.nextToken();
					AnnotationNode annotation = annotations.get(i);
					Assert.assertEquals(expected[i], toString(annotation));
				}
				checked = true;
			}
		}
		if (!checked) {
			for (MethodNode mn : methods) {
				System.out.println(mn.name + mn.desc);
			}
			Assert.fail("Unable to find method " + methodNameAndDescriptor);
		}
	}

	@SuppressWarnings({ "rawtypes" })
	protected Object toString(AnnotationNode annotation) {
		StringBuilder s = new StringBuilder();
		s.append("@");
		String s2 = annotation.desc.substring(1, annotation.desc.length() - 1);
		s.append(s2.replace('/', '.'));
		s.append("(");
		List values = annotation.values;
		if (values != null) {
			int i = 0;
			while (i < values.size()) {
				String name = (String) values.get(i++);
				Object value = values.get(i++);
				s.append(name).append("=").append(value);
			}
		}
		s.append(")");
		return s.toString();
	}

	protected void assertStartsWith(String prefix, Object value) {
		String stringValue = (String) value;
		if (!stringValue.startsWith(prefix)) {
			Assert.fail("Expected 'regular' toString() but was " + result.returnValue);
		}
	}

	protected void checkDoesNotContain(TypeDescriptor typeDescriptor, String string) {
		if (typeDescriptor.toString().indexOf(string) != -1) {
			Assert.fail("Did not expect to find '" + string + "' in\n" + typeDescriptor);
		}
	}

	protected void checkDoesContain(TypeDescriptor typeDescriptor, String string) {
		if (typeDescriptor.toString().indexOf(string) == -1) {
			Assert.fail("Expected to find '" + string + "' in\n" + typeDescriptor);
		}
	}

	protected ReloadableType loadType(TypeRegistry typeRegistry, String dottedTypeName) {
		return typeRegistry.addType(dottedTypeName, loadBytesForClass(dottedTypeName));
	}

	public Class<?> classForName(String typeName) throws ClassNotFoundException {
		if (typeName.endsWith("[]")) {
			Class<?> element = classForName(typeName.substring(0, typeName.length() - 2));
			return Array.newInstance(element, 0).getClass();
		} else if (typeName.equals("int")) {
			return int.class;
		} else if (typeName.equals("void")) {
			return void.class;
		} else if (typeName.equals("boolean")) {
			return boolean.class;
		} else if (typeName.equals("byte")) {
			return byte.class;
		} else if (typeName.equals("char")) {
			return char.class;
		} else if (typeName.equals("short")) {
			return short.class;
		} else if (typeName.equals("double")) {
			return double.class;
		} else if (typeName.equals("float")) {
			return float.class;
		} else if (typeName.equals("long")) {
			return long.class;
		}
		return Class.forName(typeName, false, binLoader);
	}

	protected Object intArrayToString(Object value) {
		int[] intArray = (int[]) value;
		StringBuilder s = new StringBuilder();
		s.append("{");
		if (intArray != null) {
			for (int j = 0; j < intArray.length; j++) {
				if (j > 0) {
					s.append(",");
				}
				s.append(intArray[j]);
			}
		}
		s.append("}");
		return s.toString();
	}

	protected String objectArrayToString(Object value) {
		Object[] array = (Object[]) value;
		StringBuilder s = new StringBuilder();
		s.append("{");
		if (array != null) {
			for (int j = 0; j < array.length; j++) {
				if (j > 0) {
					s.append(",");
				}
				s.append(array[j]);
			}
		}
		s.append("}");
		return s.toString();
	}

	protected void assertContains(String expectedToBeContained, String actual) {
		if (actual.indexOf(expectedToBeContained) == -1) {
			fail("\nCould not find expected data:\n" + expectedToBeContained + "\n in actual output:\n" + actual);
		}
	}

	protected void assertUniqueContains(String expectedToBeContained, String actual) {
		if (actual.indexOf(expectedToBeContained) == -1
				|| actual.indexOf(expectedToBeContained) != actual.lastIndexOf(expectedToBeContained)) {
			fail("Expected a unique occurrence of:\n" + expectedToBeContained + "\n in actual output:\n" + actual);
		}
	}

	protected void assertDoesNotContain(String expectedToBeContained, String actual) {
		if (actual.indexOf(expectedToBeContained) != -1) {
			fail("Did not expect to find data:\n" + expectedToBeContained + "\n in actual output:\n" + actual);
		}
	}

	protected ISMgr getFieldAccessor(Object o) {
		Class<?> clazz = o.getClass();
		try {
			Field f = clazz.getDeclaredField(Constants.fInstanceFieldsName);
			f.setAccessible(true);
			return (ISMgr) f.get(o);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	protected String getStaticFieldsMap(Class<?> clazz) {
		try {
			Field f = clazz.getDeclaredField(Constants.fStaticFieldsName);
			f.setAccessible(true);
			SSMgr m = (SSMgr) f.get(null);
			return m.toString();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	PrintStream oldo, olde;
	ByteArrayOutputStream oso, ose;

	/**
	 * Start intercepting the System.out/System.err streams
	 */
	protected void captureOn() {
		oldo = System.out;
		olde = System.err;
		oso = new ByteArrayOutputStream();
		ose = new ByteArrayOutputStream();
		System.setOut(new PrintStream(oso));
		System.setErr(new PrintStream(ose));
	}

	protected static String toSlash(String dottedName) {
		return dottedName.replace('.', '/');
	}

	protected static String toDotted(String slashedName) {
		return slashedName.replace('/', '.');
	}

	/**
	 * Stop intercepting the System.out/System.err streams and return any accumulated output since the captureOn.
	 */
	protected String captureOff() {
		if (oldo == null) {
			throw new IllegalStateException("Turning capture off without having turned it on");
		}
		System.setOut(oldo);
		System.setErr(olde);
		oldo = null;
		olde = null;
		return new String("SYSOUT\n" + oso.toString().replace("\r", "") + "\nSYSERR\n" + ose.toString().replace("\r", "") + "\n");
	}

	protected String captureOffReturnStdout() {
		if (oldo == null) {
			throw new IllegalStateException("Turning capture off without having turned it on");
		}
		System.setOut(oldo);
		System.setErr(olde);
		oldo = null;
		olde = null;
		return new String(oso.toString());
	}

	/**
	 * Called at the end of a test to tidy up in case a test crashed and failed to stop capturing.
	 */
	protected void ensureCaptureOff() {
		if (oldo != null) {
			System.setOut(oldo);
			System.setErr(olde);
			oldo = null;
		}
	}

	/**
	 * Execute a specific method, returning all output that occurred during the run to the caller.
	 */
	public String runMethodAndCollectOutput(Class<?> clazz, String methodname) throws Exception {
		captureOn();
		Method m = clazz.getDeclaredMethod(methodname);
		if (!Modifier.isStatic(m.getModifiers())) {
			fail("Method should be static: " + m);
		}
		m.invoke(null);
		return captureOff();
	}

	protected String slashed(String dotted) {
		return dotted.replaceAll("\\.", "/");
	}

	protected final static void pause(int seconds) {
		try {
			Thread.sleep(seconds*1000);
		} catch (Exception e) {}
	}
	
	protected void assertStdout(String expectedStdout, JVMOutput actualOutput) {
		if (!expectedStdout.equals(actualOutput.stdout)) {
			//			assertEquals(expectedStdout, actualOutput.stdout);
			fail("Expected stdout '" + expectedStdout + "' not found in \n" + actualOutput.toString());
		}
	}
	
	protected void assertStdoutContains(String expectedStdout, JVMOutput actualOutput) {
		if (!actualOutput.stdout.contains(expectedStdout)) {
			fail("Expected stdout:\n" + expectedStdout + "\nbut was:\n" + actualOutput.stdout.toString()+"\nComplete output: \n"+actualOutput.toString());
		}
	}
	
	protected void assertStdoutContains(String expectedStdout, Result r) {
		// TODO Auto-generated method stub
		
	}


	protected void assertStderrContains(String expectedStderrContains, JVMOutput actualOutput) {
		if (actualOutput.stderr.indexOf(expectedStderrContains) == -1) {
			fail("Expected stderr to contain '" + expectedStderrContains + "'\n" + actualOutput.toString());
		}
	}
	
}
