/*
 * Copyright (C) 2006 Google Inc.
 *
 * 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.inject;

import static com.google.inject.Asserts.assertEqualsBothWays;
import static com.google.inject.Asserts.assertNotSerializable;

import com.google.common.collect.ImmutableList;
import com.google.inject.util.Types;
import java.io.IOException;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;
import junit.framework.TestCase;

/** @author crazybob@google.com (Bob Lee) */
public class TypeLiteralTest extends TestCase {

  public void testWithParameterizedType() {
    TypeLiteral<List<String>> a = new TypeLiteral<List<String>>() {};
    TypeLiteral<List<String>> b = new TypeLiteral<List<String>>(Types.listOf(String.class)) {};
    assertEqualsBothWays(a, b);
  }

  public void testEquality() {
    TypeLiteral<List<String>> t1 = new TypeLiteral<List<String>>() {};
    TypeLiteral<List<String>> t2 = new TypeLiteral<List<String>>() {};
    TypeLiteral<List<Integer>> t3 = new TypeLiteral<List<Integer>>() {};
    TypeLiteral<String> t4 = new TypeLiteral<String>() {};

    assertEqualsBothWays(t1, t2);

    assertFalse(t2.equals(t3));
    assertFalse(t3.equals(t2));

    assertFalse(t2.equals(t4));
    assertFalse(t4.equals(t2));

    TypeLiteral<String> t5 = TypeLiteral.get(String.class);
    assertEqualsBothWays(t4, t5);
  }

  public List<? extends CharSequence> wildcardExtends;

  public void testWithWildcardType() throws NoSuchFieldException, IOException {
    TypeLiteral<?> a = TypeLiteral.get(getClass().getField("wildcardExtends").getGenericType());
    TypeLiteral<?> b = TypeLiteral.get(Types.listOf(Types.subtypeOf(CharSequence.class)));
    TypeLiteral<?> c = new TypeLiteral<List<? extends CharSequence>>() {};
    assertEqualsBothWays(a, b);
    assertEqualsBothWays(b, c);
    assertEquals("java.util.List<? extends java.lang.CharSequence>", a.toString());
    assertEquals("java.util.List<? extends java.lang.CharSequence>", b.toString());
    assertEquals("java.util.List<? extends java.lang.CharSequence>", c.toString());
    assertNotSerializable(a);
    assertNotSerializable(b);
    assertNotSerializable(c);
  }

  @SuppressWarnings("rawtypes") // Testing rawtypes.
  public void testMissingTypeParameter() {
    try {
      new TypeLiteral() {};
      fail();
    } catch (RuntimeException e) {
      /* expected */
    }
  }

  public void testTypesInvolvingArraysForEquality() {
    TypeLiteral<String[]> stringArray = new TypeLiteral<String[]>() {};
    assertEquals(stringArray, new TypeLiteral<String[]>() {});

    TypeLiteral<List<String[]>> listOfStringArray = new TypeLiteral<List<String[]>>() {};
    assertEquals(listOfStringArray, new TypeLiteral<List<String[]>>() {});
  }

  public void testEqualityOfGenericArrayAndClassArray() {
    TypeLiteral<String[]> arrayAsClass = TypeLiteral.get(String[].class);
    TypeLiteral<String[]> arrayAsType = new TypeLiteral<String[]>() {};
    assertEquals(arrayAsClass, arrayAsType);
  }

  public void testEqualityOfMultidimensionalGenericArrayAndClassArray() {
    TypeLiteral<String[][][]> arrayAsClass = TypeLiteral.get(String[][][].class);
    TypeLiteral<String[][][]> arrayAsType = new TypeLiteral<String[][][]>() {};
    assertEquals(arrayAsClass, arrayAsType);
  }

  public void testTypeLiteralsMustHaveRawTypes() {
    // kind of weird, but wildcards and type variables always go to Object
    assertEquals(Object.class, TypeLiteral.get(Types.subtypeOf(Runnable.class)).getRawType());
  }

  /**
   * Unlike Key, TypeLiteral retains full type information and differentiates between {@code
   * int.class} and {@code Integer.class}.
   */
  @SuppressWarnings("rawtypes") // Class literal uses raw type.
  public void testDifferentiationBetweenWrappersAndPrimitives() {
    Class[] primitives =
        new Class[] {
          boolean.class,
          byte.class,
          short.class,
          int.class,
          long.class,
          float.class,
          double.class,
          char.class,
          void.class
        };
    Class[] wrappers =
        new Class[] {
          Boolean.class,
          Byte.class,
          Short.class,
          Integer.class,
          Long.class,
          Float.class,
          Double.class,
          Character.class,
          Void.class
        };

    for (int t = 0; t < primitives.length; t++) {
      @SuppressWarnings("unchecked")
      TypeLiteral primitiveTl = TypeLiteral.get(primitives[t]);
      @SuppressWarnings("unchecked")
      TypeLiteral wrapperTl = TypeLiteral.get(wrappers[t]);

      assertFalse(primitiveTl.equals(wrapperTl));
      assertEquals(primitives[t], primitiveTl.getType());
      assertEquals(wrappers[t], wrapperTl.getType());
      assertEquals(primitives[t], primitiveTl.getRawType());
      assertEquals(wrappers[t], wrapperTl.getRawType());
    }
  }

  public void testSerialization() throws IOException {
    assertNotSerializable(new TypeLiteral<List<String>>() {});
  }

  @SuppressWarnings("rawtypes") // Testing raw types
  public void testTypeVariableWithNoBound() {
    TypeVariable<Class<HasTypeParameters>>[] typeVariables =
        HasTypeParameters.class.getTypeParameters();

    TypeLiteral<?> aTl = TypeLiteral.get(typeVariables[0]);
    assertEquals(Object.class, aTl.getRawType());
    assertEquals("A", aTl.toString());
    TypeVariable<?> aTv = (TypeVariable) aTl.getType();
    assertEquals(HasTypeParameters.class, aTv.getGenericDeclaration());
    assertEquals("A", aTv.getName());
    assertEquals(ImmutableList.<Type>of(Object.class), ImmutableList.copyOf(aTv.getBounds()));
    assertEquals("A", aTv.toString());
    assertEqualsBothWays(aTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[0]));
  }

  public void testTypeVariablesWithSingleBound() {
    @SuppressWarnings("rawtypes") // Class literal uses raw type.
    TypeVariable<Class<HasTypeParameters>>[] typeVariables =
        HasTypeParameters.class.getTypeParameters();

    TypeLiteral<?> cTl = TypeLiteral.get(typeVariables[2]);
    assertEquals(Object.class, cTl.getRawType());
    assertEquals("C", cTl.toString());
    TypeVariable<?> cTv = (TypeVariable) cTl.getType();
    assertEquals(HasTypeParameters.class, cTv.getGenericDeclaration());
    assertEquals("C", cTv.getName());
    assertEquals(ImmutableList.<Type>of(Runnable.class), ImmutableList.copyOf(cTv.getBounds()));
    assertEquals("C", cTv.toString());
    assertEqualsBothWays(cTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[2]));
  }

  public void testTypeVariableWithMultipleBounds() {
    @SuppressWarnings("rawtypes") // Class literal uses raw type.
    TypeVariable<Class<HasTypeParameters>>[] typeVariables =
        HasTypeParameters.class.getTypeParameters();

    TypeLiteral<?> bTl = TypeLiteral.get(typeVariables[1]);
    assertEquals(Object.class, bTl.getRawType());
    assertEquals("B", bTl.toString());
    TypeVariable<?> bTv = (TypeVariable) bTl.getType();
    assertEquals(HasTypeParameters.class, bTv.getGenericDeclaration());
    assertEquals("B", bTv.getName());
    assertEquals(
        ImmutableList.<Type>of(Types.listOf(typeVariables[0]), Runnable.class),
        ImmutableList.copyOf(bTv.getBounds()));
    assertEquals("B", bTv.toString());
    assertEqualsBothWays(bTl, TypeLiteral.get(HasTypeParameters.class.getTypeParameters()[1]));
  }

  static class HasTypeParameters<A, B extends List<A> & Runnable, C extends Runnable> {
    A a;
    B b;
    C c;
  }
}
