/*****************************************************************************
 * Copyright (c) 2015, 2020 CEA LIST.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *      Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
 *
 *****************************************************************************/
package org.eclipse.nebula.widgets.nattable.formula;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.eclipse.nebula.widgets.nattable.coordinate.IndexCoordinate;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionException;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionValue;
import org.eclipse.nebula.widgets.nattable.formula.function.ProductFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.SumFunction;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class FormulaParserTest {

    private static Locale defaultLocale;

    @BeforeClass
    public static void setup() {
        defaultLocale = Locale.getDefault();
        Locale.setDefault(Locale.ENGLISH);
    }

    @AfterClass
    public static void tearDown() {
        Locale.setDefault(defaultLocale);
    }

    IDataProvider dataProvider = new TwoDimensionalArrayDataProvider(new Object[10][10]);
    FormulaParser parser = new FormulaParser(this.dataProvider);

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnMissingOperator() {
        this.parser.parseFunction("5 3");
    }

    @Test
    public void shouldParseNativeSumFunctionString() {
        FunctionValue result = this.parser.parseFunction("5 + 3");
        assertEquals(new BigDecimal(5 + 3), result.getValue());

        result = this.parser.parseFunction("5+3");
        assertEquals(new BigDecimal(5 + 3), result.getValue());

        result = this.parser.parseFunction("5+3+15");
        assertEquals(new BigDecimal(5 + 3 + 15), result.getValue());

        result = this.parser.parseFunction("5+3+5+3");
        assertEquals(new BigDecimal(5 + 3 + 5 + 3), result.getValue());
    }

    @Test
    public void shouldParseNativeSumWithDecimals() {
        FunctionValue result = this.parser.parseFunction("5.4 + 3.2");
        assertEquals(new BigDecimal("8.6"), result.getValue());
    }

    @Test
    public void shouldParseNativeSubtractFunctionString() {
        FunctionValue result = this.parser.parseFunction("5 - 3");
        assertEquals(new BigDecimal(5 - 3), result.getValue());

        result = this.parser.parseFunction("15 - 3 -2");
        assertEquals(new BigDecimal(15 - 3 - 2), result.getValue());

        result = this.parser.parseFunction("25 - 3-15-10");
        assertEquals(new BigDecimal(25 - 3 - 15 - 10), result.getValue());

        result = this.parser.parseFunction("-42");
        assertEquals(new BigDecimal(-42), result.getValue());

        result = this.parser.parseFunction("-10-5");
        assertEquals(new BigDecimal(-10 - 5), result.getValue());
    }

    @Test
    public void shouldParseNativeSumAndSubtractionFunctionString() {
        FunctionValue result = this.parser.parseFunction("- 5 + 3");
        assertEquals(new BigDecimal(-5 + 3), result.getValue());

        result = this.parser.parseFunction("15 - 3 +2");
        assertEquals(new BigDecimal(15 - 3 + 2), result.getValue());

        result = this.parser.parseFunction("-25 + 5-15");
        assertEquals(new BigDecimal(-25 + 5 - 15), result.getValue());

        result = this.parser.parseFunction("40+10-25-5");
        assertEquals(new BigDecimal(40 + 10 - 25 - 5), result.getValue());
    }

    @Test
    public void shouldParseNativeProductFunctionString() {
        FunctionValue result = this.parser.parseFunction("5 * 3");
        assertEquals(new BigDecimal(5 * 3), result.getValue());

        result = this.parser.parseFunction("5 * 3 *2");
        assertEquals(new BigDecimal(5 * 3 * 2), result.getValue());

        result = this.parser.parseFunction("5 * 3*20 * 7");
        assertEquals(new BigDecimal(5 * 3 * 20 * 7), result.getValue());

        result = this.parser.parseFunction("5 * -3");
        assertEquals(new BigDecimal(5 * -3), result.getValue());

        result = this.parser.parseFunction("-5 * 3");
        assertEquals(new BigDecimal(-5 * 3), result.getValue());

        result = this.parser.parseFunction("5 * -3 * 7");
        assertEquals(new BigDecimal(5 * -3 * 7), result.getValue());
    }

    @Test
    public void shouldParseNativeDifferenceFunctionString() {
        FunctionValue result = this.parser.parseFunction("15 / 3");
        assertEquals(new BigDecimal(15 / 3), result.getValue());

        result = this.parser.parseFunction("20 / 2 /5");
        assertEquals(new BigDecimal(20 / 2 / 5), result.getValue());

        result = this.parser.parseFunction("5 / 2");
        assertEquals(BigDecimal.valueOf(5d / 2), result.getValue());

        result = this.parser.parseFunction("100 / 4 / 5 / 2");
        assertEquals(BigDecimal.valueOf(100d / 4 / 5 / 2), result.getValue());

        result = this.parser.parseFunction("-15 / 3");
        assertEquals(new BigDecimal(-15 / 3), result.getValue());

        result = this.parser.parseFunction("-5 / 3");
        assertEquals(new BigDecimal(-5).divide(new BigDecimal(3), 9, RoundingMode.HALF_UP), result.getValue());

        result = this.parser.parseFunction("5 / -3");
        assertEquals(new BigDecimal(5).divide(new BigDecimal(-3), 9, RoundingMode.HALF_UP), result.getValue());
    }

    @Test
    public void shouldParseMixedNativeFunction() {
        FunctionValue result = this.parser.parseFunction("5 + 3 * 2");
        assertEquals(new BigDecimal(5 + 3 * 2), result.getValue());
        assertEquals(new BigDecimal(11), result.getValue());

        result = this.parser.parseFunction("5 * 3 + 2");
        assertEquals(new BigDecimal(5 * 3 + 2), result.getValue());
        assertEquals(new BigDecimal(5 * 3 + 2), result.getValue());

        result = this.parser.parseFunction("15/3*6-4+3");
        assertEquals(new BigDecimal(15 / 3 * 6 - 4 + 3), result.getValue());
        assertEquals(new BigDecimal(29), result.getValue());

        result = this.parser.parseFunction("2*5+10/4-3");
        assertEquals(BigDecimal.valueOf(2 * 5 + 10d / 4 - 3), result.getValue());
        assertEquals(BigDecimal.valueOf(9.5), result.getValue());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnclosedParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("(5+3", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnopenedParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("5+3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test
    public void shouldReplaceOneParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processParenthesis("(5+3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("10 + (5+3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("10 + {0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("(5+3) + 42", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0} + 42", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnclosedTwoParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("(5+3) + (7 - 2", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnopenedTwoParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("(5+3) + 7 - 3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test
    public void shouldReplaceTwoParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processParenthesis("(5+3) - (5+5)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0} - {1}", processed);
        assertEquals(2, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertTrue(replacements.get(1) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());
        assertEquals(new BigDecimal(5 + 5), replacements.get(1).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("(5+3) + 10 + (5+5)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0} + 10 + {1}", processed);
        assertEquals(2, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertTrue(replacements.get(1) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());
        assertEquals(new BigDecimal(5 + 5), replacements.get(1).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("10 + (5+3) + (5+5) + 42", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("10 + {0} + {1} + 42", processed);
        assertEquals(2, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertTrue(replacements.get(1) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());
        assertEquals(new BigDecimal(5 + 5), replacements.get(1).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("10 + (5+3) + 42 + (5+5)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("10 + {0} + 42 + {1}", processed);
        assertEquals(2, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertTrue(replacements.get(1) instanceof SumFunction);
        assertEquals(new BigDecimal(5 + 3), replacements.get(0).getValue());
        assertEquals(new BigDecimal(5 + 5), replacements.get(1).getValue());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnclosedNestedParanthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("((5+3 - 3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnUnopenedNestedParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processParenthesis("(5+3) + 7 - 3)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test
    public void shouldReplaceNestedParenthesis() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processParenthesis("((5+3)*2)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof ProductFunction);
        assertEquals(new BigDecimal(((5 + 3) * 2)), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("(2*(5+3))", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof ProductFunction);
        assertEquals(new BigDecimal((2 * (5 + 3))), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processParenthesis("5+(2*(5+3))", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("5+{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof ProductFunction);
        assertEquals(new BigDecimal((2 * (5 + 3))), replacements.get(0).getValue());
    }

    @Test
    public void shouldMatchPlaceholderRegex() {
        assertTrue("{0}".matches(FormulaParser.PLACEHOLDER_REGEX));
        assertTrue("{1}".matches(FormulaParser.PLACEHOLDER_REGEX));
        assertTrue("{10}".matches(FormulaParser.PLACEHOLDER_REGEX));
        assertTrue("{42}".matches(FormulaParser.PLACEHOLDER_REGEX));
        assertTrue("{100}".matches(FormulaParser.PLACEHOLDER_REGEX));
    }

    @Test
    public void shouldParseComplexNativeFunctionString() {
        FunctionValue result = this.parser.parseFunction("5+(2*(5+3))");
        assertEquals(new BigDecimal(5 + (2 * (5 + 3))), result.getValue());

        result = this.parser.parseFunction("(5 + 3) * 2");
        assertEquals(new BigDecimal((5 + 3) * 2), result.getValue());
        assertEquals(new BigDecimal(16), result.getValue());

        result = this.parser.parseFunction("(5 + 3) * 2");
        assertEquals(new BigDecimal((5 + 3) * 2), result.getValue());
        assertEquals(new BigDecimal(16), result.getValue());

        result = this.parser.parseFunction("30/(3+2)-2*3");
        assertEquals(new BigDecimal(30 / (3 + 2) - 2 * 3), result.getValue());
        assertEquals(new BigDecimal(0), result.getValue());
    }

    @Test
    public void shouldMatchOperatorRegex() {
        assertTrue("+".matches(FormulaParser.OPERATOR_REGEX));
        assertTrue("-".matches(FormulaParser.OPERATOR_REGEX));
        assertTrue("*".matches(FormulaParser.OPERATOR_REGEX));
        assertTrue("/".matches(FormulaParser.OPERATOR_REGEX));
        assertTrue("^".matches(FormulaParser.OPERATOR_REGEX));
    }

    @Test
    public void shouldMatchDigiRegex() {
        assertTrue("5".matches(FormulaParser.DIGIT_REGEX));
        assertTrue("15".matches(FormulaParser.DIGIT_REGEX));
        assertTrue("426".matches(FormulaParser.DIGIT_REGEX));
    }

    @Test
    public void shouldMatchLocalizedDigitRegex() {
        assertTrue("5".matches(this.parser.localizedDigitRegex));
        assertTrue("15".matches(this.parser.localizedDigitRegex));
        assertTrue("426".matches(this.parser.localizedDigitRegex));
        assertTrue("1.5".matches(this.parser.localizedDigitRegex));
        assertTrue("42.75".matches(this.parser.localizedDigitRegex));

        this.parser.setDecimalFormat((DecimalFormat) DecimalFormat.getInstance(Locale.GERMAN));
        assertTrue("1,5".matches(this.parser.localizedDigitRegex));
        assertTrue("42,75".matches(this.parser.localizedDigitRegex));

        this.parser.setDecimalFormat((DecimalFormat) DecimalFormat.getInstance());
        assertTrue("1.5".matches(this.parser.localizedDigitRegex));
        assertTrue("42.75".matches(this.parser.localizedDigitRegex));
    }

    @Test
    public void shouldParseNativePowerFunction() {
        FunctionValue result = this.parser.parseFunction("5 ^ 2");
        assertEquals(new BigDecimal(5).pow(2), result.getValue());

        result = this.parser.parseFunction("5+3^2");
        assertEquals(new BigDecimal(14), result.getValue());
    }

    @Test
    public void shouldProcessPowerFirst() {
        FunctionValue result = this.parser.parseFunction("(4 * 8) ^ 2");
        assertEquals(new BigDecimal(1024), result.getValue());

        result = this.parser.parseFunction("4 * 8 ^ 2");
        assertEquals(new BigDecimal(256), result.getValue());
    }

    @Test
    public void shouldMatchFunctionRegex() {
        assertTrue(this.parser.functionPattern.matcher("AVERAGE(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("AVERAGE()").find());
        assertTrue(this.parser.functionPattern.matcher("AVERAGE(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("AVERAGE(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("AVERAGE(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("NEGATE(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("NEGATE()").find());
        assertTrue(this.parser.functionPattern.matcher("NEGATE(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("NEGATE(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("NEGATE(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("POWER(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("POWER()").find());
        assertTrue(this.parser.functionPattern.matcher("POWER(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("POWER(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("POWER(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("PRODUCT(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("PRODUCT()").find());
        assertTrue(this.parser.functionPattern.matcher("PRODUCT(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("PRODUCT(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("PRODUCT(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("QUOTIENT(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("QUOTIENT()").find());
        assertTrue(this.parser.functionPattern.matcher("QUOTIENT(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("QUOTIENT(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("QUOTIENT(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("SQRT(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("SQRT()").find());
        assertTrue(this.parser.functionPattern.matcher("SQRT(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("SQRT(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("SQRT(5; 3; -1)").find());

        assertTrue(this.parser.functionPattern.matcher("SUM(5+3)").find());
        assertTrue(this.parser.functionPattern.matcher("SUM()").find());
        assertTrue(this.parser.functionPattern.matcher("SUM(5; 3)").find());
        assertTrue(this.parser.functionPattern.matcher("SUM(5;3)").find());
        assertTrue(this.parser.functionPattern.matcher("SUM(5; 3; -1)").find());
    }

    @Test
    public void shouldParseFunction() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processFunctions("SUM(5; 3; 2)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal((5 + 3 + 2)), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processFunctions("42 + SUM(5; 3; 2)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("42 + {0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal((5 + 3 + 2)), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processFunctions("SUM(5; 3; 2) - 42", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0} - 42", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal((5 + 3 + 2)), replacements.get(0).getValue());
    }

    @Test
    public void shouldParseMultipleFunction() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processFunctions("SUM(5; 3; 2) - PRODUCT(2; 5)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0} - {1}", processed);
        assertEquals(2, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertTrue(replacements.get(1) instanceof ProductFunction);
        assertEquals(new BigDecimal((5 + 3 + 2)), replacements.get(0).getValue());
        assertEquals(new BigDecimal((2 * 5)), replacements.get(1).getValue());
    }

    @Test
    public void shouldParseNestedFunctions() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        String processed = this.parser.processFunctions("SUM(PRODUCT(2; 5); QUOTIENT(10; 5))", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal((2 * 5) + (10 / 5)), replacements.get(0).getValue());

        replacements = new HashMap<>();
        processed = this.parser.processFunctions("SUM(PRODUCT(2; SUM(2; 2)); 5)", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
        assertEquals("{0}", processed);
        assertEquals(1, replacements.size());
        assertTrue(replacements.get(0) instanceof SumFunction);
        assertEquals(new BigDecimal((2 * (2 + 2)) + 5), replacements.get(0).getValue());
    }

    @Test(expected = IllegalArgumentException.class)
    public void shouldThrowExceptionOnNotClosingFunction() {
        Map<Integer, FunctionValue> replacements = new HashMap<>();
        this.parser.processFunctions("SUM(5; 3; 2", replacements, new HashMap<IndexCoordinate, Set<IndexCoordinate>>(), null);
    }

    @Test
    public void shouldProcessFunction() {
        FunctionValue result = this.parser.parseFunction("SUM(5; 3; 2)");
        assertEquals(new BigDecimal(10), result.getValue());

        result = this.parser.parseFunction("PRODUCT(7; 6)");
        assertEquals(new BigDecimal(42), result.getValue());

        result = this.parser.parseFunction("AVERAGE(7; 6; 13; 14)");
        assertEquals(new BigDecimal(10), result.getValue());

        result = this.parser.parseFunction("QUOTIENT(15; 3)");
        assertEquals(new BigDecimal(5), result.getValue());

        result = this.parser.parseFunction("POWER(5; 3)");
        assertEquals(new BigDecimal(125), result.getValue());

        result = this.parser.parseFunction("NEGATE(42)");
        assertEquals(new BigDecimal(-42), result.getValue());

        result = this.parser.parseFunction("SQRT(144)");
        assertEquals(BigDecimal.valueOf(12d), result.getValue());
    }

    @Test
    public void shouldProcessMultipleFunctions() {
        FunctionValue result = this.parser.parseFunction("SUM(5; 3; 2) - PRODUCT(2; 5)");
        assertEquals(new BigDecimal(0), result.getValue());
    }

    @Test
    public void shouldProcessNestedFunctions() {
        FunctionValue result = this.parser.parseFunction("SUM(PRODUCT(2; 5); QUOTIENT(10; 5))");
        assertEquals(new BigDecimal(12), result.getValue());

        result = this.parser.parseFunction("SUM(PRODUCT(2; SUM(2; 2)); 5)");
        assertEquals(new BigDecimal(13), result.getValue());
    }

    @Test
    public void shouldMatchReferenceRegex() {
        assertTrue("A7".matches(FormulaParser.REFERENCE_REGEX));
        assertTrue("Z9".matches(FormulaParser.REFERENCE_REGEX));
        assertTrue("AZ1".matches(FormulaParser.REFERENCE_REGEX));
        assertTrue("C27".matches(FormulaParser.REFERENCE_REGEX));
        assertTrue("AC420".matches(FormulaParser.REFERENCE_REGEX));
    }

    @Test
    public void shouldNotMatchReferenceRegex() {
        assertFalse("A".matches(FormulaParser.REFERENCE_REGEX));
        assertFalse("9".matches(FormulaParser.REFERENCE_REGEX));
    }

    @Test
    public void shouldMatchReferenceRangeRegex() {
        assertTrue("A7:C7".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertTrue("Z9:Z10".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertTrue("AZ1:AZ12".matches(FormulaParser.REFERENCE_RANGE_REGEX));
    }

    @Test
    public void shouldNotMatchReferenceRangeRegex() {
        assertFalse("A:C7".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertFalse("Z9:Z".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertFalse("1:AZ12".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertFalse("AZ1:12".matches(FormulaParser.REFERENCE_RANGE_REGEX));
        assertFalse("A1:A2:A3".matches(FormulaParser.REFERENCE_RANGE_REGEX));
    }

    @Test
    public void shouldMatchRowRangeRegex() {
        assertTrue("7:7".matches(FormulaParser.ROW_RANGE_REGEX));
        assertTrue("9:10".matches(FormulaParser.ROW_RANGE_REGEX));
        assertTrue("1:112".matches(FormulaParser.ROW_RANGE_REGEX));
    }

    @Test
    public void shouldNotMatchRowRangeRegex() {
        assertFalse("A1:C7".matches(FormulaParser.ROW_RANGE_REGEX));
        assertFalse("9:Z".matches(FormulaParser.ROW_RANGE_REGEX));
        assertFalse("1:AZ12".matches(FormulaParser.ROW_RANGE_REGEX));
        assertFalse("AZ1:12".matches(FormulaParser.ROW_RANGE_REGEX));
    }

    @Test
    public void shouldMatchColumnRangeRegex() {
        assertTrue("A:C".matches(FormulaParser.COLUMN_RANGE_REGEX));
        assertTrue("Z:Z".matches(FormulaParser.COLUMN_RANGE_REGEX));
        assertTrue("A:AZ".matches(FormulaParser.COLUMN_RANGE_REGEX));
    }

    @Test
    public void shouldNotMatchColumnRangeRegex() {
        assertFalse("A7:C7".matches(FormulaParser.COLUMN_RANGE_REGEX));
        assertFalse("9:Z".matches(FormulaParser.COLUMN_RANGE_REGEX));
        assertFalse("1:AZ12".matches(FormulaParser.COLUMN_RANGE_REGEX));
        assertFalse("AZ1:12".matches(FormulaParser.COLUMN_RANGE_REGEX));
    }

    @Test
    public void shouldMatchRangeRegex() {
        assertTrue("A:C".matches(FormulaParser.RANGE_REGEX));
        assertTrue("1:1".matches(FormulaParser.RANGE_REGEX));
        assertTrue("A1:C4".matches(FormulaParser.RANGE_REGEX));
    }

    @Test
    public void shouldNotMatchRangeRegex() {
        assertFalse("A:1".matches(FormulaParser.RANGE_REGEX));
        assertFalse("1:A".matches(FormulaParser.RANGE_REGEX));
        assertFalse("A:C4".matches(FormulaParser.RANGE_REGEX));
        assertFalse("A1:4".matches(FormulaParser.RANGE_REGEX));
        assertFalse("A1:A4:B3".matches(FormulaParser.RANGE_REGEX));
    }

    @Test
    public void shouldEvaluateReference() {
        int[] coords = this.parser.evaluateReference("A1");
        assertEquals(0, coords[0]);
        assertEquals(0, coords[1]);

        coords = this.parser.evaluateReference("Z42");
        assertEquals(25, coords[0]);
        assertEquals(41, coords[1]);

        coords = this.parser.evaluateReference("AZ213");
        assertEquals(51, coords[0]);
        assertEquals(212, coords[1]);

        coords = this.parser.evaluateReference("BA1");
        assertEquals(52, coords[0]);
        assertEquals(0, coords[1]);

        coords = this.parser.evaluateReference("ZZ1");
        assertEquals(701, coords[0]);
        assertEquals(0, coords[1]);

        coords = this.parser.evaluateReference("AAA1");
        assertEquals(702, coords[0]);
        assertEquals(0, coords[1]);
    }

    @Test
    public void shouldConvertIndexToColumnString() {
        assertEquals("A", this.parser.convertIndexToColumnString(0));
        assertEquals("Z", this.parser.convertIndexToColumnString(25));
        assertEquals("AZ", this.parser.convertIndexToColumnString(51));
        assertEquals("BA", this.parser.convertIndexToColumnString(52));
        assertEquals("BJ", this.parser.convertIndexToColumnString(61));
        assertEquals("AAA", this.parser.convertIndexToColumnString(702));
    }

    @Test
    public void shouldEvaluateReferences() {
        this.dataProvider.setDataValue(0, 0, 5);
        this.dataProvider.setDataValue(0, 1, 5);
        this.dataProvider.setDataValue(0, 2, "=SUM(A1;A2)");
        this.dataProvider.setDataValue(1, 0, 3);
        this.dataProvider.setDataValue(1, 1, 3);
        this.dataProvider.setDataValue(1, 2, "=SUM(B1;B2)");

        FunctionValue result = this.parser.parseFunction("SUM(A1;A2)");
        assertEquals(new BigDecimal(10), result.getValue());

        result = this.parser.parseFunction("SUM(B1;B2)");
        assertEquals(new BigDecimal(6), result.getValue());

        result = this.parser.parseFunction("AVERAGE(A3;B3)");
        assertEquals(new BigDecimal(8), result.getValue());
    }

    @Test
    public void shouldEvaluateRowRange() {
        for (int row = 0; row < this.dataProvider.getRowCount(); row++) {
            for (int column = 0; column < this.dataProvider.getColumnCount(); column++) {
                this.dataProvider.setDataValue(column, row, row + 1);
            }
        }

        FunctionValue result = this.parser.parseFunction("SUM(5:5)");
        assertEquals(new BigDecimal(50), result.getValue());

        result = this.parser.parseFunction("SUM(5:6)");
        assertEquals(new BigDecimal(110), result.getValue());

        result = this.parser.parseFunction("SUM(5:8)");
        assertEquals(new BigDecimal(260), result.getValue());

        result = this.parser.parseFunction("SUM(8:5)");
        assertEquals(new BigDecimal(260), result.getValue());
    }

    @Test
    public void shouldEvaluateColumnRange() {
        for (int column = 0; column < this.dataProvider.getColumnCount(); column++) {
            for (int row = 0; row < this.dataProvider.getRowCount(); row++) {
                this.dataProvider.setDataValue(column, row, column + 1);
            }
        }

        FunctionValue result = this.parser.parseFunction("SUM(E:E)");
        assertEquals(new BigDecimal(50), result.getValue());

        result = this.parser.parseFunction("SUM(E:F)");
        assertEquals(new BigDecimal(110), result.getValue());

        result = this.parser.parseFunction("SUM(E:H)");
        assertEquals(new BigDecimal(260), result.getValue());

        result = this.parser.parseFunction("SUM(H:E)");
        assertEquals(new BigDecimal(260), result.getValue());
    }

    @Test
    public void shouldEvaluateReferenceRange() {
        for (int row = 0; row < this.dataProvider.getRowCount(); row++) {
            for (int column = 0; column < this.dataProvider.getColumnCount(); column++) {
                this.dataProvider.setDataValue(column, row, row + 1);
            }
        }

        FunctionValue result = this.parser.parseFunction("=SUM(C3:F5)");
        assertEquals(new BigDecimal(48), result.getValue());

        result = this.parser.parseFunction("=SUM(F5:F8)");
        assertEquals(new BigDecimal(26), result.getValue());

        result = this.parser.parseFunction("=SUM(C5:F5)");
        assertEquals(new BigDecimal(20), result.getValue());

        result = this.parser.parseFunction("=SUM(F5:C3)");
        assertEquals(new BigDecimal(48), result.getValue());
    }

    @Test
    public void shouldEvaluateDecimalCalculation() {
        this.dataProvider.setDataValue(0, 0, "3.4");
        this.dataProvider.setDataValue(1, 0, "4");

        FunctionValue result = this.parser.parseFunction("=PRODUCT(A1;B1)");
        assertEquals(new BigDecimal("13.6"), result.getValue());

        this.parser.setDecimalFormat((DecimalFormat) DecimalFormat.getInstance(Locale.GERMAN));

        this.dataProvider.setDataValue(0, 0, "3,4");
        this.dataProvider.setDataValue(1, 0, "4");

        result = this.parser.parseFunction("=PRODUCT(A1;B1)");
        assertEquals(new BigDecimal("13.6"), result.getValue());
    }

    @Test
    public void shouldEvaluateInteger() {
        assertTrue(this.parser.isIntegerValue(new BigDecimal("0")));
        assertTrue(this.parser.isIntegerValue(new BigDecimal("5")));
        assertTrue(this.parser.isIntegerValue(new BigDecimal("10")));
        assertTrue(this.parser.isIntegerValue(new BigDecimal("240")));
        assertTrue(this.parser.isIntegerValue(new BigDecimal("3.0")));

        assertFalse(this.parser.isIntegerValue(new BigDecimal("2.4")));
        assertFalse(this.parser.isIntegerValue(new BigDecimal("12.34")));
        assertFalse(this.parser.isIntegerValue(new BigDecimal("12.01")));
    }

    @Test(expected = FunctionException.class)
    public void shouldThrowExceptionOnUnsupportedTypes() {
        this.dataProvider.setDataValue(0, 0, "3");
        this.dataProvider.setDataValue(1, 0, "a");

        this.parser.parseFunction("=PRODUCT(A1;B1)");
    }

    @Test(expected = FunctionException.class)
    public void shouldThrowExceptionOnUnsupportedTypesInRange() {
        this.dataProvider.setDataValue(0, 0, "3");
        this.dataProvider.setDataValue(1, 0, "a");

        this.parser.parseFunction("=PRODUCT(A1:B1)");
    }

    @Test(expected = FunctionException.class)
    public void shouldThrowExceptionOnNegativeSqrt() {
        this.parser.parseFunction("=SQRT(-9)");
    }

    @Test
    public void shouldEvaluateSqrt() {
        this.dataProvider.setDataValue(0, 1, "34.81");

        FunctionValue result = this.parser.parseFunction("=SQRT(36)");
        assertEquals(new BigDecimal("6.0"), result.getValue());

        result = this.parser.parseFunction("=SQRT(A2)");
        assertTrue(result.getValue().toString().startsWith("5.9"));

        result = this.parser.parseFunction("=SQRT(POWER(2.5;2))");
        assertEquals(new BigDecimal("2.5"), result.getValue());
    }

    @Test(expected = FunctionException.class)
    public void shouldThrowExceptionOnWrongArgumentsMod() {
        this.parser.parseFunction("=MOD(9)").getValue();
    }

    @Test
    public void shouldEvaluateMod() {
        this.dataProvider.setDataValue(0, 2, "6");
        this.dataProvider.setDataValue(1, 2, "2.5");

        FunctionValue result = this.parser.parseFunction("=MOD(6;4)");
        assertEquals(new BigDecimal("2"), result.getValue());

        result = this.parser.parseFunction("=MOD(6;3)");
        assertEquals(new BigDecimal("0"), result.getValue());

        result = this.parser.parseFunction("=MOD(A3;B3)");
        assertEquals(new BigDecimal("1.0"), result.getValue());
    }

    @Test
    public void shouldUpdateReferences() {
        assertEquals("=PRODUCT(C1:D1)", this.parser.updateReferences("=PRODUCT(A1:B1)", 2, 0, 4, 0));
        assertEquals("=PRODUCT(SUM(E4;E5):D1)", this.parser.updateReferences("=PRODUCT(SUM(C4;C5):B1)", 2, 0, 4, 0));
        assertEquals("=SUM(G7;SUM(E6;E7):D3)", this.parser.updateReferences("=SUM(E5;SUM(C4;C5):B1)", 2, 0, 4, 2));
        assertEquals("=PRODUCT(C3:C3)", this.parser.updateReferences("=PRODUCT(A1:A1)", 2, 0, 4, 2));

        assertEquals("=PRODUCT(A1:B1)", this.parser.updateReferences("=PRODUCT(C1:D1)", 4, 0, 2, 0));
        assertEquals("=PRODUCT(A1:B1)", this.parser.updateReferences("=PRODUCT(C3:D3)", 4, 2, 2, 0));
    }

    @Test
    public void shouldEvaluateEmptyReferences() {
        this.dataProvider.setDataValue(1, 1, "=4");

        // both references are not set, result should be 0
        assertEquals(new BigDecimal("0"), this.parser.parseFunction("=A1*B1").getValue());
        assertEquals(new BigDecimal("0"), this.parser.parseFunction("=C1+D1").getValue());
        // only one reference is set, result should be the value of that cell
        assertEquals(new BigDecimal("4"), this.parser.parseFunction("=A2*B2").getValue());
    }

    @Test(expected = FunctionException.class)
    public void shouldThrowExceptionOnSelfReference() {
        this.dataProvider.setDataValue(0, 0, "=A1");
        this.parser.parseFunction("=A1");
    }

    @Test(expected = FunctionException.class)
    public void shouldNoticeSimpleCycleReference() {
        this.dataProvider.setDataValue(0, 0, "=B1");
        this.dataProvider.setDataValue(1, 0, "=A1");

        this.parser.parseFunction("=A1");
    }

    @Test(expected = FunctionException.class)
    public void shouldNoticeCycleReference() {
        this.dataProvider.setDataValue(0, 0, "=B1");
        this.dataProvider.setDataValue(1, 0, "=C1");
        this.dataProvider.setDataValue(2, 0, "=A1");

        this.parser.parseFunction("=A1");
    }

    @Test
    public void shouldEvaluateMultiReferences() {
        this.dataProvider.setDataValue(0, 0, "5");
        this.dataProvider.setDataValue(1, 0, "3");
        this.dataProvider.setDataValue(3, 0, "=A1+B1");

        assertEquals(new BigDecimal("0.625"), this.parser.parseFunction("=A1/D1").getValue());
    }

    @Test(expected = FunctionException.class)
    public void shouldNoticeCycleReferenceWithRange() {
        this.dataProvider.setDataValue(0, 0, "5");
        this.dataProvider.setDataValue(0, 1, "5");
        this.dataProvider.setDataValue(0, 2, "=SUM(A1:A2)");
        this.dataProvider.setDataValue(1, 0, "3");
        this.dataProvider.setDataValue(1, 1, "3");
        this.dataProvider.setDataValue(1, 2, "=SUM(B1:B2)");

        // this should fail for circular dependencies
        this.dataProvider.setDataValue(2, 2, "=SUM(A3:C3)");

        this.parser.parseFunction("=C3");
    }

}
