/*
 * Copyright (C) 2009 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.reflect;

import static org.truth0.Truth.ASSERT;

import com.google.common.base.Predicate;
import com.google.common.base.Supplier;

import junit.framework.TestCase;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Unit test for {@link TypeToken} and {@link TypeResolver}.
 * 
 * @author Ben Yu
 */
public class TypeTokenResolutionTest extends TestCase {

  private static class Foo<A, B> {
    
    Class<? super A> getClassA() {
      return new TypeToken<A>(getClass()) {}.getRawType();
    }
    
    Class<? super B> getClassB() {
      return new TypeToken<B>(getClass()) {}.getRawType();
    }
    
    Class<? super A[]> getArrayClassA() {
      return new TypeToken<A[]>(getClass()) {}.getRawType();
    }
   
    Type getArrayTypeA() {
      return new TypeToken<A[]>(getClass()) {}.getType();
    }
    
    Class<? super B[]> getArrayClassB() {
      return new TypeToken<B[]>(getClass()) {}.getRawType();
    }
  }
  
  public void testSimpleTypeToken() {
    Foo<String, Integer> foo = new Foo<String, Integer>() {};
    assertEquals(String.class, foo.getClassA());
    assertEquals(Integer.class, foo.getClassB());
    assertEquals(String[].class, foo.getArrayClassA());
    assertEquals(Integer[].class, foo.getArrayClassB());
  }
  
  public void testCompositeTypeToken() {
    Foo<String[], List<int[]>> foo = new Foo<String[], List<int[]>>() {};
    assertEquals(String[].class, foo.getClassA());
    assertEquals(List.class, foo.getClassB());
    assertEquals(String[][].class, foo.getArrayClassA());
    assertEquals(List[].class, foo.getArrayClassB());
  }
  
  private static class StringFoo<T> extends Foo<String, T> {}
  
  public void testPartialSpecialization() {
    StringFoo<Integer> foo = new StringFoo<Integer>() {};
    assertEquals(String.class, foo.getClassA());
    assertEquals(Integer.class, foo.getClassB());
    assertEquals(String[].class, foo.getArrayClassA());
    assertEquals(Integer[].class, foo.getArrayClassB());
    assertEquals(new TypeToken<String[]>() {}.getType(), foo.getArrayTypeA());
  }
  
  public void testTypeArgNotFound() {
    StringFoo<Integer> foo = new StringFoo<Integer>();
    assertEquals(String.class, foo.getClassA());
    assertEquals(String[].class, foo.getArrayClassA());
    assertEquals(Object.class, foo.getClassB());
    assertEquals(Object[].class, foo.getArrayClassB());
  }
  
  private static abstract class Bar<T> {}
  
  private abstract static class Parameterized<O, T, P> {
    ParameterizedType parameterizedType() {
      return new ParameterizedType() {
        @Override public Type[] getActualTypeArguments() {
          return new Type[]{new TypeCapture<P>() {}.capture()};
        }
        @Override public Type getOwnerType() {
          return new TypeCapture<O>() {}.capture();
        }
        @Override public Type getRawType() {
          return new TypeCapture<T>() {}.capture();
        }
      };
    }
  }
  
  public void testResolveType_parameterizedType() {
    @SuppressWarnings("rawtypes") // trying to test raw type
    Parameterized<?, ?, ?> parameterized =
        new Parameterized<TypeTokenResolutionTest, Bar, String>() {};
    TypeResolver typeResolver = TypeResolver.accordingTo(parameterized.getClass());
    ParameterizedType resolved = (ParameterizedType) typeResolver.resolveType(
        parameterized.parameterizedType());
    assertEquals(TypeTokenResolutionTest.class, resolved.getOwnerType());
    assertEquals(Bar.class, resolved.getRawType());
    ASSERT.that(resolved.getActualTypeArguments()).has().item(String.class);
  }
  
  private interface StringListPredicate extends Predicate<List<String>> {}

  private interface IntegerSupplier extends Supplier<Integer> {}
  
  // Intentionally duplicate the Predicate interface to test that it won't cause
  // exceptions
  private interface IntegerStringFunction extends IntegerSupplier,
      Predicate<List<String>>, StringListPredicate {}
  
  public void testGenericInterface() {
    // test the 1st generic interface on the class
    Type fType = Supplier.class.getTypeParameters()[0];
    assertEquals(Integer.class,
        TypeToken.of(IntegerStringFunction.class).resolveType(fType)
            .getRawType());
    
    // test the 2nd generic interface on the class
    Type predicateParameterType = Predicate.class.getTypeParameters()[0];
    assertEquals(new TypeToken<List<String>>() {}.getType(),
        TypeToken.of(IntegerStringFunction.class).resolveType(predicateParameterType)
            .getType());
  }
  
  private static abstract class StringIntegerFoo extends Foo<String, Integer> {}
  
  public void testConstructor_typeArgsResolvedFromAncestorClass() {
    assertEquals(String.class, new StringIntegerFoo() {}.getClassA());
    assertEquals(Integer.class, new StringIntegerFoo() {}.getClassB());
  }
  
  private static class Owner<T> {
    private static abstract class Nested<X> {
      Class<? super X> getTypeArgument() {
        return new TypeToken<X>(getClass()) {}.getRawType();
      }
    }
    
    private abstract class Inner<Y> extends Nested<Y> {
      Class<? super T> getOwnerType() {
        return new TypeToken<T>(getClass()) {}.getRawType();
      }
    }
  }
  
  public void testResolveNestedClass() {
    assertEquals(String.class, new Owner.Nested<String>() {}.getTypeArgument());
  }
  
  public void testResolveInnerClass() {
    assertEquals(String.class,
        new Owner<Integer>().new Inner<String>() {}.getTypeArgument());
  }
  
  public void testResolveOwnerClass() {
    assertEquals(Integer.class,
        new Owner<Integer>().new Inner<String>() {}.getOwnerType());
  }
  
  private static class Mapping<F, T> {
    
    final Type f = new TypeToken<F>(getClass()) {}.getType();
    final Type t = new TypeToken<T>(getClass()) {}.getType();
    
    Type getFromType() {
      return new TypeToken<F>(getClass()) {}.getType();
    }
    
    Type getToType() {
      return new TypeToken<T>(getClass()) {}.getType();
    }
    
    Mapping<T, F> flip() {
      return new Mapping<T, F>() {};
    }
    
    Mapping<F, T> selfMapping() {
      return new Mapping<F, T>() {};
    }
  }
  
  public void testCyclicMapping() {
    Mapping<Integer, String> mapping = new Mapping<Integer, String>();
    assertEquals(mapping.f, mapping.getFromType());
    assertEquals(mapping.t, mapping.getToType());
    assertEquals(mapping.f, mapping.flip().getFromType());
    assertEquals(mapping.t, mapping.flip().getToType());
    assertEquals(mapping.f, mapping.selfMapping().getFromType());
    assertEquals(mapping.t, mapping.selfMapping().getToType());
  }
  
  private static class ParameterizedOuter<T> {
    
    @SuppressWarnings("unused") // used by reflection
    public Inner field;
    
    class Inner {}
  }
  
  public void testInnerClassWithParameterizedOwner() throws Exception {
    Type fieldType = ParameterizedOuter.class.getField("field")
        .getGenericType();
    assertEquals(fieldType,
        TypeToken.of(ParameterizedOuter.class).resolveType(fieldType).getType());
  }
  
  private interface StringIterable extends Iterable<String> {}

  public void testResolveType() {
    assertEquals(String.class, TypeToken.of(this.getClass()).resolveType(String.class).getType());
    assertEquals(String.class,
        TypeToken.of(StringIterable.class)
            .resolveType(Iterable.class.getTypeParameters()[0]).getType());
    assertEquals(String.class,
        TypeToken.of(StringIterable.class)
            .resolveType(Iterable.class.getTypeParameters()[0]).getType());
    try {
      TypeToken.of(this.getClass()).resolveType(null);
      fail();
    } catch (NullPointerException expected) {}
  }
  
  public void testConextIsParameterizedType() throws Exception {
    class Context {
      @SuppressWarnings("unused") // used by reflection
      Map<String, Integer> returningMap() {
        throw new AssertionError();
      }
    }
    Type context = Context.class.getDeclaredMethod("returningMap")
        .getGenericReturnType();
    Type keyType = Map.class.getTypeParameters()[0];
    Type valueType = Map.class.getTypeParameters()[1];
    
    // context is parameterized type
    assertEquals(String.class, TypeToken.of(context).resolveType(keyType).getType());
    assertEquals(Integer.class,
        TypeToken.of(context).resolveType(valueType).getType());
    
    // context is type variable
    assertEquals(keyType, TypeToken.of(keyType).resolveType(keyType).getType());
    assertEquals(valueType, TypeToken.of(valueType).resolveType(valueType).getType());
  }
  
  private static final class GenericArray<T> {
    final Type t = new TypeToken<T>(getClass()) {}.getType();
    final Type array = new TypeToken<T[]>(getClass()) {}.getType();
  }
  
  public void testGenericArrayType() {
    GenericArray<?> genericArray = new GenericArray<Integer>();
    assertEquals(GenericArray.class.getTypeParameters()[0], genericArray.t);
    assertEquals(Types.newArrayType(genericArray.t),
        genericArray.array);
  }
  
  public void testClassWrapper() {
    TypeToken<String> typeExpression = TypeToken.of(String.class);
    assertEquals(String.class, typeExpression.getType());
    assertEquals(String.class, typeExpression.getRawType());
  }
  
  private static class Red<A> {
    private class Orange {
      Class<?> getClassA() {
        return new TypeToken<A>(getClass()) {}.getRawType();
      }

      Red<A> getSelfB() {
        return Red.this;
      }
    }

    Red<A> getSelfA() {
      return this;
    }

    private class Yellow<B> extends Red<B>.Orange {
      Yellow(Red<B> red) {
        red.super();
      }

      Class<?> getClassB() {
        return new TypeToken<B>(getClass()) {}.getRawType();
      }

      Red<A> getA() {
        return getSelfA();
      }

      Red<B> getB() {
        return getSelfB();
      }
    }

    Class<?> getClassDirect() {
      return new TypeToken<A>(getClass()) {}.getRawType();
    }
  }

  public void test1() {
    Red<String> redString = new Red<String>() {};
    Red<Integer> redInteger = new Red<Integer>() {};
    assertEquals(String.class, redString.getClassDirect());
    assertEquals(Integer.class, redInteger.getClassDirect());

    Red<String>.Yellow<Integer> yellowInteger =
        redString.new Yellow<Integer>(redInteger) {};
    assertEquals(Integer.class, yellowInteger.getClassA());
    assertEquals(Integer.class, yellowInteger.getClassB());
    assertEquals(String.class, yellowInteger.getA().getClassDirect());
    assertEquals(Integer.class, yellowInteger.getB().getClassDirect());
  }

  public void test2() {
    Red<String> redString = new Red<String>();
    Red<Integer> redInteger = new Red<Integer>();
    Red<String>.Yellow<Integer> yellowInteger =
        redString.new Yellow<Integer>(redInteger) {};
    assertEquals(Integer.class, yellowInteger.getClassA());
    assertEquals(Integer.class, yellowInteger.getClassB());
  }
  
  private static <T> Type staticMethodWithLocalClass() {
    class MyLocalClass {
      Type getType() {
        return new TypeToken<T>(getClass()) {}.getType();
      }
    }
    return new MyLocalClass().getType();
  }
  
  public void testLocalClassInsideStaticMethod() {
    assertNotNull(staticMethodWithLocalClass());
  }
  
  public void testLocalClassInsideNonStaticMethod() {
    class MyLocalClass<T> {
      Type getType() {
        return new TypeToken<T>(getClass()) {}.getType();
      }
    }
    assertNotNull(new MyLocalClass<String>().getType());
  }
  
  private static <T> Type staticMethodWithAnonymousClass() {
    return new Object() {
      Type getType() {
        return new TypeToken<T>(getClass()) {}.getType();
      }
    }.getType();
  }
  
  public void testAnonymousClassInsideStaticMethod() {
    assertNotNull(staticMethodWithAnonymousClass());
  }
  
  public void testAnonymousClassInsideNonStaticMethod() {
    assertNotNull(new Object() {
      Type getType() {
        return new TypeToken<Object>() {}.getType();
      }
    }.getType());
  }
  
  public void testStaticContext() {
    assertEquals(Map.class, mapType().getRawType());
  }
  
  private abstract static class Holder<T> {
    Type getContentType() {
      return new TypeToken<T>(getClass()) {}.getType();
    }
  }
  
  public void testResolvePrimitiveArrayType() {
    assertEquals(new TypeToken<int[]>() {}.getType(),
        new Holder<int[]>() {}.getContentType());
    assertEquals(new TypeToken<int[][]> () {}.getType(),
        new Holder<int[][]>() {}.getContentType());
  }
  
  public void testResolveToGenericArrayType() {
    GenericArrayType arrayType = (GenericArrayType)
        new Holder<List<int[][]>[]>() {}.getContentType();
    ParameterizedType listType = (ParameterizedType)
        arrayType.getGenericComponentType();
    assertEquals(List.class, listType.getRawType());
    assertEquals(Types.newArrayType(int[].class),
        listType.getActualTypeArguments()[0]);
  }
  
  private abstract class WithGenericBound<A> {

    @SuppressWarnings("unused")
    public <B extends A> void withTypeVariable(List<B> list) {}

    @SuppressWarnings("unused")
    public <E extends Enum<E>> void withRecursiveBound(List<E> list) {}

    @SuppressWarnings("unused")
    public <K extends List<V>, V extends List<K>> void withMutualRecursiveBound(
        List<Map<K, V>> list) {}

    @SuppressWarnings("unused")
    void withWildcardLowerBound(List<? super A> list) {}

    @SuppressWarnings("unused")
    void withWildcardUpperBound(List<? extends A> list) {}
    
    Type getTargetType(String methodName) throws Exception {
      ParameterizedType parameterType = (ParameterizedType)
          WithGenericBound.class.getDeclaredMethod(methodName, List.class)
              .getGenericParameterTypes()[0];
      parameterType = (ParameterizedType)
          TypeToken.of(this.getClass()).resolveType(parameterType).getType();
      return parameterType.getActualTypeArguments()[0];
    }
  }
  
  public void testWithGenericBoundInTypeVariable() throws Exception {
    TypeVariable<?> typeVariable = (TypeVariable<?>)
        new WithGenericBound<String>() {}.getTargetType("withTypeVariable");
    assertEquals(String.class, typeVariable.getBounds()[0]);
  }
  
  public void testWithRecursiveBoundInTypeVariable() throws Exception {
    TypeVariable<?> typeVariable = (TypeVariable<?>)
        new WithGenericBound<String>() {}.getTargetType("withRecursiveBound");
    assertEquals(Types.newParameterizedType(Enum.class, typeVariable),
        typeVariable.getBounds()[0]);
  }
  
  public void testWithMutualRecursiveBoundInTypeVariable() throws Exception {
    ParameterizedType paramType = (ParameterizedType)
        new WithGenericBound<String>() {}
            .getTargetType("withMutualRecursiveBound");
    TypeVariable<?> k = (TypeVariable<?>) paramType.getActualTypeArguments()[0];
    TypeVariable<?> v = (TypeVariable<?>) paramType.getActualTypeArguments()[1];
    assertEquals(Types.newParameterizedType(List.class, v), k.getBounds()[0]);
    assertEquals(Types.newParameterizedType(List.class, k), v.getBounds()[0]);
  }
  
  public void testWithGenericLowerBoundInWildcard() throws Exception {
    WildcardType wildcardType = (WildcardType)
        new WithGenericBound<String>() {}
            .getTargetType("withWildcardLowerBound");
    assertEquals(String.class, wildcardType.getLowerBounds()[0]);
  }
  
  public void testWithGenericUpperBoundInWildcard() throws Exception {
    WildcardType wildcardType = (WildcardType)
        new WithGenericBound<String>() {}
            .getTargetType("withWildcardUpperBound");
    assertEquals(String.class, wildcardType.getUpperBounds()[0]);
  }
  
  public void testInterfaceTypeParameterResolution() throws Exception {
    assertEquals(String.class,
        TypeToken.of(new TypeToken<ArrayList<String>>() {}.getType())
            .resolveType(List.class.getTypeParameters()[0]).getType());
  }
  
  private static TypeToken<Map<Object, Object>> mapType() {
    return new TypeToken<Map<Object, Object>>() {};
  }
  
  // Looks like recursive, but legit.
  private interface WithFalseRecursiveType<K, V> {
    WithFalseRecursiveType<List<V>, String> keyShouldNotResolveToStringList();
    WithFalseRecursiveType<List<K>, List<V>> shouldNotCauseInfiniteLoop();
    SubTypeOfWithFalseRecursiveType<List<V>, List<K>> evenSubTypeWorks();
  }
  
  private interface SubTypeOfWithFalseRecursiveType<K1, V1>
      extends WithFalseRecursiveType<List<K1>, List<V1>> {
    SubTypeOfWithFalseRecursiveType<V1, K1> revertKeyAndValueTypes();
  }
  
  public void testFalseRecursiveType_mappingOnTheSameDeclarationNotUsed() {
    Type returnType = genericReturnType(
        WithFalseRecursiveType.class, "keyShouldNotResolveToStringList");
    TypeToken<?> keyType = TypeToken.of(returnType)
        .resolveType(WithFalseRecursiveType.class.getTypeParameters()[0]);
    assertEquals("java.util.List<V>", keyType.getType().toString());
  }
  
  public void testFalseRecursiveType_notRealRecursiveMapping() {
    Type returnType = genericReturnType(
        WithFalseRecursiveType.class, "shouldNotCauseInfiniteLoop");
    TypeToken<?> keyType = TypeToken.of(returnType)
        .resolveType(WithFalseRecursiveType.class.getTypeParameters()[0]);
    assertEquals("java.util.List<K>", keyType.getType().toString());
  }
  
  public void testFalseRecursiveType_referenceOfSubtypeDoesNotConfuseMe() {
    Type returnType = genericReturnType(
        WithFalseRecursiveType.class, "evenSubTypeWorks");
    TypeToken<?> keyType = TypeToken.of(returnType)
        .resolveType(WithFalseRecursiveType.class.getTypeParameters()[0]);
    assertEquals("java.util.List<java.util.List<V>>", keyType.getType().toString());
  }
  
  public void testFalseRecursiveType_intermediaryTypeMappingDoesNotConfuseMe() {
    Type returnType = genericReturnType(
        SubTypeOfWithFalseRecursiveType.class, "revertKeyAndValueTypes");
    TypeToken<?> keyType = TypeToken.of(returnType)
        .resolveType(WithFalseRecursiveType.class.getTypeParameters()[0]);
    assertEquals("java.util.List<K1>", keyType.getType().toString());
  }
  
  private static Type genericReturnType(Class<?> cls, String methodName) {
    try {
      return cls.getMethod(methodName).getGenericReturnType();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void testTwoStageResolution() {
    class ForTwoStageResolution<A extends Number> {
      <B extends A> void verifyTwoStageResolution() {
        @SuppressWarnings({"unchecked", "rawtypes"})
        Type type = new TypeToken<B>(getClass()) {}
            // B's bound may have already resolved to something.
            // Make sure it can still further resolve when given a context.
            .where(new TypeParameter<B>() {}, (Class) Integer.class)
            .getType();
        assertEquals(Integer.class, type);
      }
    }
    new ForTwoStageResolution<Integer>().verifyTwoStageResolution();
    new ForTwoStageResolution<Integer>() {}.verifyTwoStageResolution();
  }
}
