/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.inject;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;

import junit.framework.TestCase;

import com.google.inject.ImplementedBy;
import com.google.inject.ProvidedBy;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.util.Types;

public class TypeArgumentsTest
    extends TestCase
{
    static TypeLiteral<Object> OBJECT_TYPE = TypeLiteral.get( Object.class );

    static TypeLiteral<String> STRING_TYPE = TypeLiteral.get( String.class );

    static TypeLiteral<Float> FLOAT_TYPE = TypeLiteral.get( Float.class );

    static TypeLiteral<Short> SHORT_TYPE = TypeLiteral.get( Short.class );

    static TypeLiteral<Number> NUMBER_TYPE = TypeLiteral.get( Number.class );

    @SuppressWarnings( "rawtypes" )
    List rawList;

    List<Short> shortList;

    List<?> wildcardList;

    List<? extends String> wildcardStringList;

    @SuppressWarnings( "rawtypes" )
    Map rawMap;

    Map<String, Float> stringFloatMap;

    Map<?, ?> wildcardMap;

    Map<? extends Float, ? extends Short> wildcardFloatShortMap;

    interface CallableNumber<T extends Number>
        extends Callable<T>
    {
    }

    public void testTypeArguments()
    {
        TypeLiteral<?>[] types;

        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "rawList" ), 0 ) );
        types = TypeArguments.get( getFieldType( "rawList" ) );
        assertEquals( 0, types.length );

        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "rawMap" ), 0 ) );
        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "rawMap" ), 1 ) );
        types = TypeArguments.get( getFieldType( "rawMap" ) );
        assertEquals( 0, types.length );

        assertEquals( SHORT_TYPE, TypeArguments.get( getFieldType( "shortList" ), 0 ) );
        types = TypeArguments.get( getFieldType( "shortList" ) );
        assertEquals( 1, types.length );
        assertEquals( SHORT_TYPE, types[0] );

        assertEquals( STRING_TYPE, TypeArguments.get( getFieldType( "stringFloatMap" ), 0 ) );
        assertEquals( FLOAT_TYPE, TypeArguments.get( getFieldType( "stringFloatMap" ), 1 ) );
        types = TypeArguments.get( getFieldType( "stringFloatMap" ) );
        assertEquals( 2, types.length );
        assertEquals( STRING_TYPE, types[0] );
        assertEquals( FLOAT_TYPE, types[1] );

        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "wildcardList" ), 0 ) );
        types = TypeArguments.get( getFieldType( "wildcardList" ) );
        assertEquals( 1, types.length );
        assertEquals( OBJECT_TYPE, types[0] );

        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "wildcardMap" ), 0 ) );
        assertEquals( OBJECT_TYPE, TypeArguments.get( getFieldType( "wildcardMap" ), 1 ) );
        types = TypeArguments.get( getFieldType( "wildcardMap" ) );
        assertEquals( 2, types.length );
        assertEquals( OBJECT_TYPE, types[0] );
        assertEquals( OBJECT_TYPE, types[1] );

        assertEquals( STRING_TYPE, TypeArguments.get( getFieldType( "wildcardStringList" ), 0 ) );
        types = TypeArguments.get( getFieldType( "wildcardStringList" ) );
        assertEquals( 1, types.length );
        assertEquals( STRING_TYPE, types[0] );

        assertEquals( FLOAT_TYPE, TypeArguments.get( getFieldType( "wildcardFloatShortMap" ), 0 ) );
        assertEquals( SHORT_TYPE, TypeArguments.get( getFieldType( "wildcardFloatShortMap" ), 1 ) );
        types = TypeArguments.get( getFieldType( "wildcardFloatShortMap" ) );
        assertEquals( 2, types.length );
        assertEquals( FLOAT_TYPE, types[0] );
        assertEquals( SHORT_TYPE, types[1] );

        final TypeLiteral<?> genericSuperType = TypeLiteral.get( CallableNumber.class ).getSupertype( Callable.class );

        assertEquals( NUMBER_TYPE, TypeArguments.get( genericSuperType, 0 ) );
        types = TypeArguments.get( genericSuperType );
        assertEquals( 1, types.length );
        assertEquals( NUMBER_TYPE, types[0] );
    }

    @SuppressWarnings( "rawtypes" )
    List[] rawListArray;

    List<Short>[] shortListArray;

    List<?>[] wildcardListArray;

    List<? extends String>[] wildcardStringListArray;

    @SuppressWarnings( "rawtypes" )
    Map[] rawMapArray;

    Map<String, Float>[] stringFloatMapArray;

    Map<?, ?>[] wildcardMapArray;

    Map<? extends Float, ? extends Short>[] wildcardFloatShortMapArray;

    List<String[]> stringArrayList;

    public void testComponentType()
    {
        TypeLiteral<?>[] types;

        types = TypeArguments.get( getFieldType( "rawListArray" ) );
        assertEquals( getFieldType( "rawList" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "rawListArray" ), 0 ) );
        assertEquals( List.class, types[0].getType() );

        types = TypeArguments.get( getFieldType( "rawMapArray" ) );
        assertEquals( getFieldType( "rawMap" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "rawMapArray" ), 0 ) );
        assertEquals( Map.class, types[0].getType() );

        types = TypeArguments.get( getFieldType( "shortListArray" ) );
        assertEquals( getFieldType( "shortList" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "shortListArray" ), 0 ) );
        assertEquals( Types.listOf( Short.class ), types[0].getType() );

        types = TypeArguments.get( getFieldType( "stringFloatMapArray" ) );
        assertEquals( getFieldType( "stringFloatMap" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "stringFloatMapArray" ), 0 ) );
        assertEquals( Types.mapOf( String.class, Float.class ), types[0].getType() );

        types = TypeArguments.get( getFieldType( "wildcardListArray" ) );
        assertEquals( getFieldType( "wildcardList" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "wildcardListArray" ), 0 ) );
        assertEquals( Types.listOf( Types.subtypeOf( Object.class ) ), types[0].getType() );

        types = TypeArguments.get( getFieldType( "wildcardMapArray" ) );
        assertEquals( getFieldType( "wildcardMap" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "wildcardMapArray" ), 0 ) );
        assertEquals( Types.mapOf( Types.subtypeOf( Object.class ), Types.subtypeOf( Object.class ) ),
                      types[0].getType() );

        types = TypeArguments.get( getFieldType( "wildcardStringListArray" ) );
        assertEquals( getFieldType( "wildcardStringList" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "wildcardStringListArray" ), 0 ) );
        assertEquals( Types.listOf( Types.subtypeOf( String.class ) ), types[0].getType() );

        types = TypeArguments.get( getFieldType( "wildcardFloatShortMapArray" ) );
        assertEquals( getFieldType( "wildcardFloatShortMap" ), types[0] );
        assertEquals( types[0], TypeArguments.get( getFieldType( "wildcardFloatShortMapArray" ), 0 ) );
        assertEquals( Types.mapOf( Types.subtypeOf( Float.class ), Types.subtypeOf( Short.class ) ), types[0].getType() );

        types = TypeArguments.get( TypeArguments.get( getFieldType( "stringArrayList" ) )[0] );
        assertEquals( STRING_TYPE, types[0] );
        assertEquals( types[0], TypeArguments.get( TypeArguments.get( getFieldType( "stringArrayList" ), 0 ), 0 ) );
    }

    public void testTypeArgumentRangeChecks()
    {
        try
        {
            TypeArguments.get( getFieldType( "stringFloatMap" ), -1 );
            fail( "Expected IndexOutOfBoundsException" );
        }
        catch ( final IndexOutOfBoundsException e )
        {
        }
        try
        {
            TypeArguments.get( getFieldType( "stringFloatMap" ), 2 );
            fail( "Expected IndexOutOfBoundsException" );
        }
        catch ( final IndexOutOfBoundsException e )
        {
        }

        try
        {
            TypeArguments.get( getFieldType( "wildcardStringListArray" ), -1 );
            fail( "Expected IndexOutOfBoundsException" );
        }
        catch ( final IndexOutOfBoundsException e )
        {
        }
        try
        {
            TypeArguments.get( getFieldType( "wildcardStringListArray" ), 1 );
            fail( "Expected IndexOutOfBoundsException" );
        }
        catch ( final IndexOutOfBoundsException e )
        {
        }
    }

    static class CallableImpl<T>
        implements Callable<T>
    {
        public T call()
            throws Exception
        {
            return null;
        }
    }

    static class CallableNumberImpl<T extends Number>
        implements CallableNumber<T>
    {
        public T call()
            throws Exception
        {
            return null;
        }
    }

    @SuppressWarnings( "rawtypes" )
    static class CallableListImpl
        implements Callable<List>
    {
        public List call()
            throws Exception
        {
            return null;
        }
    }

    @SuppressWarnings( "rawtypes" )
    public void testIsAssignableFrom()
    {
        // === simple types ===

        assertTrue( TypeArguments.isAssignableFrom( TypeLiteral.get( Object.class ), TypeLiteral.get( String.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( TypeLiteral.get( Number.class ), TypeLiteral.get( Short.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( TypeLiteral.get( Collection.class ), TypeLiteral.get( Set.class ) ) );

        // === generic types ===

        assertFalse( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<Collection>>()
        {
        }, TypeLiteral.get( CallableListImpl.class ) ) ); // not assignable since no wild-card
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<List>>()
        {
        }, TypeLiteral.get( CallableListImpl.class ) ) );
        assertFalse( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<String>>()
        {
        }, TypeLiteral.get( CallableListImpl.class ) ) );

        // === unbound type-variables ===

        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable>()
        {
        }, TypeLiteral.get( Callable.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable>()
        {
        }, TypeLiteral.get( CallableImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<String>>()
        {
        }, TypeLiteral.get( CallableImpl.class ) ) );
        assertFalse( TypeArguments.isAssignableFrom( new TypeLiteral<CallableImpl>()
        {
        }, TypeLiteral.get( Callable.class ) ) );

        // === bound type-variables ===

        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<CallableNumber>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<CallableNumber<Number>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<CallableNumber<Float>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertFalse( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<String>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) ); // mismatched type-bounds

        // === unbound wild-cards ===

        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<?>>()
        {
        }, TypeLiteral.get( CallableImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<?>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<CallableNumber<?>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );

        // === bound wild-cards ===

        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<? extends Collection>>()
        {
        }, TypeLiteral.get( CallableListImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<? extends Number>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertTrue( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<? extends Float>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );
        assertFalse( TypeArguments.isAssignableFrom( new TypeLiteral<Callable<? extends String>>()
        {
        }, TypeLiteral.get( CallableNumberImpl.class ) ) );

        // === array types ===

        assertTrue( TypeArguments.isAssignableFrom( TypeLiteral.get( Types.arrayOf( Object.class ) ),
                                                    TypeLiteral.get( Types.arrayOf( String.class ) ) ) );
        assertTrue( TypeArguments.isAssignableFrom( TypeLiteral.get( Types.arrayOf( Number.class ) ),
                                                    TypeLiteral.get( Types.arrayOf( Float.class ) ) ) );

        // === mismatched types ===

        assertFalse( TypeArguments.isAssignableFrom( TypeLiteral.get( Types.arrayOf( Object.class ) ),
                                                     TypeLiteral.get( Types.listOf( Object.class ) ) ) );
        assertFalse( TypeArguments.isAssignableFrom( TypeLiteral.get( Types.listOf( Object.class ) ),
                                                     TypeLiteral.get( Types.arrayOf( Object.class ) ) ) );

        // === corner case ===

        final Type T =
            (Type) Proxy.newProxyInstance( getClass().getClassLoader(), new Class<?>[] { TypeVariable.class },
                                           new InvocationHandler()
                                           {
                                               public Object invoke( final Object proxy, final Method method,
                                                                     final Object[] args )
                                                   throws Throwable
                                               {
                                                   final String name = method.getName();
                                                   if ( "getBounds".equals( name ) )
                                                   {
                                                       return new Type[] { String.class };
                                                   }
                                                   if ( "getName".equals( name ) )
                                                   {
                                                       return "T";
                                                   }
                                                   if ( "hashCode".equals( name ) )
                                                   {
                                                       return hashCode();
                                                   }
                                                   if ( "equals".equals( name ) )
                                                   {
                                                       return equals( args[0] );
                                                   }
                                                   return null;
                                               }
                                           } );

        final Type callableT =
            (Type) Proxy.newProxyInstance( getClass().getClassLoader(), new Class<?>[] { ParameterizedType.class },
                                           new InvocationHandler()
                                           {
                                               public Object invoke( final Object proxy, final Method method,
                                                                     final Object[] args )
                                                   throws Throwable
                                               {
                                                   final String name = method.getName();
                                                   if ( "getActualTypeArguments".equals( name ) )
                                                   {
                                                       return new Type[] { T };
                                                   }
                                                   if ( "getRawType".equals( name ) )
                                                   {
                                                       return Callable.class;
                                                   }
                                                   if ( "hashCode".equals( name ) )
                                                   {
                                                       return hashCode();
                                                   }
                                                   if ( "equals".equals( name ) )
                                                   {
                                                       return equals( args[0] );
                                                   }
                                                   return null;
                                               }
                                           } );

        assertFalse( TypeArguments.isAssignableFrom( TypeLiteral.get( callableT ), TypeLiteral.get( Callable.class ) ) );

        assertFalse( TypeArguments.isAssignableFrom( TypeLiteral.get( callableT ),
                                                     TypeLiteral.get( CallableNumberImpl.class ) ) );
    }

    public void testIsConcrete()
    {
        assertFalse( TypeArguments.isConcrete( Map.class ) );
        assertFalse( TypeArguments.isConcrete( AbstractMap.class ) );
        assertTrue( TypeArguments.isConcrete( HashMap.class ) );

        assertFalse( TypeArguments.isConcrete( new TypeLiteral<Map<String, String>>()
        {
        } ) );
        assertFalse( TypeArguments.isConcrete( new TypeLiteral<AbstractMap<String, String>>()
        {
        } ) );
        assertTrue( TypeArguments.isConcrete( new TypeLiteral<HashMap<String, String>>()
        {
        } ) );
    }

    @ImplementedBy( Object.class )
    static interface Implicit1<T>
    {
    }

    static class SomeProvider
        implements Provider<Object>
    {
        public Object get()
        {
            return null;
        }
    }

    @ProvidedBy( SomeProvider.class )
    static interface Implicit2<T>
    {
    }

    public void testIsImplicit()
    {
        assertFalse( TypeArguments.isImplicit( Map.class ) );
        assertFalse( TypeArguments.isImplicit( AbstractMap.class ) );
        assertTrue( TypeArguments.isImplicit( HashMap.class ) );

        assertFalse( TypeArguments.isImplicit( new TypeLiteral<Map<String, String>>()
        {
        } ) );
        assertFalse( TypeArguments.isImplicit( new TypeLiteral<AbstractMap<String, String>>()
        {
        } ) );
        assertTrue( TypeArguments.isImplicit( new TypeLiteral<HashMap<String, String>>()
        {
        } ) );

        assertTrue( TypeArguments.isImplicit( Implicit1.class ) );
        assertTrue( TypeArguments.isImplicit( Implicit2.class ) );

        assertTrue( TypeArguments.isImplicit( new TypeLiteral<Implicit1<String>>()
        {
        } ) );
        assertTrue( TypeArguments.isImplicit( new TypeLiteral<Implicit2<String>>()
        {
        } ) );
    }

    private static TypeLiteral<?> getFieldType( final String name )
    {
        try
        {
            return TypeLiteral.get( TypeArgumentsTest.class.getDeclaredField( name ).getGenericType() );
        }
        catch ( final NoSuchFieldException e )
        {
            throw new IllegalArgumentException( "Unknown test field " + name );
        }
    }
}
