/*
 * Copyright 2010 the original author or authors.
 *
 * 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 powermock.classloading;

import javassist.CtClass;
import org.junit.Test;
import org.powermock.classloading.SingleClassloaderExecutor;
import org.powermock.core.classloader.MockClassLoader;
import org.powermock.core.transformers.MockTransformer;
import powermock.classloading.classes.MyArgument;
import powermock.classloading.classes.MyClass;
import powermock.classloading.classes.MyCollectionHolder;
import powermock.classloading.classes.MyEnum;
import powermock.classloading.classes.MyEnumHolder;
import powermock.classloading.classes.MyHierarchicalFieldHolder;
import powermock.classloading.classes.MyHierarchicalOverloadedFieldHolder;
import powermock.classloading.classes.MyIntegerHolder;
import powermock.classloading.classes.MyPrimitiveArrayHolder;
import powermock.classloading.classes.MyReferenceFieldHolder;
import powermock.classloading.classes.MyReturnValue;
import powermock.classloading.classes.MyStaticFinalArgumentHolder;
import powermock.classloading.classes.MyStaticFinalNumberHolder;
import powermock.classloading.classes.MyStaticFinalPrimitiveHolder;
import powermock.classloading.classes.ReflectionMethodInvoker;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.Callable;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

public class XStreamClassloaderExecutorTest {

	@Test
	public void loadsObjectGraphInSpecifiedClassloaderAndReturnsResultInOriginalClassloader() throws Exception {
		MockClassLoader classloader = createClassloader();
		final MyReturnValue expectedConstructorValue = new MyReturnValue(new MyArgument("first value"));
		final MyClass myClass = new MyClass(expectedConstructorValue);
		final MyArgument expected = new MyArgument("A value");
		MyReturnValue[] actual = new SingleClassloaderExecutor(classloader).execute(new Callable<MyReturnValue[]>() {
			public MyReturnValue[] call() throws Exception {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				return myClass.myMethod(expected);
			}
		});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));

		final MyReturnValue myReturnValue = actual[0];
		assertEquals(expectedConstructorValue.getMyArgument().getValue(), myReturnValue.getMyArgument().getValue());
		assertEquals(expected.getValue(), actual[1].getMyArgument().getValue());
	}

	@Test
	public void loadsObjectGraphThatIncludesPrimitiveValuesInSpecifiedClassloaderAndReturnsResultInOriginalClassloader()
			throws Exception {
		MockClassLoader classloader = createClassloader();
		final Integer expected = 42;
		final MyIntegerHolder myClass = new MyIntegerHolder(expected);
		Integer actual = new SingleClassloaderExecutor(classloader).execute(new Callable<Integer>() {
			public Integer call() throws Exception {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				final int myInteger = myClass.getMyInteger();
				assertEquals((int) expected, myInteger);
				return myInteger;
			}
		});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));

		assertEquals(expected, actual);
	}

	@Test
	public void loadsObjectGraphThatIncludesEnumsInSpecifiedClassloaderAndReturnsResultInOriginalClassloader()
			throws Exception {
		MockClassLoader classloader = createClassloader();
		final MyEnum expected = MyEnum.MyEnum1;
		final MyEnumHolder myClass = new MyEnumHolder(expected);
		MyEnum actual = new SingleClassloaderExecutor(classloader).execute(new Callable<MyEnum>() {
			public MyEnum call() throws Exception {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				MyEnum myEnum = myClass.getMyEnum();
				assertEquals(expected, myEnum);
				return myEnum;
			}
		});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertEquals(expected, actual);
	}

	@Test
	public void clonesStaticFinalObjectFields() throws Exception {
		MockClassLoader classloader = createClassloader();
		final MyStaticFinalArgumentHolder expected = new MyStaticFinalArgumentHolder();
		MyStaticFinalArgumentHolder actual = new SingleClassloaderExecutor(classloader)
				.execute(new Callable<MyStaticFinalArgumentHolder>() {
					public MyStaticFinalArgumentHolder call() throws Exception {
						assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass()
								.getName());
						MyStaticFinalArgumentHolder actual = new MyStaticFinalArgumentHolder();
						assertEquals(expected.getMyObject(), actual.getMyObject());
						return actual;
					}
				});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertEquals(expected.getMyObject(), actual.getMyObject());
	}

	@Test
	public void clonesStaticFinalPrimitiveFields() throws Exception {
		MockClassLoader classloader = createClassloader();
		final MyStaticFinalPrimitiveHolder expected = new MyStaticFinalPrimitiveHolder();
		MyStaticFinalPrimitiveHolder actual = new SingleClassloaderExecutor(classloader)
				.execute(new Callable<MyStaticFinalPrimitiveHolder>() {
					public MyStaticFinalPrimitiveHolder call() throws Exception {
						assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass()
								.getName());
						MyStaticFinalPrimitiveHolder actual = new MyStaticFinalPrimitiveHolder();
						assertEquals(expected.getMyInt(), actual.getMyInt());
						return actual;
					}
				});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertEquals(expected.getMyInt(), actual.getMyInt());
	}

	@Test
	public void clonesStaticFinalNumberFields() throws Exception {
		MockClassLoader classloader = createClassloader();
		final MyStaticFinalNumberHolder expected = new MyStaticFinalNumberHolder();
		MyStaticFinalNumberHolder actual = new SingleClassloaderExecutor(classloader)
				.execute(new Callable<MyStaticFinalNumberHolder>() {
					public MyStaticFinalNumberHolder call() throws Exception {
						assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass()
								.getName());
						MyStaticFinalNumberHolder actual = new MyStaticFinalNumberHolder();
						assertEquals(expected.getMyLong(), actual.getMyLong());
						return actual;
					}
				});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertEquals(expected.getMyLong(), actual.getMyLong());
	}

	@Test
	public void loadsObjectGraphThatIncludesPrimitiveArraysInSpecifiedClassloaderAndReturnsResultInOriginalClassloader()
			throws Exception {
		MockClassLoader classloader = createClassloader();
		final int[] expected = new int[] { 1, 2 };
		final MyPrimitiveArrayHolder myClass = new MyPrimitiveArrayHolder(expected);
		int[] actual = new SingleClassloaderExecutor(classloader).execute(new Callable<int[]>() {
			public int[] call() throws Exception {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				int[] myArray = myClass.getMyArray();
				assertArrayEquals(expected, myArray);
				return myArray;
			}
		});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertArrayEquals(expected, actual);
	}

	@Test
	public void loadsObjectGraphThatIncludesCollectionInSpecifiedClassloaderAndReturnsResultInOriginalClassloader()
			throws Exception {
		final MockClassLoader classloader = createClassloader();
		final Collection<MyReturnValue> expected = new LinkedList<MyReturnValue>();
		expected.add(new MyReturnValue(new MyArgument("one")));
		expected.add(new MyReturnValue(new MyArgument("two")));
		final MyCollectionHolder myClass = new MyCollectionHolder(expected);
		Collection<?> actual = new SingleClassloaderExecutor(classloader).execute(new Callable<Collection<?>>() {
			public Collection<?> call() throws Exception {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				Collection<?> myCollection = myClass.getMyCollection();
				for (Object object : myCollection) {
					assertEquals(MockClassLoader.class.getName(), object.getClass().getClassLoader().getClass()
							.getName());
				}
				return myCollection;
			}
		});

		assertFalse(MockClassLoader.class.getName().equals(this.getClass().getClassLoader().getClass().getName()));
		assertEquals(2, actual.size());
		for (Object object : actual) {
			final String value = ((MyReturnValue) object).getMyArgument().getValue();
			assertTrue(value.equals("one") || value.equals("two"));
		}
	}

	@Test
	public void usesReferenceCloningWhenTwoFieldsPointToSameInstance() throws Exception {
		final MockClassLoader classloader = createClassloader();
		final MyReferenceFieldHolder tested = new MyReferenceFieldHolder();
		assertSame(tested.getMyArgument1(), tested.getMyArgument2());
		assertSame(tested.getMyArgument1(), MyReferenceFieldHolder.MY_ARGUMENT);
		new SingleClassloaderExecutor(classloader).execute(new Runnable() {
			public void run() {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				assertEquals(tested.getMyArgument1(), tested.getMyArgument2());
				assertEquals(tested.getMyArgument1(), MyReferenceFieldHolder.MY_ARGUMENT);
				assertSame(tested.getMyArgument1(), tested.getMyArgument2());
				// FIXME: This assertion should work:
				// assertSame(tested.getMyArgument1(), MyReferenceFieldHolder.MY_ARGUMENT);
			}
		});
	}

	@Test
	public void worksWithObjectHierarchy() throws Exception {
		final MockClassLoader classloader = createClassloader();
		final MyHierarchicalFieldHolder tested = new MyHierarchicalFieldHolder();
		assertSame(tested.getMyArgument1(), tested.getMyArgument2());
		assertEquals(tested.getMyArgument3(), tested.getMyArgument2());
		new SingleClassloaderExecutor(classloader).execute(new Runnable() {
			public void run() {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				assertSame(tested.getMyArgument1(), tested.getMyArgument2());
				assertEquals(tested.getMyArgument3(), tested.getMyArgument2());
			}
		});
	}

	@Test
	public void worksWithObjectHierarchyAndOverloadedFields() throws Exception {
		final MockClassLoader classloader = createClassloader();
		final MyHierarchicalOverloadedFieldHolder tested = new MyHierarchicalOverloadedFieldHolder();
		assertSame(tested.getMyArgument1(), tested.getMyArgument2());
		assertEquals(tested.getMyArgument1(), tested.getMyArgument3());
		assertSame(tested.getMyArgument3(), MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
		assertNotSame(MyReferenceFieldHolder.MY_ARGUMENT, MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
		assertEquals(MyReferenceFieldHolder.MY_ARGUMENT, MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
		new SingleClassloaderExecutor(classloader).execute(new Runnable() {
			public void run() {
				assertEquals(MockClassLoader.class.getName(), this.getClass().getClassLoader().getClass().getName());
				assertSame(tested.getMyArgument1(), tested.getMyArgument2());
				assertEquals(tested.getMyArgument1(), tested.getMyArgument3());
                // Note: Cannot be same using X-Stream
				assertEquals(tested.getMyArgument3(), MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
				assertNotSame(MyReferenceFieldHolder.MY_ARGUMENT, MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
				assertEquals(MyReferenceFieldHolder.MY_ARGUMENT, MyHierarchicalOverloadedFieldHolder.MY_ARGUMENT);
			}
		});
	}

	@Test
	public void worksWithReflection() throws Exception {
		final MockClassLoader classloader = createClassloader();
		final MyArgument myArgument = new MyArgument("test");
		final MyReturnValue instance = new MyReturnValue(myArgument);
		Method method = instance.getClass().getMethod("getMyArgument");
		final ReflectionMethodInvoker tested = new ReflectionMethodInvoker(method, instance);
		new SingleClassloaderExecutor(classloader).execute(new Runnable() {
			public void run() {
				Object invoke = tested.invoke();
				assertSame(invoke, myArgument);
			}
		});
	}

	private MockClassLoader createClassloader() {
		MockClassLoader classloader = new MockClassLoader(new String[] { MyClass.class.getName(),
				MyArgument.class.getName(), MyReturnValue.class.getName() });
		MockTransformer mainMockTransformer = new MockTransformer() {
			public CtClass transform(CtClass clazz) throws Exception {
				return clazz;
			}
		};
		LinkedList<MockTransformer> linkedList = new LinkedList<MockTransformer>();
		linkedList.add(mainMockTransformer);
		classloader.setMockTransformerChain(linkedList);
		return classloader;
	}
}
