/**
 * License Agreement.
 *
 * Rich Faces - Natural Ajax for Java Server Faces (JSF)
 *
 * Copyright (C) 2007 Exadel, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
 */
package org.richfaces.cdk.templatecompiler.parser.el.test;

import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.expectLastCall;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.richfaces.cdk.templatecompiler.statements.HelperMethod.EMPTINESS_CHECK;
import static org.richfaces.cdk.templatecompiler.statements.HelperMethod.EQUALS_CHECK;
import static org.richfaces.cdk.templatecompiler.statements.HelperMethod.TO_BOOLEAN_CONVERSION;
import static org.richfaces.cdk.templatecompiler.statements.HelperMethod.TO_STRING_CONVERSION;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.richfaces.cdk.As;
import org.richfaces.cdk.CdkClassLoader;
import org.richfaces.cdk.CdkTestBase;
import org.richfaces.cdk.CdkTestRunner;
import org.richfaces.cdk.Logger;
import org.richfaces.cdk.Mock;
import org.richfaces.cdk.MockController;
import org.richfaces.cdk.templatecompiler.ELParser;
import org.richfaces.cdk.templatecompiler.builder.model.Variables;
import org.richfaces.cdk.templatecompiler.el.ELParserImpl;
import org.richfaces.cdk.templatecompiler.el.ParsingException;
import org.richfaces.cdk.templatecompiler.el.types.ELType;
import org.richfaces.cdk.templatecompiler.el.types.TypesFactory;
import org.richfaces.cdk.templatecompiler.el.types.TypesFactoryImpl;
import org.richfaces.cdk.templatecompiler.statements.HelperMethod;
import org.richfaces.cdk.templatecompiler.statements.TypedTemplateStatement;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.inject.Binder;
import com.google.inject.Inject;

@RunWith(CdkTestRunner.class)
public class ELParserTest extends CdkTestBase {
    @Inject
    @As(ELParserImpl.class)
    private ELParser parser;
    @Mock
    private Logger log;
    @Inject
    @As(TypesFactoryImpl.class)
    private TypesFactory typesFactory;
    @Inject
    private MockController controller;

    @Override
    public void configure(Binder binder) {
        super.configure(binder);
        binder.bind(CdkClassLoader.class).toInstance(createClassLoader());
    }

    private TypedTemplateStatement parseExpression(String expression) throws ParsingException {
        return parseExpression(expression, Object.class);
    }

    private TypedTemplateStatement parseExpression(String expression, String expected, HelperMethod... requiredMethods)
            throws ParsingException {
        return parseExpression(expression, Object.class, expected, requiredMethods);
    }

    private TypedTemplateStatement parseExpression(String expression, String expected, Class<?> expectedType,
            HelperMethod... requiredMethods) throws ParsingException {
        return parseExpression(expression, Object.class, expected, expectedType, requiredMethods);
    }

    private TypedTemplateStatement parseExpression(String expression, Class<?> returnType, String expected,
            Class<?> expectedType, HelperMethod... requiredMethods) throws ParsingException {
        TypedTemplateStatement statement = parseExpression(expression, returnType, expected, requiredMethods);
        assertEquals(expectedType.getName(), statement.getType().getRawName());
        return statement;
    }

    private TypedTemplateStatement parseExpression(String expression, Class<?> returnType, String expected,
            HelperMethod... requiredMethods) throws ParsingException {
        controller.replay();
        TypedTemplateStatement parseExpression = parseExpression(expression, returnType);
        controller.verify();
        assertEquals(expected, parseExpression.getCode());
        for (HelperMethod helperMethod : requiredMethods) {
            assertTrue("Expect helper method " + helperMethod.getName(),
                    Iterables.contains(parseExpression.getRequiredMethods(), helperMethod));
        }
        for (HelperMethod helperMethod : parseExpression.getRequiredMethods()) {
            assertTrue("Unexpected helper method " + helperMethod.getName(),
                    Iterables.contains(ImmutableSet.copyOf(requiredMethods), helperMethod));
        }
        return parseExpression;
    }

    private TypedTemplateStatement parseExpression(String expression, Class<?> returnType) throws ParsingException {
        final Map<String, ELType> contextMap = new HashMap<String, ELType>();

        Variables variables = new Variables() {
            @Override
            public ELType getVariable(String name) throws ParsingException {
                return contextMap.get(name);
            }

            @Override
            public boolean isDefined(String name) throws ParsingException {
                return contextMap.containsKey(name);
            }

            @Override
            public ELType setVariable(String name, ELType type) throws ParsingException {
                return contextMap.put(name, type);
            }
        };
        contextMap.put("action", this.getType(org.richfaces.cdk.templatecompiler.parser.el.test.Bean.class));
        contextMap.put("clientId", this.getType(String.class));
        contextMap.put("test", this.getType(boolean.class));
        contextMap.put("otherTest", this.getType(boolean.class));
        contextMap.put("this", this.getType(Object.class));
        contextMap.put("super", this.getType(Object.class));
        contextMap.put("objectVar", this.getType(Object.class));

        return parser.parse(expression, variables, this.getType(returnType));
    }

    private ELType getType(Class<?> returnType) {
        return typesFactory.getType(returnType);
    }

    @Test
    public void testAnd() throws Exception {
        parseExpression("#{test and otherTest}", "(test && otherTest)");
    }

    @Test
    public void testAnd1() throws Exception {
        parseExpression("#{otherTest && test}", "(otherTest && test)");
        // assertEquals(Boolean.TYPE, visitor.getExpressionType().getRawType());
    }

    @Test
    public void testAnd2() throws Exception {
        parseExpression("#{action and otherTest}", "(convertToBoolean(action) && otherTest)", TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testAnd3() throws Exception {
        parseExpression("#{test && action}", "(test && convertToBoolean(action))", TO_BOOLEAN_CONVERSION);
        // assertEquals(Boolean.TYPE, visitor.getExpressionType().getRawType());
    }

    @Test
    public void testBooleanReturnType() throws Exception {
        parseExpression("#{clientId}", Boolean.TYPE, "convertToBoolean(clientId)", Boolean.TYPE, TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testBooleanReturnType1() throws Exception {
        parseExpression("#{test}", Boolean.TYPE, "test", Boolean.TYPE);
    }

    @Test
    public void testChoice() throws Exception {
        parseExpression("#{test ? 2 : 3}", Object.class, "(test ? 2 : 3)", Integer.TYPE);
    }

    @Test
    public void testChoice1() throws Exception {
        parseExpression("#{test ? null : 'string'}", Object.class, "(test ? null : \"string\")", String.class);
    }

    @Test
    public void testChoice2() throws Exception {
        parseExpression("#{action ? null : 'string'}", Object.class, "(convertToBoolean(action) ? null : \"string\")",
                TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testDiv() throws Exception {
        parseExpression("#{1/2}", "(1 / 2)", Integer.TYPE);
    }

    @Test
    public void testEmpty() throws Exception {
        parseExpression("#{empty action.array}", "isEmpty(action.getArray())", Boolean.TYPE, EMPTINESS_CHECK);
    }

    @Test
    public void testEmptyString() throws Exception {
        parseExpression("", "\"\"", String.class);
    }

    @Test
    public void testEquals() throws Exception {
        parseExpression("#{1 eq 2}", "(1 == 2)", Boolean.TYPE);
    }

    @Test
    public void testEquals1() throws Exception {
        parseExpression("#{3 == 2}", "(3 == 2)", Boolean.TYPE);
    }

    @Test
    public void testEquals2() throws Exception {
        parseExpression("#{action == 2}", "isEqual(action,2)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testEquals3() throws Exception {
        parseExpression("#{2 eq action}", "isEqual(2,action)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testEquals4() throws Exception {
        parseExpression("#{action == clientId}", "isEqual(action,clientId)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testEquals5() throws Exception {
        parseExpression("#{action eq clientId}", "isEqual(action,clientId)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testEquals6() throws Exception {
        parseExpression("#{action eq null}", "(action == null)", Boolean.TYPE);
    }

    @Test
    public void testEquals7() throws Exception {
        parseExpression("#{2 == null}", "isEqual(2,null)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testFalse() throws Exception {
        parseExpression("#{false}", "false", Boolean.TYPE);
    }

    @Test
    public void testFloat() throws Exception {
        parseExpression("#{5.0}", "Double.valueOf(5.0)", Double.TYPE);
    }

    @Test
    public void testFloat1() throws Exception {
        parseExpression("#{5.012e+34}", "Double.valueOf(5.012e+34)", Double.TYPE);
    }

    @Test
    public void testFunction() throws Exception {
        parseExpression("#{super:getType()}", "super.getType()");
    }

    @Test
    public void testGreatThen() throws Exception {
        parseExpression("#{1 gt 2}", "(1 > 2)", Boolean.TYPE);
    }

    @Test
    public void testGreatThen1() throws Exception {
        parseExpression("#{3 > 2}", "(3 > 2)", Boolean.TYPE);
    }

    @Test
    public void testGreatThenEquals() throws Exception {
        parseExpression("#{1 ge 2}", "(1 >= 2)", Boolean.TYPE);
    }

    @Test
    public void testGreatThenEquals1() throws Exception {
        parseExpression("#{3 >= 2}", "(3 >= 2)", Boolean.TYPE);
    }

    @Test
    public void testIdentifier() throws Exception {
        parseExpression("#{clientId}", "clientId", String.class);
    }

    @Test
    public void testInteger() throws Exception {
        parseExpression("#{152}", "152", Integer.TYPE);
    }

    @Test
    public void testLessThen() throws Exception {
        parseExpression("#{1 lt 2}", "(1 < 2)", Boolean.TYPE);
    }

    @Test
    public void testLessThen1() throws Exception {

        parseExpression("#{3 < 2}", "(3 < 2)", Boolean.TYPE);
    }

    @Test
    public void testLessThenEquals() throws Exception {
        parseExpression("#{1 le 2}", "(1 <= 2)", Boolean.TYPE);
    }

    @Test
    public void testLessThenEquals1() throws Exception {

        parseExpression("#{3 <= 2}", "(3 <= 2)", Boolean.TYPE);
    }

    @Test
    public void testLiteral() throws Exception {
        parseExpression("clientId", "\"clientId\"", String.class);
    }

    @Test
    public void testLiteralWithDeferred() throws Exception {
        parseExpression("#{1}#{2}", "convertToString(1) + convertToString(2)", String.class, HelperMethod.TO_STRING_CONVERSION);
    }

    @Test
    public void testLiteralWithDeferred1() throws Exception {
        parseExpression("abs #{getType()}", "\"abs \" + convertToString(this.getType())", String.class, TO_STRING_CONVERSION);
    }

    @Test
    public void testLiteralWithDeferred2() throws Exception {
        parseExpression("#{getType()} abs ", "convertToString(this.getType()) + \" abs \"", String.class, TO_STRING_CONVERSION);
    }

    @Test
    public void testMethod() throws Exception {
        parseExpression("#{action.readOnly}", "action.isReadOnly()", Boolean.TYPE);
    }

    @Test
    public void testMethodReturnArray() throws Exception {
        parseExpression("#{action.array}", "action.getArray()"/* , UIComponent[].class */);
    }

    @Test
    public void testMethodReturnArrayElement() throws Exception {
        parseExpression("#{action.array[0]}", "action.getArray()[0]", UIComponent.class);
    }

    @Test
    public void testMethodReturnList() throws Exception {
        TypedTemplateStatement statement = parseExpression("#{action.components}", "action.getComponents()", List.class);
        ELType variableType = statement.getType();
        assertEquals(List.class.getName(), variableType.getRawName());
        assertEquals(UIComponent.class.getName(), variableType.getContainerType().getRawName());
    }

    @Test
    public void testMethodReturnListElement() throws Exception {
        parseExpression("#{action.components[0]}", "action.getComponents().get(0)", UIComponent.class);
    }

    @Test
    public void testMethodReturnListElement2() throws Exception {
        parseExpression("#{action.components[0].rendered}", "action.getComponents().get(0).isRendered()", Boolean.TYPE);
    }

    // @Test
    // public void testMethodReturnMapElement1() throws Exception {
    // assertEquals("action.getFacets().get(\"header\")", resolveExpression("#{action.facets.header}"));
    // }

    @Test
    public void testMethodReturnMap() throws Exception {
        TypedTemplateStatement statement = parseExpression("#{action.facets}", "action.getFacets()", Map.class);
        ELType variableType = statement.getType();
        assertEquals(Map.class.getName(), variableType.getRawName());
        assertEquals(UIComponent.class.getName(), variableType.getContainerType().getRawName());
    }

    @Test
    public void testMethodReturnMap1() throws Exception {
        parseExpression("#{action.rawMap}", "action.getRawMap()", Map.class);
    }

    @Test
    public void testMethodReturnMapElement() throws Exception {
        parseExpression("#{action.getFacet('header')}", "action.getFacet(\"header\")", UIComponent.class);
    }

    @Test
    public void testMethodReturnMapElement1() throws Exception {

        parseExpression("#{action.facets['header']}", "action.getFacets().get(\"header\")", UIComponent.class);
    }

    @Test
    public void testMethodReturnMapElement2() throws Exception {

        parseExpression("#{action.rawMap['something']}", "action.getRawMap().get(\"something\")", Object.class);
    }

    @Test
    public void testMethodReturnMapElement3() throws Exception {
        parseExpression("#{action.facets.toString()}", "action.getFacets().toString()", String.class);
    }

    @Test
    public void testMethodReturnMapElement4() throws Exception {
        // assertEquals("action.getFacet(\"header\").isRendered()",
        // resolveExpression("#{action.getFacet('header').rendered}"));
        parseExpression("#{action.facets['header'].rendered}", "action.getFacets().get(\"header\").isRendered()", Boolean.TYPE);
    }

    @Test
    public void testMethodWithParam() throws Exception {
        parseExpression("#{getType(action.array[0].rendered, action.readOnly, true)}",
                "this.getType(action.getArray()[0].isRendered(),action.isReadOnly(),true)");
    }

    @Test
    public void testMethodWithParam1() throws Exception {
        parseExpression("#{action.count(123)}", "action.count(123)", Integer.class);
    }

    @Test
    public void testMethodWithParam2() throws Exception {

        parseExpression("#{action.count(clientId)}", "action.count(clientId)", Object.class);
    }

    @Test
    public void testMinus() throws Exception {
        parseExpression("#{1-2}", "(1 - 2)", Integer.TYPE);
    }

    @Test
    public void testMod() throws Exception {
        parseExpression("#{1%2}", "(1 % 2)", Integer.TYPE);
    }

    @Test
    public void testMult() throws Exception {
        parseExpression("#{1*2}", "(1 * 2)", Integer.TYPE);
    }

    @Test
    public void testNegative() throws Exception {
        parseExpression("#{-5}", "-5", Integer.TYPE);
    }

    @Test
    public void testNegativeFloat() throws Exception {
        parseExpression("#{-5.0}", "-Double.valueOf(5.0)", Double.TYPE);
    }

    @Test
    public void testNestedMethod() throws Exception {
        parseExpression("#{action.testBean2.string}", "action.getTestBean2().getString()", String.class);
    }

    @Test
    public void testNonExistingMethod() throws Exception {
        parseExpression("#{action.doSomething(clientId, 123)}", "action.doSomething(clientId,123)", Object.class);
    }

    @Test
    public void testNot() throws Exception {
        parseExpression("#{not test}", "(!test)", Boolean.TYPE);
    }

    @Test
    public void testNot1() throws Exception {
        parseExpression("#{!otherTest}", "(!otherTest)", Boolean.TYPE);
    }

    @Test
    public void testNot2() throws Exception {

        parseExpression("#{!action}", "(!convertToBoolean(action))", Boolean.TYPE, TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testNotEqual() throws Exception {
        parseExpression("#{1 ne 3}", "(1 != 3)", Boolean.TYPE);
    }

    @Test
    public void testNotEqual1() throws Exception {

        parseExpression("#{2 != 3}", "(2 != 3)", Boolean.TYPE);
    }

    @Test
    public void testNotEqual2() throws Exception {

        parseExpression("#{action != 2}", "!isEqual(action,2)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testNotEqual3() throws Exception {

        parseExpression("#{2 ne action}", "!isEqual(2,action)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testNotEqual4() throws Exception {

        parseExpression("#{action != clientId}", "!isEqual(action,clientId)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testNotEqual5() throws Exception {

        parseExpression("#{action ne clientId}", "!isEqual(action,clientId)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testNotEqual6() throws Exception {

        parseExpression("#{action ne null}", "(action != null)", Boolean.TYPE);
    }

    @Test
    public void testNotEqual7() throws Exception {

        parseExpression("#{2 != null}", "!isEqual(2,null)", Boolean.TYPE, EQUALS_CHECK);
    }

    @Test
    public void testNull() throws Exception {
        TypedTemplateStatement statement = parseExpression("#{null}", "null");
        assertTrue(statement.getType().isNullType());
    }

    @Test
    public void testOr() throws Exception {
        parseExpression("#{test or otherTest}", "(test || otherTest)", Boolean.TYPE);
    }

    @Test
    public void testOr1() throws Exception {
        parseExpression("#{otherTest || test}", "(otherTest || test)", Boolean.TYPE);
    }

    @Test
    public void testOr2() throws Exception {

        parseExpression("#{action or otherTest}", "(convertToBoolean(action) || otherTest)", Boolean.TYPE,
                TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testOr3() throws Exception {

        parseExpression("#{test || action}", "(test || convertToBoolean(action))", Boolean.TYPE, TO_BOOLEAN_CONVERSION);
    }

    @Test
    public void testPlus() throws Exception {
        // TODO: tests involving double values
        parseExpression("#{1+2}", "(1 + 2)", Integer.TYPE);
    }

    @Test
    public void testString() throws Exception {
        parseExpression("#{\"nabc\"}", "\"nabc\"", String.class);
    }

    @Test
    public void testString1() throws Exception {

        parseExpression("#{'nabc'}", "\"nabc\"", String.class);
    }

    @Test
    public void testString2() throws Exception {

        parseExpression("#{'\tabc'}", "\"\\tabc\"", String.class);
    }

    @Test
    public void testString3() throws Exception {

        parseExpression("#{'/nabc'}", "\"/nabc\"", String.class);
    }

    @Test
    public void testString4() throws Exception {

        parseExpression("#{'na\"bc'}", "\"na\\\"bc\"", String.class);
    }

    @Test
    public void testString5() throws Exception {

        parseExpression("#{'na\\\\bc'}", "\"na\\\\bc\"", String.class);
    }

    @Test
    public void testStringReturnType() throws Exception {
        parseExpression("#{clientId}", String.class, "clientId", String.class);
    }

    @Test
    public void testStringReturnType1() throws Exception {

        parseExpression("#{test}", String.class, "convertToString(test)", String.class, TO_STRING_CONVERSION);
    }

    @Test
    public void testThisFunction() throws Exception {
        parseExpression("#{getType()}", "this.getType()");
    }

    @Test
    public void testThisFunction1() throws Exception {

        parseExpression("#{this.getType()}", "this.getType()");
    }

    @Test
    public void testTrue() throws Exception {
        parseExpression("#{true}", "true", Boolean.TYPE);
    }

    @Test
    public void testVariableFunction() throws Exception {
        parseExpression("#{objectVar.getType()}", "objectVar.getType()");
    }

    @Test(/* expected = ParsingException.class */)
    public void testWrongExpression() throws Exception {
        log.warn((CharSequence) anyObject());
        expectLastCall();
        parseExpression("#{bean.property}", "bean.getProperty()");
    }

    @Test
    public void testWrongExpression2() throws Exception {
        parseExpression("#{action.property}", "action.getProperty()", Object.class);
    }

    @Test
    public void testLiteralExpression() throws Exception {
        controller.replay();
        TypedTemplateStatement parseExpression = parseExpression("Literal", Object.class);
        controller.verify();
        assertTrue(parseExpression.isLiteral());
        assertEquals("\"Literal\"", parseExpression.getCode());
        assertEquals(String.class.getName(), parseExpression.getType().getRawName());
    }

    @Test
    public void testToScriptArgs() throws Exception {
        TypedTemplateStatement parseExpression = parseExpression("#{toScriptArgs(clientId, bean)}");

        assertTrue(Iterables.contains(parseExpression.getRequiredMethods(), HelperMethod.TO_SCRIPT_ARGS));
        assertEquals("toScriptArgs(clientId,bean)", parseExpression.getCode());
    }
}
