/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.cdk.templatecompiler.parser.el.test;

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

import java.io.Writer;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.richfaces.cdk.CdkClassLoader;
import org.richfaces.cdk.CdkTestBase;
import org.richfaces.cdk.CdkTestRunner;
import org.richfaces.cdk.templatecompiler.el.types.ELPropertyDescriptor;
import org.richfaces.cdk.templatecompiler.el.types.ELType;
import org.richfaces.cdk.templatecompiler.el.types.TypeParserFactory;
import org.richfaces.cdk.templatecompiler.el.types.TypesFactory;
import org.richfaces.cdk.templatecompiler.el.types.TypesFactoryImpl;

import com.google.inject.Binder;
import com.google.inject.Inject;

/**
 * @author Nick Belaevski
 *
 */
@RunWith(CdkTestRunner.class)
public class TypesFactoryTest extends CdkTestBase {
    @Inject
    TypesFactory typesFactory;

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

    private static final class ParameterizedTypesHolder {
        @SuppressWarnings("unused")
        public List<String>[] getArray() {
            return null;
        }

        @SuppressWarnings("unused")
        public Map<String, Object> getMap() {
            return null;
        }

        @SuppressWarnings("unused")
        public <Abc> Abc getTypeVariableList() {
            return null;
        }

        @SuppressWarnings("unused")
        public List<? extends String> getWildcardList() {
            return null;
        }
    }

    private java.lang.reflect.Type getParameterizedArrayType() throws Exception {
        return ParameterizedTypesHolder.class.getMethod("getArray").getGenericReturnType();
    }

    private java.lang.reflect.Type getParameterizedMapType() throws Exception {
        return ParameterizedTypesHolder.class.getMethod("getMap").getGenericReturnType();
    }

    private java.lang.reflect.Type getTypeVariableType() throws Exception {
        return ParameterizedTypesHolder.class.getMethod("getTypeVariableList").getGenericReturnType();
    }

    private java.lang.reflect.Type getWildcardType() throws Exception {
        return ParameterizedTypesHolder.class.getMethod("getWildcardList").getGenericReturnType();
    }

    @Test
    public void testCaching() throws Exception {
        ELType objectType = typesFactory.getType(Object.class);
        ELType objectType2 = typesFactory.getType(Object.class);

        assertNotNull(objectType);
        assertNotNull(objectType2);

        assertSame(objectType, objectType2);
    }

    @Test
    public void testGetTypeFromReflectionType() throws Exception {
        ELType integerType = typesFactory.getType(Integer.TYPE);
        assertNotNull(integerType);
        assertEquals(Integer.TYPE.getName(), integerType.getRawName());
        assertFalse(integerType.isArray());
        assertFalse(integerType.isNullType());

        ELType stringType = typesFactory.getType(String.class);
        assertNotNull(stringType);
        assertEquals(String.class.getName(), stringType.getRawName());
        assertEquals(0, stringType.getTypeArguments().length);
        assertFalse(stringType.isArray());
        assertFalse(stringType.isNullType());

        ELType arrayType = typesFactory.getType(String[].class);
        assertNotNull(arrayType);
        assertEquals(0, arrayType.getTypeArguments().length);
        assertTrue(arrayType.isArray());
        assertEquals(String.class.getName(), arrayType.getContainerType().getRawName());
        assertFalse(arrayType.isNullType());

        ELType multiDimArrayType = typesFactory.getType(String[][][].class);
        assertNotNull(multiDimArrayType);
        assertEquals(0, multiDimArrayType.getTypeArguments().length);
        assertTrue(multiDimArrayType.isArray());
        // assertEquals(String[][].class, multiDimArrayType.getContainerType().getRawName());
        assertFalse(multiDimArrayType.isNullType());

        ELType parameterizedMapType = typesFactory.getType(getParameterizedMapType());
        assertNotNull(parameterizedMapType);
        assertFalse(parameterizedMapType.isArray());
        assertEquals(Map.class.getName(), parameterizedMapType.getRawName());

        ELType[] parameterizedMapTypeArguments = parameterizedMapType.getTypeArguments();
        assertNotNull(parameterizedMapTypeArguments);
        assertEquals(2, parameterizedMapTypeArguments.length);
        assertEquals(String.class.getName(), parameterizedMapTypeArguments[0].getRawName());
        assertEquals(Object.class.getName(), parameterizedMapTypeArguments[1].getRawName());

        ELType parameterizedArrayType = typesFactory.getType(getParameterizedArrayType());
        assertNotNull(parameterizedArrayType);
        assertTrue(parameterizedArrayType.isArray());
        // assertEquals(List[].class, parameterizedArrayType.getRawName());

        ELType[] parameterizedArrayTypeArguments = parameterizedArrayType.getTypeArguments();
        assertNotNull(parameterizedArrayTypeArguments);
        assertEquals(1, parameterizedArrayTypeArguments.length);
        ELType parameterizedArrayTypeArgument = parameterizedArrayTypeArguments[0];
        assertEquals(String.class.getName(), parameterizedArrayTypeArgument.getRawName());
        assertFalse(parameterizedArrayTypeArgument.isArray());

        ELType typeVariableType = typesFactory.getType(getTypeVariableType());

        assertNotNull(typeVariableType);
        assertEquals("Abc", typeVariableType.getCode());

        ELType wildcardTypeHolder = typesFactory.getType(getWildcardType());
        assertNotNull(wildcardTypeHolder);
        assertEquals(List.class.getName(), wildcardTypeHolder.getRawName());

        ELType[] wildcardTypeHolderArguments = wildcardTypeHolder.getTypeArguments();
        assertNotNull(wildcardTypeHolderArguments);
        assertEquals(1, wildcardTypeHolderArguments.length);
        ELType wildcardType = wildcardTypeHolderArguments[0];
        assertEquals("? extends java.lang.String", wildcardType.getCode());
    }

    @Test
    public void testGetTypeFromString() throws Exception {

        ELType primitiveIntType = typesFactory.getType("int");
        assertNotNull(primitiveIntType);
        assertEquals(Integer.TYPE.getName(), primitiveIntType.getRawName());

        ELType guessedMapType = typesFactory.getType("Map");
        assertNotNull(guessedMapType);
        assertEquals(Map.class.getName(), guessedMapType.getRawName());

        ELType writerType = typesFactory.getType(java.io.Writer.class.getName());
        assertNotNull(writerType);
        assertEquals(Writer.class.getName(), writerType.getRawName());
        assertEquals(0, writerType.getTypeArguments().length);

        ELType genericMapType = typesFactory.getType("Map<String, Object>");
        assertNotNull(genericMapType);

        assertEquals(Map.class.getName(), genericMapType.getRawName());
        ELType[] genericMapTypeArguments = genericMapType.getTypeArguments();
        assertNotNull(genericMapTypeArguments);
        assertFalse(genericMapType.isArray());
        assertEquals(2, genericMapTypeArguments.length);

        ELType genericMapTypeKeyArgument = genericMapTypeArguments[0];
        assertEquals(String.class.getName(), genericMapTypeKeyArgument.getRawName());

        ELType genericMapTypeValueArgument = genericMapTypeArguments[1];
        assertEquals(Object.class.getName(), genericMapTypeValueArgument.getRawName());

        ELType arrayType = typesFactory.getType("String[]");
        assertNotNull(arrayType);
        assertTrue(arrayType.isArray());
        // assertEquals(String[].class, arrayType.getRawName());

        ELType genericArrayType = typesFactory.getType("List<String>[]");
        assertNotNull(genericArrayType);
        assertTrue(genericArrayType.isArray());
        // assertEquals(List[].class, genericArrayType.getRawName());

        ELType[] genericArrayTypeArguments = genericArrayType.getTypeArguments();
        assertNotNull(genericArrayTypeArguments);
        assertEquals(1, genericArrayTypeArguments.length);

        ELType genericArrayTypeArgument = genericArrayTypeArguments[0];
        assertEquals(String.class.getName(), genericArrayTypeArgument.getRawName());
    }

    @Test
    public void testReferencedType() throws Exception {
        String className = "some.not.available.Class";
        ELType plainReferencedType = typesFactory.getType(className);

        assertNotNull(plainReferencedType);
        assertEquals(className, plainReferencedType.getCode());

        String arraySignature = className + "[]";
        ELType arrayReferencedType = typesFactory.getType(arraySignature);
        assertNotNull(arrayReferencedType);
        assertTrue(arrayReferencedType.isArray());
        assertEquals(arraySignature, arrayReferencedType.getCode());

        String genericSignature = className + "<String>";
        ELType genericReferenceType = typesFactory.getType(genericSignature);
        assertNotNull(genericReferenceType);
        assertEquals(genericSignature, genericReferenceType.getCode());

        ELType[] genericTypeArguments = genericReferenceType.getTypeArguments();
        assertNotNull(genericTypeArguments);
        assertEquals(1, genericTypeArguments.length);
        ELType genericTypeArgument = genericTypeArguments[0];
        assertEquals(String.class.getName(), genericTypeArgument.getRawName());
    }

    public ELPropertyDescriptor getAndCheckPropertyDescriptor(Class<?> clazz, String propertyName, String getMethodName,
            Class<?> expectedType, boolean readable, boolean writable) throws Exception {
        ELPropertyDescriptor checkPropertyDescriptor = getAndCheckPropertyDescriptor(clazz, propertyName, getMethodName,
                expectedType);
        assertEquals(writable, checkPropertyDescriptor.isWritable());
        assertEquals(readable, checkPropertyDescriptor.isReadable());
        return checkPropertyDescriptor;
    }

    public ELPropertyDescriptor getAndCheckPropertyDescriptor(Class<?> clazz, String propertyName, String getMethodName,
            Class<?> expectedType) throws Exception {
        ELType beanType = typesFactory.getType(clazz);
        ELPropertyDescriptor propertyDescriptor = typesFactory.getPropertyDescriptor(beanType, propertyName);
        assertNotNull(propertyDescriptor);
        assertEquals(getMethodName, propertyDescriptor.getReadMethodName());
        assertEquals(expectedType.getName(), propertyDescriptor.getType().getRawName());
        return propertyDescriptor;
    }

    @Test
    public void testGetStringProperty() throws Exception {
        ELPropertyDescriptor propertyDescriptor = getAndCheckPropertyDescriptor(Bean2.class, "string", "getString",
                String.class, true, true);
    }

    @Test
    public void testGetRawMapProperty() throws Exception {
        ELPropertyDescriptor propertyDescriptor = getAndCheckPropertyDescriptor(Bean.class, "rawMap", "getRawMap", Map.class,
                true, true);
    }

    @Test
    public void testGetBooleanProperty() throws Exception {
        ELPropertyDescriptor propertyDescriptor = getAndCheckPropertyDescriptor(Bean.class, "readOnly", "isReadOnly",
                Boolean.TYPE, true, true);
    }

    @Test
    public void testGetContextProperty() throws Exception {
        ELPropertyDescriptor propertyDescriptor = getAndCheckPropertyDescriptor(Bean.class, "context", "getContext",
                FacesContext.class, true, false);
    }

    @Test
    public void testGetDummyProperty() throws Exception {
        ELPropertyDescriptor propertyDescriptor = getAndCheckPropertyDescriptor(Bean.class, "nonExistedProperty",
                "getNonExistedProperty", Object.class, false, false);
    }

    public ELType findAndCheckMethod(Class<?> clazz, String methodName, Class<?> expectedType, Class<?>... arguments)
            throws Exception {
        ELType beanType = typesFactory.getType(clazz);
        ELType[] parameters = new ELType[arguments.length];
        for (int i = 0; i < arguments.length; i++) {
            parameters[i] = typesFactory.getType(arguments[i]);
        }
        ELType returnType = typesFactory.getMatchingVisibleMethodReturnType(beanType, methodName, parameters);
        assertNotNull(returnType);
        assertEquals(expectedType.getName(), returnType.getRawName());
        return returnType;
    }

    @Test
    public void testFindCountMethod() throws Exception {
        findAndCheckMethod(Bean.class, "count", Integer.class, Integer.TYPE);
    }

    @Test
    public void testFindCountMethodWithWrongParameter() throws Exception {
        findAndCheckMethod(Bean.class, "count", Object.class, String.class);
    }

    @Test
    public void testFindToStringMethod() throws Exception {
        findAndCheckMethod(Bean.class, "toString", String.class);
    }

    @Test
    public void testFindCollectionMethod() throws Exception {
        ELType type = findAndCheckMethod(Bean.class, "getComponents", List.class);
        assertEquals(UIComponent.class.getName(), type.getContainerType().getRawName());
    }
}
