/*
 * Copyright 2013-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.data.mongodb.core.aggregation;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.data.mongodb.core.Person;

/**
 * Unit tests for {@link SpelExpressionTransformer}.
 * 
 * @see DATAMONGO-774
 * @author Thomas Darimont
 * @author Oliver Gierke
 */
public class SpelExpressionTransformerUnitTests {

	SpelExpressionTransformer transformer = new SpelExpressionTransformer();

	Data data;

	@Before
	public void setup() {

		this.data = new Data();
		this.data.primitiveLongValue = 42;
		this.data.primitiveDoubleValue = 1.2345;
		this.data.doubleValue = 23.0;
		this.data.item = new DataItem();
		this.data.item.primitiveIntValue = 21;
	}

	@Test
	public void shouldRenderConstantExpression() {

		assertThat(transform("1"), is("1"));
		assertThat(transform("-1"), is("-1"));
		assertThat(transform("1.0"), is("1.0"));
		assertThat(transform("-1.0"), is("-1.0"));
		assertThat(transform("null"), is(nullValue()));
	}

	@Test
	public void shouldSupportKnownOperands() {

		assertThat(transform("a + b"), is("{ \"$add\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("a - b"), is("{ \"$subtract\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("a * b"), is("{ \"$multiply\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("a / b"), is("{ \"$divide\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("a % b"), is("{ \"$mod\" : [ \"$a\" , \"$b\"]}"));
	}

	@Test(expected = IllegalArgumentException.class)
	public void shouldThrowExceptionOnUnknownOperand() {
		transform("a ^ 1");
	}

	@Test
	public void shouldRenderSumExpression() {
		assertThat(transform("a + 1"), is("{ \"$add\" : [ \"$a\" , 1]}"));
	}

	@Test
	public void shouldRenderFormula() {

		assertThat(
				transform("(netPrice + surCharge) * taxrate + 42"),
				is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}"));
	}

	@Test
	public void shouldRenderFormulaInCurlyBrackets() {

		assertThat(
				transform("{(netPrice + surCharge) * taxrate + 42}"),
				is("{ \"$add\" : [ { \"$multiply\" : [ { \"$add\" : [ \"$netPrice\" , \"$surCharge\"]} , \"$taxrate\"]} , 42]}"));
	}

	@Test
	public void shouldRenderFieldReference() {

		assertThat(transform("foo"), is("$foo"));
		assertThat(transform("$foo"), is("$foo"));
	}

	@Test
	public void shouldRenderNestedFieldReference() {

		assertThat(transform("foo.bar"), is("$foo.bar"));
		assertThat(transform("$foo.bar"), is("$foo.bar"));
	}

	@Test
	@Ignore
	public void shouldRenderNestedIndexedFieldReference() {

		// TODO add support for rendering nested indexed field references
		assertThat(transform("foo[3].bar"), is("$foo[3].bar"));
	}

	@Test
	public void shouldRenderConsecutiveOperation() {
		assertThat(transform("1 + 1 + 1"), is("{ \"$add\" : [ 1 , 1 , 1]}"));
	}

	@Test
	public void shouldRenderComplexExpression0() {

		assertThat(transform("-(1 + q)"), is("{ \"$multiply\" : [ -1 , { \"$add\" : [ 1 , \"$q\"]}]}"));
	}

	@Test
	public void shouldRenderComplexExpression1() {

		assertThat(transform("1 + (q + 1) / (q - 1)"),
				is("{ \"$add\" : [ 1 , { \"$divide\" : [ { \"$add\" : [ \"$q\" , 1]} , { \"$subtract\" : [ \"$q\" , 1]}]}]}"));
	}

	@Test
	public void shouldRenderComplexExpression2() {

		assertThat(
				transform("(q + 1 + 4 - 5) / (q + 1 + 3 + 4)"),
				is("{ \"$divide\" : [ { \"$subtract\" : [ { \"$add\" : [ \"$q\" , 1 , 4]} , 5]} , { \"$add\" : [ \"$q\" , 1 , 3 , 4]}]}"));
	}

	@Test
	public void shouldRenderBinaryExpressionWithMixedSignsCorrectly() {

		assertThat(transform("-4 + 1"), is("{ \"$add\" : [ -4 , 1]}"));
		assertThat(transform("1 + -4"), is("{ \"$add\" : [ 1 , -4]}"));
	}

	@Test
	public void shouldRenderConsecutiveOperationsInComplexExpression() {

		assertThat(transform("1 + 1 + (1 + 1 + 1) / q"),
				is("{ \"$add\" : [ 1 , 1 , { \"$divide\" : [ { \"$add\" : [ 1 , 1 , 1]} , \"$q\"]}]}"));
	}

	@Test
	public void shouldRenderParameterExpressionResults() {
		assertThat(transform("[0] + [1] + [2]", 1, 2, 3), is("{ \"$add\" : [ 1 , 2 , 3]}"));
	}

	@Test
	public void shouldRenderNestedParameterExpressionResults() {

		assertThat(transform("[0].primitiveLongValue + [0].primitiveDoubleValue + [0].doubleValue.longValue()", data),
				is("{ \"$add\" : [ 42 , 1.2345 , 23]}"));
	}

	@Test
	public void shouldRenderNestedParameterExpressionResultsInNestedExpressions() {

		assertThat(
				transform("((1 + [0].primitiveLongValue) + [0].primitiveDoubleValue) * [0].doubleValue.longValue()", data),
				is("{ \"$multiply\" : [ { \"$add\" : [ 1 , 42 , 1.2345]} , 23]}"));
	}

	/**
	 * @see DATAMONGO-840
	 */
	@Test
	public void shouldRenderCompoundExpressionsWithIndexerAndFieldReference() {

		Person person = new Person();
		person.setAge(10);
		assertThat(transform("[0].age + a.c", person), is("{ \"$add\" : [ 10 , \"$a.c\"]}"));
	}

	/**
	 * @see DATAMONGO-840
	 */
	@Test
	public void shouldRenderCompoundExpressionsWithOnlyFieldReferences() {

		assertThat(transform("a.b + a.c"), is("{ \"$add\" : [ \"$a.b\" , \"$a.c\"]}"));
	}

	@Test
	public void shouldRenderStringFunctions() {

		assertThat(transform("concat(a, b)"), is("{ \"$concat\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("substr(a, 1, 2)"), is("{ \"$substr\" : [ \"$a\" , 1 , 2]}"));
		assertThat(transform("strcasecmp(a, b)"), is("{ \"$strcasecmp\" : [ \"$a\" , \"$b\"]}"));
		assertThat(transform("toLower(a)"), is("{ \"$toLower\" : [ \"$a\"]}"));
		assertThat(transform("toUpper(a)"), is("{ \"$toUpper\" : [ \"$a\"]}"));
		assertThat(transform("toUpper(toLower(a))"), is("{ \"$toUpper\" : [ { \"$toLower\" : [ \"$a\"]}]}"));
	}

	private String transform(String expression, Object... params) {
		Object result = transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params);
		return result == null ? null : result.toString();
	}
}
