/*
 * Copyright 2002-2012 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.integration.xquery.core;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.xquery.XQDataSource;
import javax.xml.xquery.XQResultSequence;

import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.integration.Message;
import org.springframework.integration.MessagingException;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.integration.xquery.support.AbstractXQueryResultMapper;
import org.springframework.integration.xquery.support.BooleanResultMapper;
import org.springframework.integration.xquery.support.NodeResultMapper;
import org.springframework.integration.xquery.support.NumberResultMapper;
import org.springframework.integration.xquery.support.StringResultMapper;
import org.springframework.integration.xquery.support.XQueryParameter;
import org.springframework.integration.xquery.support.XQueryResultMapper;
import org.w3c.dom.Node;

/**
 * The base test class for the XQueryExecutor tests
 *
 * @author Amol Nayak
 *
 * @since 1.0
 *
 */
public abstract class AbstractXQueryExecutorTests {


	private final static String EXT_VARIABLE_XQUERY =
		"declare variable $name as xs:string external;" +
		"declare variable $class as xs:int external;" +
		" for 	$student in /mappings/students/student," +
		"		$subject in /mappings/subjects/subject " +
		"where	$student/@id = $subject/students/studentId " +
		"and	$student/name = $name " +
		"and	$student/class = $class " +
		"return $subject/name/text()";

	private final String xmlString = "<person active=\"true\">" +
											"<name>Mike</name>" +
											"<age>29</age>" +
										"</person>";


	/**
	 * Test if default created result mappers are appropriately instantiated
	 */
	@Test
	public void initializeDefaultMappers() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		@SuppressWarnings("rawtypes")
		Map executorMap = TestUtils.getPropertyValue(executor, "resultMappers", Map.class);
		Assert.assertNotNull(executorMap);
		Assert.assertEquals(4, executorMap.size());
		Assert.assertEquals(StringResultMapper.class, executorMap.get(String.class).getClass());
		Assert.assertEquals(BooleanResultMapper.class, executorMap.get(Boolean.class).getClass());
		Assert.assertEquals(NumberResultMapper.class, executorMap.get(Number.class).getClass());
		Assert.assertEquals(NodeResultMapper.class, executorMap.get(Node.class).getClass());
	}

	/**
	 * Tests of the registered mappers take priority over the defaults
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Test
	public void registerCustomMappers() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello'");
		Map map = new HashMap();
		map.put(MyCustomClass.class, new XQueryResultMapper<MyCustomClass>() {
			public List<MyCustomClass> mapResults(XQResultSequence result) {
				// TODO Auto-generated method stub
				return null;
			}
		});
		map.put(Node.class, new CustomNodeMapper());
		executor.setResultMappers(map);
		executor.afterPropertiesSet();
		Map executorMap = TestUtils.getPropertyValue(executor, "resultMappers", Map.class);
		Assert.assertNotNull(executorMap);
		Assert.assertEquals(5, executorMap.size());
		Assert.assertEquals(StringResultMapper.class, executorMap.get(String.class).getClass());
		Assert.assertEquals(BooleanResultMapper.class, executorMap.get(Boolean.class).getClass());
		Assert.assertEquals(NumberResultMapper.class, executorMap.get(Number.class).getClass());
		Assert.assertEquals(CustomNodeMapper.class, executorMap.get(Node.class).getClass());
	}


	/**
	 * Tries to execute the result passing an unmapped class
	 */
	@Test
	public void executeForUnknownClassType() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		try {
			executor.execute(MessageBuilder.withPayload("").build(), MyCustomClass.class);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("No Result mapper found for the type org.springframework.integration.xquery.core.AbstractXQueryExecutorTests$MyCustomClass",
					e.getMessage());
		}
	}

	/**
	 * @return
	 */
	protected abstract XQueryExecutor getExecutor();

	/**
	 * Test with null message
	 */
	@Test
	public void withNullMessage() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		try {
			executor.execute(null, String.class);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Non null message expected", e.getMessage());
		}

	}

	@Test
	public void executeWithStringForNonLeaf() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery(".//person");
		executor.afterPropertiesSet();
		List<String> names = executor.executeForString(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
	}


	@Test
	public void executeWithBooleanForNonLeaf() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//person");
		executor.afterPropertiesSet();
		List<Boolean> names = executor.executeForBoolean(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
		Assert.assertTrue(names.contains(Boolean.FALSE));
	}


	@Test
	public void executeWithNumberForNonLeaf() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//person");
		executor.afterPropertiesSet();
		@SuppressWarnings("unused")
		List<Number> names;
		try {
			names = executor.executeForNumber(MessageBuilder.withPayload(xmlString).build());
		} catch (MessagingException e) {
			Assert.assertEquals(NumberFormatException.class,e.getCause().getClass());
		}
	}


	/**
	 * Test case to execute the XQuery and return a {@link List} of {@link String}
	 */
	@Test
	public void executeWithString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery(".//person/name/text()");
		executor.afterPropertiesSet();
		List<String> names = executor.executeForString(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
		Assert.assertTrue(names.contains("Mike"));
	}


	/**
	 * Test case to execute the XQuery and return a {@link List} of {@link Boolean}
	 */
	@Test
	public void executeWithBoolean() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//person/@active");
		executor.afterPropertiesSet();
		List<Boolean> names = executor.executeForBoolean(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
		Assert.assertTrue(names.contains(Boolean.TRUE));
	}


	/**
	 * Test case to execute the XQuery and return a {@link List} of {@link Number}
	 */
	@Test
	public void executeWithNumber() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//person/age/text()");
		executor.afterPropertiesSet();
		List<Number> names = executor.executeForNumber(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
		Assert.assertTrue(names.contains(Long.valueOf(29)));
	}


	/**
	 * Test case to execute the XQuery and return a {@link List} of {@link Node}
	 */
	@Test
	public void executeWithNode() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//person");
		executor.afterPropertiesSet();
		List<Node> names = executor.executeForNode(MessageBuilder.withPayload(xmlString).build());
		Assert.assertNotNull(names);
		Assert.assertEquals(1, names.size());
		Assert.assertEquals("person",names.get(0).getLocalName());
	}


	/**
	 * Test with null message
	 */
	@Test
	public void withNullType() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		try {
			executor.execute(MessageBuilder.withPayload("").build(),(Class<?>) null);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Non null type expected", e.getMessage());
		}

	}


	/**
	 * Executes the case where the router has no parameters but the query expects parameters
	 */
	@Test
	public void noParametersProvided() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery(EXT_VARIABLE_XQUERY);
		try {
			executor.afterPropertiesSet();
		} catch (MessagingException e) {
			Assert.assertEquals("Expecting 2 parameters in the xquery, but none provided to the router", e.getMessage());
		}
	}

	/**
	 * Execute the case where parameters are provided, but some or all of the parameters are missing
	 */
	@Test
	public void fewParametersProvided() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery(EXT_VARIABLE_XQUERY);
		executor.addXQueryParameter(new XQueryParameter("class",Integer.valueOf(3)));
		try {
			executor.afterPropertiesSet();
		} catch (MessagingException e) {
			Assert.assertEquals("Missing parameter(s) [$name]", e.getMessage());
		}
	}

	/**
	 * Sets null XQuery, should get an exception
	 */
	@Test
	public void setNullXQuery() {
		XQueryExecutor executor = getExecutor();
		try {
			executor.setXQuery(null);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Provide a non null XQuery", e.getMessage());
		}
	}


	/**
	 * Sets the XQuery file resource and then the xquery
	 */
	@Test
	public void setResourceAndXQuery() {
		XQueryExecutor executor = getExecutor();
		Resource res = new ClassPathResource("/org/springframework/integration/xquery/XQuery.xq");
		Assert.assertTrue(res.exists());
		executor.setXQueryFileResource(res);
		try {
			executor.setXQuery("'Hello World'");
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Only one of XQuery resource file or XQuery may be specified", e.getMessage());
		}
		Assert.assertNull(TestUtils.getPropertyValue(executor, "xQuery"));
		executor.addXQueryParameter(new XQueryParameter("name", "headers['studentName']"));
		executor.addXQueryParameter(new XQueryParameter("class", Integer.valueOf(1)));
		executor.afterPropertiesSet();
		Assert.assertNotNull(TestUtils.getPropertyValue(executor, "xQuery",String.class));
	}

	/**
	 * Initialize the router without setting the xquery
	 */
	@Test
	public void withoutXQuery() {
		XQueryExecutor executor = getExecutor();
		try {
			executor.afterPropertiesSet();
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("One of XQuery or the XQuery resource is mandatory", e.getMessage());
		}
	}

	/**
	 *Sets the XQuery and then the resource
	 */
	@Test
	public void setXQueryAndResource() {
		XQueryExecutor executor = getExecutor();
		Resource res = new ClassPathResource("org/springframework/integration/xquery/XQuery.xq");
		Assert.assertTrue(res.exists());
		executor.setXQuery("'Hello World'");
		try {
			executor.setXQueryFileResource(res);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Only one of XQuery resource file or XQuery may be specified", e.getMessage());
		}
	}

	/**
	 *Sets a null {@link XQDataSource} instance
	 */
	@Test
	public void setNullXQataSource() {
		XQueryExecutor executor = getExecutor();
		try {
			executor.setXQDataSource(null);
		} catch (IllegalArgumentException e) {
			Assert.assertEquals("Provide a non null instance of the XQDatasource", e.getMessage());
		}
	}


	/**
	 * Tests the router with valid xml payload. The values returned will be the
	 * text content of the node, if the node is a text or attribute type
	 */
	@Test
	public void withValidPayload() {
		withValidPayloadInternal(true);
	}

	/**
	 * Tests the router with valid xml payload. The values returned will be the
	 * {@link Node}, irrespective of the type of the node
	 */
	@Test
	public void withValidPayload2() {
		withValidPayloadInternal(false);
	}

	/**
	 *
	 */
	private void withValidPayloadInternal(boolean extractNodeText) {
		File file = new File("./src/test/resources/org/springframework/integration/xquery/SubjectMapping.xml");
		Message<File> message = MessageBuilder.withPayload(file)
									.setHeader("studentName", "Jughead")
									.build();
		XQueryExecutor executor = getExecutor();
		executor.setXQuery(EXT_VARIABLE_XQUERY);
		executor.addXQueryParameter(new XQueryParameter("name", "headers['studentName']"));
		executor.addXQueryParameter(new XQueryParameter("class", Integer.valueOf(1)));
		executor.afterPropertiesSet();
		List<Object> subjects;
		if(extractNodeText) {
			subjects = new ArrayList<Object>(executor.executeForString(message));
		}
		else {
			subjects = new ArrayList<Object>(executor.executeForNode(message));
		}
		Assert.assertNotNull(subjects);
		Assert.assertEquals(2, subjects.size());
		if(extractNodeText) {
			Assert.assertEquals(String.class, subjects.get(0).getClass());
			Assert.assertTrue(subjects.contains("Cooking"));
			Assert.assertTrue(subjects.contains("Math"));
		}
		else {
			Assert.assertTrue(Node.class.isAssignableFrom(subjects.get(0).getClass()));
		}
	}

	/**
	 * Tests with a query that will generate a numeric value and we get the result as {@link Number}
	 */
	@Test
	public void withNumericExpression() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("2 + 2");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.get(0).intValue() == 4);

	}

	/**
	 *Tests a query that will generate a decimal value and we get the result as a {@link Number}
	 */
	@Test
	public void withDecimalExpression() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("2.0 + 2.1");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Double.valueOf(4.1)));

	}

	/**
	 * Execute the XQuery that gives a boolean value and get the value as a Boolean
	 */
	@Test
	public void withBooleanResult() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("1 = 1");
		executor.afterPropertiesSet();
		List<Boolean> result = executor.executeForBoolean(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Boolean.TRUE));
	}

	/**
	 *The XQuery gives a string value and we want the result to be a string value
	 */
	@Test
	public void getTextString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		List<String> result = executor.executeForString(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains("Hello World"));
	}

	/**
	 *The XQuery gives a boolean value and we want the result to be a string value
	 */
	@Test
	public void getBooleanAsTextString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("1 = 2");
		executor.afterPropertiesSet();
		List<String> result = executor.executeForString(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains("false"));
	}

	/**
	 *The XQuery gives a text value in the node and we want the result to be a string value
	 */
	@Test
	public void getTextNodeAsString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//test/text()");
		executor.afterPropertiesSet();
		List<String> result = executor.executeForString(MessageBuilder.withPayload("<test>Some Value</test>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains("Some Value"));
	}

	/**
	 *The XQuery gives a text value in the node and we want the result to be a Boolean
	 */
	@Test
	public void getTextNodeAsBoolean() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//test/text()");
		executor.afterPropertiesSet();
		List<Boolean> result = executor.executeForBoolean(MessageBuilder.withPayload("<test>Some Value</test>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Boolean.FALSE));
	}

	/**
	 *The XQuery gives a text value in the node and we want the result to be a Long
	 */
	@Test
	public void getTextNodeAsLong() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//number/text()");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<number>1</number>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Long.valueOf(1)));
	}

	/**
	 *The XQuery gives a text value in the node and we want the result to be a Long
	 */
	@Test
	public void getTextNodeAsDouble() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("//number/text()");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<number>1.1</number>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Double.valueOf(1.1)));
	}



	/**
	 * Tests by using a String that is a non decimal number and we get the value as a {@link Number}
	 */
	@Test
	public void withNonDecimalStringExpression() {
		XQueryExecutor executor = getExecutor();
		executor.setResultMappers(Collections.singletonMap(Number.class,
				(XQueryResultMapper<Number>)new NumberResultMapper()));
		executor.setXQuery("'4'");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Long.valueOf(4)));
	}

	/**
	 * Tests by using a String that is a decimal number and we get the value as a {@link Number}
	 */
	@Test
	public void withDecimalStringExpression() {
		XQueryExecutor executor = getExecutor();
		executor.setResultMappers(Collections.singletonMap(Number.class,
				(XQueryResultMapper<Number>)new NumberResultMapper()));
		executor.setXQuery("'4.1'");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Double.valueOf(4.1)));
	}

	/**
	 * Tests by getting the Integer value and getting the result as {@link String}
	 */
	@Test
	public void getIntegerAsString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("4");
		executor.afterPropertiesSet();
		List<String> result = executor.executeForString(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains("4"));
	}



	/**
	 * Uses a decimal value and gets the result as {@link String}
	 */
	@Test
	public void getDecimalAsString() {
		XQueryExecutor executor = getExecutor();
		executor.setXQuery("4.1");
		executor.afterPropertiesSet();
		List<String> result = executor.executeForString(MessageBuilder.withPayload("<test/>").build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains("4.1"));
	}

//	@Test
//	public void getCurrentDate() {
//		XQueryExecutor executor = new XQueryExecutor();
//		executor.setXQuery("current-date()");
//		executor.afterPropertiesSet();
//		executor.executeForString(MessageBuilder.withPayload("<test/>").build());
//	}


	@Test
	public void withStringExpression2() {
		XQueryExecutor executor = getExecutor();
		executor.setResultMappers(Collections.singletonMap(Number.class,
				(XQueryResultMapper<Number>)new NumberResultMapper()));
		executor.setXQuery("/person/age/text()");
		executor.afterPropertiesSet();
		List<Number> result = executor.executeForNumber(MessageBuilder.withPayload(xmlString).build());
		Assert.assertEquals(1, result.size());
		Assert.assertTrue(result.contains(Long.valueOf(29)));
	}

	@Test
	public void withFormatingFlag() {
		XQueryExecutor executor = getExecutor();
		executor.setFormatOutput(true);
		executor.setXQuery("'Hello World'");
		executor.afterPropertiesSet();
		//should not fail
		executor.executeForString(MessageBuilder.withPayload("<dummy/>").build());
		@SuppressWarnings("unchecked")
		Map<Class<Object>, XQueryResultMapper<Object>>  mappers = TestUtils.getPropertyValue(executor, "resultMappers", Map.class);
		for(Object val:mappers.values()) {
			if(val instanceof AbstractXQueryResultMapper) {
				Assert.assertEquals(true,
						TestUtils.getPropertyValue(val, "formatOutput", Boolean.class).booleanValue());
			}
		}

	}



	private class MyCustomClass {}
	private class CustomNodeMapper implements XQueryResultMapper<Node> {
		public List<Node> mapResults(XQResultSequence result) {
			return null;
		}

	}
}
