/*
 * Copyright 2002-2014 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 org.springframework.expression.spel.support;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ParseException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.AbstractExpressionTests;
import org.springframework.expression.spel.SpelUtilities;
import org.springframework.expression.spel.ast.FormatHelper;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind;

import static org.junit.Assert.*;

/**
 * Tests for any helper code.
 *
 * @author Andy Clement
 */
public class ReflectionHelperTests extends AbstractExpressionTests {

	@Test
	public void testFormatHelperForClassName() {
		assertEquals("java.lang.String",FormatHelper.formatClassNameForMessage(String.class));
		assertEquals("java.lang.String[]",FormatHelper.formatClassNameForMessage(new String[1].getClass()));
		assertEquals("int[]",FormatHelper.formatClassNameForMessage(new int[1].getClass()));
		assertEquals("int[][]",FormatHelper.formatClassNameForMessage(new int[1][2].getClass()));
		assertEquals("null",FormatHelper.formatClassNameForMessage(null));
	}

	/*
	@Test
	public void testFormatHelperForMethod() {
		assertEquals("foo(java.lang.String)",FormatHelper.formatMethodForMessage("foo", String.class));
		assertEquals("goo(java.lang.String,int[])",FormatHelper.formatMethodForMessage("goo", String.class, new int[1].getClass()));
		assertEquals("boo()",FormatHelper.formatMethodForMessage("boo"));
	}
	*/

	@Test
	public void testUtilities() throws ParseException {
		SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2");
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		PrintStream ps = new PrintStream(baos);
		SpelUtilities.printAbstractSyntaxTree(ps, expr);
		ps.flush();
		String s = baos.toString();
//		===> Expression '3+4+5+6+7-2' - AST start
//		OperatorMinus  value:(((((3 + 4) + 5) + 6) + 7) - 2)  #children:2
//		  OperatorPlus  value:((((3 + 4) + 5) + 6) + 7)  #children:2
//		    OperatorPlus  value:(((3 + 4) + 5) + 6)  #children:2
//		      OperatorPlus  value:((3 + 4) + 5)  #children:2
//		        OperatorPlus  value:(3 + 4)  #children:2
//		          CompoundExpression  value:3
//		            IntLiteral  value:3
//		          CompoundExpression  value:4
//		            IntLiteral  value:4
//		        CompoundExpression  value:5
//		          IntLiteral  value:5
//		      CompoundExpression  value:6
//		        IntLiteral  value:6
//		    CompoundExpression  value:7
//		      IntLiteral  value:7
//		  CompoundExpression  value:2
//		    IntLiteral  value:2
//		===> Expression '3+4+5+6+7-2' - AST end
		assertTrue(s.contains("===> Expression '3+4+5+6+7-2' - AST start"));
		assertTrue(s.contains(" OpPlus  value:((((3 + 4) + 5) + 6) + 7)  #children:2"));
	}

	@Test
	public void testTypedValue() {
		TypedValue tv1 = new TypedValue("hello");
		TypedValue tv2 = new TypedValue("hello");
		TypedValue tv3 = new TypedValue("bye");
		assertEquals(String.class, tv1.getTypeDescriptor().getType());
		assertEquals("TypedValue: 'hello' of [java.lang.String]", tv1.toString());
		assertEquals(tv1, tv2);
		assertEquals(tv2, tv1);
		assertNotEquals(tv1, tv3);
		assertNotEquals(tv2, tv3);
		assertNotEquals(tv3, tv1);
		assertNotEquals(tv3, tv2);
		assertEquals(tv1.hashCode(), tv2.hashCode());
		assertNotEquals(tv1.hashCode(), tv3.hashCode());
		assertNotEquals(tv2.hashCode(), tv3.hashCode());
	}

	@Test
	public void testReflectionHelperCompareArguments_ExactMatching() {
		StandardTypeConverter tc = new StandardTypeConverter();

		// Calling foo(String) with (String) is exact match
		checkMatch(new Class[] {String.class}, new Class[] {String.class}, tc, ReflectionHelper.ArgumentsMatchKind.EXACT);

		// Calling foo(String,Integer) with (String,Integer) is exact match
		checkMatch(new Class[] {String.class, Integer.class}, new Class[] {String.class, Integer.class}, tc, ArgumentsMatchKind.EXACT);
	}

	@Test
	public void testReflectionHelperCompareArguments_CloseMatching() {
		StandardTypeConverter tc = new StandardTypeConverter();

		// Calling foo(List) with (ArrayList) is close match (no conversion required)
		checkMatch(new Class[] {ArrayList.class}, new Class[] {List.class}, tc, ArgumentsMatchKind.CLOSE);

		// Passing (Sub,String) on call to foo(Super,String) is close match
		checkMatch(new Class[] {Sub.class, String.class}, new Class[] {Super.class, String.class}, tc, ArgumentsMatchKind.CLOSE);

		// Passing (String,Sub) on call to foo(String,Super) is close match
		checkMatch(new Class[] {String.class, Sub.class}, new Class[] {String.class, Super.class}, tc, ArgumentsMatchKind.CLOSE);
	}

	@Test
	public void testReflectionHelperCompareArguments_RequiresConversionMatching() {
		StandardTypeConverter tc = new StandardTypeConverter();

		// Calling foo(String,int) with (String,Integer) requires boxing conversion of argument one
		checkMatch(new Class[] {String.class, Integer.TYPE}, new Class[] {String.class,Integer.class},tc, ArgumentsMatchKind.CLOSE);

		// Passing (int,String) on call to foo(Integer,String) requires boxing conversion of argument zero
		checkMatch(new Class[] {Integer.TYPE, String.class}, new Class[] {Integer.class, String.class},tc, ArgumentsMatchKind.CLOSE);

		// Passing (int,Sub) on call to foo(Integer,Super) requires boxing conversion of argument zero
		checkMatch(new Class[] {Integer.TYPE, Sub.class}, new Class[] {Integer.class, Super.class}, tc, ArgumentsMatchKind.CLOSE);

		// Passing (int,Sub,boolean) on call to foo(Integer,Super,Boolean) requires boxing conversion of arguments zero and two
		// TODO checkMatch(new Class[] {Integer.TYPE, Sub.class, Boolean.TYPE}, new Class[] {Integer.class, Super.class, Boolean.class}, tc, ArgsMatchKind.REQUIRES_CONVERSION);
	}

	@Test
	public void testReflectionHelperCompareArguments_NotAMatch() {
		StandardTypeConverter typeConverter = new StandardTypeConverter();

		// Passing (Super,String) on call to foo(Sub,String) is not a match
		checkMatch(new Class[] {Super.class,String.class}, new Class[] {Sub.class,String.class},typeConverter,null);
	}

	@Test
	public void testReflectionHelperCompareArguments_Varargs_ExactMatching() {
		StandardTypeConverter tc = new StandardTypeConverter();
		Class<?> stringArrayClass = new String[0].getClass();
		Class<?> integerArrayClass = new Integer[0].getClass();

		// Passing (String[]) on call to (String[]) is exact match
		checkMatch2(new Class[] {stringArrayClass}, new Class[] {stringArrayClass}, tc, ArgumentsMatchKind.EXACT);

		// Passing (Integer, String[]) on call to (Integer, String[]) is exact match
		checkMatch2(new Class[] {Integer.class, stringArrayClass}, new Class[] {Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);

		// Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match
		checkMatch2(new Class[] {String.class, Integer.class, stringArrayClass}, new Class[] {String.class,Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);

		// Passing (Sub, String[]) on call to (Super, String[]) is exact match
		checkMatch2(new Class[] {Sub.class, stringArrayClass}, new Class[] {Super.class,stringArrayClass}, tc, ArgumentsMatchKind.CLOSE);

		// Passing (Integer, String[]) on call to (String, String[]) is exact match
		checkMatch2(new Class[] {Integer.class, stringArrayClass}, new Class[] {String.class, stringArrayClass}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);

		// Passing (Integer, Sub, String[]) on call to (String, Super, String[]) is exact match
		checkMatch2(new Class[] {Integer.class, Sub.class, String[].class}, new Class[] {String.class,Super .class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);

		// Passing (String) on call to (String[]) is exact match
		checkMatch2(new Class[] {String.class}, new Class[] {stringArrayClass}, tc, ArgumentsMatchKind.EXACT);

		// Passing (Integer,String) on call to (Integer,String[]) is exact match
		checkMatch2(new Class[] {Integer.class, String.class}, new Class[] {Integer.class, stringArrayClass}, tc, ArgumentsMatchKind.EXACT);

		// Passing (String) on call to (Integer[]) is conversion match (String to Integer)
		checkMatch2(new Class[] {String.class}, new Class[] {integerArrayClass}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);

		// Passing (Sub) on call to (Super[]) is close match
		checkMatch2(new Class[] {Sub.class}, new Class[] {new Super[0].getClass()}, tc, ArgumentsMatchKind.CLOSE);

		// Passing (Super) on call to (Sub[]) is not a match
		checkMatch2(new Class[] {Super.class}, new Class[] {new Sub[0].getClass()}, tc, null);

		checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null);

		checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null);

		checkMatch2(new Class[] {Unconvertable.class, String.class}, new Class[] {Sub.class, Super[].class}, tc, null);

		checkMatch2(new Class[] {Integer.class, Integer.class, String.class}, new Class[] {String.class, String.class, Super[].class}, tc, null);

		checkMatch2(new Class[] {Integer.class, Integer.class, Sub.class}, new Class[] {String.class, String.class, Super[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);

		checkMatch2(new Class[] {Integer.class, Integer.class, Integer.class}, new Class[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION);
		// what happens on (Integer,String) passed to (Integer[]) ?
	}

	@Test
	public void testConvertArguments() throws Exception {
		StandardTypeConverter tc = new StandardTypeConverter();
		Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
		Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);

		// basic conversion int>String
		Object[] args = new Object[] {3};
		ReflectionHelper.convertArguments(tc, args, oneArg, null);
		checkArguments(args, "3");

		// varargs but nothing to convert
		args = new Object[] {3};
		ReflectionHelper.convertArguments(tc, args, twoArg, 1);
		checkArguments(args, "3");

		// varargs with nothing needing conversion
		args = new Object[] {3, "abc", "abc"};
		ReflectionHelper.convertArguments(tc, args, twoArg, 1);
		checkArguments(args, "3","abc","abc");

		// varargs with conversion required
		args = new Object[] {3, false ,3.0d};
		ReflectionHelper.convertArguments(tc, args, twoArg, 1);
		checkArguments(args, "3","false","3.0");
	}

	@Test
	public void testConvertArguments2() throws Exception {
		StandardTypeConverter tc = new StandardTypeConverter();
		Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
		Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);

		// Simple conversion: int to string
		Object[] args = new Object[] {3};
		ReflectionHelper.convertAllArguments(tc, args, oneArg);
		checkArguments(args,"3");

		// varargs conversion
		args = new Object[] {3, false, 3.0f};
		ReflectionHelper.convertAllArguments(tc, args, twoArg);
		checkArguments(args,"3","false","3.0");

		// varargs conversion but no varargs
		args = new Object[] {3};
		ReflectionHelper.convertAllArguments(tc, args, twoArg);
		checkArguments(args,"3");

		// null value
		args = new Object[] {3, null, 3.0f};
		ReflectionHelper.convertAllArguments(tc, args, twoArg);
		checkArguments(args,"3",null,"3.0");
	}

	@Test
	public void testSetupArguments() {
		Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(new Class[] {new String[0].getClass()},"a","b","c");

		assertEquals(1, newArray.length);
		Object firstParam = newArray[0];
		assertEquals(String.class,firstParam.getClass().getComponentType());
		Object[] firstParamArray = (Object[])firstParam;
		assertEquals(3,firstParamArray.length);
		assertEquals("a",firstParamArray[0]);
		assertEquals("b",firstParamArray[1]);
		assertEquals("c",firstParamArray[2]);
	}

	@Test
	public void testReflectivePropertyResolver() throws Exception {
		ReflectivePropertyAccessor rpr = new ReflectivePropertyAccessor();
		Tester t = new Tester();
		t.setProperty("hello");
		EvaluationContext ctx = new StandardEvaluationContext(t);
		assertTrue(rpr.canRead(ctx, t, "property"));
		assertEquals("hello",rpr.read(ctx, t, "property").getValue());
		assertEquals("hello",rpr.read(ctx, t, "property").getValue()); // cached accessor used

		assertTrue(rpr.canRead(ctx, t, "field"));
		assertEquals(3,rpr.read(ctx, t, "field").getValue());
		assertEquals(3,rpr.read(ctx, t, "field").getValue()); // cached accessor used

		assertTrue(rpr.canWrite(ctx, t, "property"));
		rpr.write(ctx, t, "property","goodbye");
		rpr.write(ctx, t, "property","goodbye"); // cached accessor used

		assertTrue(rpr.canWrite(ctx, t, "field"));
		rpr.write(ctx, t, "field",12);
		rpr.write(ctx, t, "field",12);

		// Attempted write as first activity on this field and property to drive testing
		// of populating type descriptor cache
		rpr.write(ctx,t,"field2",3);
		rpr.write(ctx, t, "property2","doodoo");
		assertEquals(3,rpr.read(ctx,t,"field2").getValue());

		// Attempted read as first activity on this field and property (no canRead before them)
		assertEquals(0,rpr.read(ctx,t,"field3").getValue());
		assertEquals("doodoo",rpr.read(ctx,t,"property3").getValue());

		// Access through is method
//		assertEquals(0,rpr.read(ctx,t,"field3").getValue());
		assertEquals(false,rpr.read(ctx,t,"property4").getValue());
		assertTrue(rpr.canRead(ctx,t,"property4"));

		// repro SPR-9123, ReflectivePropertyAccessor JavaBean property names compliance tests
		assertEquals("iD",rpr.read(ctx,t,"iD").getValue());
		assertTrue(rpr.canRead(ctx,t,"iD"));
		assertEquals("id",rpr.read(ctx,t,"id").getValue());
		assertTrue(rpr.canRead(ctx,t,"id"));
		assertEquals("ID",rpr.read(ctx,t,"ID").getValue());
		assertTrue(rpr.canRead(ctx,t,"ID"));
		// note: "Id" is not a valid JavaBean name, nevertheless it is treated as "id"
		assertEquals("id",rpr.read(ctx,t,"Id").getValue());
		assertTrue(rpr.canRead(ctx,t,"Id"));

		// repro SPR-10994
		assertEquals("xyZ",rpr.read(ctx,t,"xyZ").getValue());
		assertTrue(rpr.canRead(ctx,t,"xyZ"));
		assertEquals("xY",rpr.read(ctx,t,"xY").getValue());
		assertTrue(rpr.canRead(ctx,t,"xY"));

		// SPR-10122, ReflectivePropertyAccessor JavaBean property names compliance tests - setters
		rpr.write(ctx, t, "pEBS","Test String");
		assertEquals("Test String",rpr.read(ctx,t,"pEBS").getValue());
	}

	@Test
	public void testOptimalReflectivePropertyResolver() throws Exception {
		ReflectivePropertyAccessor rpr = new ReflectivePropertyAccessor();
		Tester t = new Tester();
		t.setProperty("hello");
		EvaluationContext ctx = new StandardEvaluationContext(t);
//		assertTrue(rpr.canRead(ctx, t, "property"));
//		assertEquals("hello",rpr.read(ctx, t, "property").getValue());
//		assertEquals("hello",rpr.read(ctx, t, "property").getValue()); // cached accessor used

		PropertyAccessor optA = rpr.createOptimalAccessor(ctx, t, "property");
		assertTrue(optA.canRead(ctx, t, "property"));
		assertFalse(optA.canRead(ctx, t, "property2"));
		try {
			optA.canWrite(ctx, t, "property");
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		try {
			optA.canWrite(ctx, t, "property2");
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		assertEquals("hello",optA.read(ctx, t, "property").getValue());
		assertEquals("hello",optA.read(ctx, t, "property").getValue()); // cached accessor used

		try {
			optA.getSpecificTargetClasses();
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		try {
			optA.write(ctx,t,"property",null);
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}

		optA = rpr.createOptimalAccessor(ctx, t, "field");
		assertTrue(optA.canRead(ctx, t, "field"));
		assertFalse(optA.canRead(ctx, t, "field2"));
		try {
			optA.canWrite(ctx, t, "field");
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		try {
			optA.canWrite(ctx, t, "field2");
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		assertEquals(3,optA.read(ctx, t, "field").getValue());
		assertEquals(3,optA.read(ctx, t, "field").getValue()); // cached accessor used

		try {
			optA.getSpecificTargetClasses();
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
		try {
			optA.write(ctx,t,"field",null);
			fail();
		}
		catch (UnsupportedOperationException uoe) {
			// success
		}
	}


	// test classes
	static class Tester {
		String property;
		public int field = 3;
		public int field2;
		public int field3 = 0;
		String property2;
		String property3 = "doodoo";
		boolean property4 = false;
		String iD = "iD";
		String id = "id";
		String ID = "ID";
		String pEBS = "pEBS";
		String xY = "xY";
		String xyZ = "xyZ";

		public String getProperty() { return property; }
		public void setProperty(String value) { property = value; }

		public void setProperty2(String value) { property2 = value; }

		public String getProperty3() { return property3; }

		public boolean isProperty4() { return property4; }

		public String getiD() { return iD; }

		public String getId() { return id; }

		public String getID() { return ID; }

		public String getXY() { return xY; }

		public String getXyZ() { return xyZ; }

		public String getpEBS() {
			return pEBS;
		}

		public void setpEBS(String pEBS) {
			this.pEBS = pEBS;
		}
	}

	static class Super {
	}

	static class Sub extends Super {
	}

	static class Unconvertable {}

	// ---

	/**
	 * Used to validate the match returned from a compareArguments call.
	 */
	private void checkMatch(Class<?>[] inputTypes, Class<?>[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) {
		ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArguments(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter);
		if (expectedMatchKind == null) {
			assertNull("Did not expect them to match in any way", matchInfo);
		}
		else {
			assertNotNull("Should not be a null match", matchInfo);
		}

		if (expectedMatchKind == ArgumentsMatchKind.EXACT) {
			assertTrue(matchInfo.isExactMatch());
		}
		else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) {
			assertTrue(matchInfo.isCloseMatch());
		}
		else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) {
			assertTrue("expected to be a match requiring conversion, but was " + matchInfo, matchInfo.isMatchRequiringConversion());
		}
	}

	/**
	 * Used to validate the match returned from a compareArguments call.
	 */
	private void checkMatch2(Class<?>[] inputTypes, Class<?>[] expectedTypes, StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) {
		ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.compareArgumentsVarargs(getTypeDescriptors(expectedTypes), getTypeDescriptors(inputTypes), typeConverter);
		if (expectedMatchKind == null) {
			assertNull("Did not expect them to match in any way: " + matchInfo, matchInfo);
		}
		else {
			assertNotNull("Should not be a null match", matchInfo);
		}

		if (expectedMatchKind == ArgumentsMatchKind.EXACT) {
			assertTrue(matchInfo.isExactMatch());
		}
		else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) {
			assertTrue(matchInfo.isCloseMatch());
		}
		else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) {
			assertTrue("expected to be a match requiring conversion, but was " + matchInfo, matchInfo.isMatchRequiringConversion());
		}
	}

	private void checkArguments(Object[] args, Object... expected) {
		assertEquals(expected.length,args.length);
		for (int i = 0; i < expected.length; i++) {
			checkArgument(expected[i],args[i]);
		}
	}

	private void checkArgument(Object expected, Object actual) {
		assertEquals(expected,actual);
	}

	private List<TypeDescriptor> getTypeDescriptors(Class<?>... types) {
		List<TypeDescriptor> typeDescriptors = new ArrayList<TypeDescriptor>(types.length);
		for (Class<?> type : types) {
			typeDescriptors.add(TypeDescriptor.valueOf(type));
		}
		return typeDescriptors;
	}


	public interface TestInterface {

		void oneArg(String arg1);

		void twoArg(String arg1, String... arg2);
	}

}
