/*
 * Copyright (C) 2008 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.util;

import static com.google.inject.Asserts.assertContains;
import static com.google.inject.Asserts.assertEqualWhenReserialized;
import static com.google.inject.Asserts.assertEqualsBothWays;
import static com.google.inject.util.Types.subtypeOf;
import static com.google.inject.util.Types.supertypeOf;

import com.google.inject.TypeLiteral;
import com.google.inject.internal.MoreTypes;
import java.io.IOException;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.List;
import java.util.Map;
import java.util.Set;
import junit.framework.Assert;
import junit.framework.TestCase;

/** @author jessewilson@google.com (Jesse Wilson) */
public class TypesTest extends TestCase {

  // generic types for comparison
  Map<String, Integer> a;
  Inner<Float, Double> b;
  List<Set<String>[][]> c;
  List<String> d;
  Set<String> e;
  Outer<String>.Inner f;

  private ParameterizedType mapStringInteger;
  private ParameterizedType innerFloatDouble;
  private ParameterizedType listSetStringArray;
  private ParameterizedType listString;
  private ParameterizedType setString;
  private ParameterizedType outerInner;
  private GenericArrayType setStringArray;

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    mapStringInteger = (ParameterizedType) getClass().getDeclaredField("a").getGenericType();
    innerFloatDouble = (ParameterizedType) getClass().getDeclaredField("b").getGenericType();
    listSetStringArray = (ParameterizedType) getClass().getDeclaredField("c").getGenericType();
    listString = (ParameterizedType) getClass().getDeclaredField("d").getGenericType();
    setString = (ParameterizedType) getClass().getDeclaredField("e").getGenericType();
    outerInner = (ParameterizedType) getClass().getDeclaredField("f").getGenericType();
    setStringArray = (GenericArrayType) listSetStringArray.getActualTypeArguments()[0];
  }

  public void testListSetMap() {
    assertEqualsBothWays(mapStringInteger, Types.mapOf(String.class, Integer.class));
    assertEqualsBothWays(listString, Types.listOf(String.class));
    assertEqualsBothWays(setString, Types.setOf(String.class));
  }

  public void testDefensiveCopies() {
    Type[] arguments = new Type[] {String.class, Integer.class};
    ParameterizedType parameterizedType = Types.newParameterizedType(Map.class, arguments);
    arguments[0] = null;
    assertEquals(String.class, parameterizedType.getActualTypeArguments()[0]);
    parameterizedType.getActualTypeArguments()[1] = null;
    assertEquals(Integer.class, parameterizedType.getActualTypeArguments()[1]);
  }

  public void testTypeWithOwnerType() {
    ParameterizedType actual =
        Types.newParameterizedTypeWithOwner(
            TypesTest.class, Inner.class, Float.class, Double.class);
    assertEquals(TypesTest.class, actual.getOwnerType());
    assertEqualsBothWays(innerFloatDouble, actual);
    // The JDK prints this out as:
    //     com.google.inject.util.TypesTest.com.google.inject.util.TypesTest$Inner<java.lang.Float, java.lang.Double>
    // and we think that's wrong, so the assertEquals comparison is worthless. :-(
    //    assertEquals(innerFloatDouble.toString(), actual.toString());

    // We think the correct comparison is:
    assertEquals(
        "com.google.inject.util.TypesTest$Inner<java.lang.Float, java.lang.Double>",
        actual.toString());
  }

  public void testTypeParametersMustNotBePrimitives() {
    try {
      Types.newParameterizedType(Map.class, String.class, int.class);
      fail();
    } catch (IllegalArgumentException expected) {
      assertContains(
          expected.getMessage(), "Primitive types are not allowed in type parameters: int");
    }
  }

  public List<? extends CharSequence> wildcardExtends;
  public List<? super CharSequence> wildcardSuper;
  public List<?> wildcardObject;

  public void testWildcardTypes() throws NoSuchFieldException, IOException {
    assertEqualsBothWays(getWildcard("wildcardSuper"), supertypeOf(CharSequence.class));
    assertEqualsBothWays(getWildcard("wildcardExtends"), subtypeOf(CharSequence.class));
    assertEqualsBothWays(getWildcard("wildcardObject"), subtypeOf(Object.class));

    assertEquals("? super java.lang.CharSequence", supertypeOf(CharSequence.class).toString());
    assertEquals("? extends java.lang.CharSequence", subtypeOf(CharSequence.class).toString());
    assertEquals("?", subtypeOf(Object.class).toString());

    assertEqualWhenReserialized(supertypeOf(CharSequence.class));
    assertEqualWhenReserialized(subtypeOf(CharSequence.class));
  }

  public void testWildcardBoundsMustNotBePrimitives() {
    try {
      supertypeOf(int.class);
      fail();
    } catch (IllegalArgumentException expected) {
      assertContains(
          expected.getMessage(), "Primitive types are not allowed in wildcard bounds: int");
    }

    try {
      subtypeOf(int.class);
      fail();
    } catch (IllegalArgumentException expected) {
      assertContains(
          expected.getMessage(), "Primitive types are not allowed in wildcard bounds: int");
    }
  }

  private WildcardType getWildcard(String fieldName) throws NoSuchFieldException {
    ParameterizedType type = (ParameterizedType) getClass().getField(fieldName).getGenericType();
    return (WildcardType) type.getActualTypeArguments()[0];
  }

  public void testEqualsAndHashcode() {
    ParameterizedType parameterizedType =
        Types.newParameterizedType(Map.class, String.class, Integer.class);
    assertEqualsBothWays(mapStringInteger, parameterizedType);
    assertEquals(mapStringInteger.toString(), parameterizedType.toString());

    GenericArrayType genericArrayType =
        Types.arrayOf(Types.arrayOf(Types.newParameterizedType(Set.class, String.class)));
    assertEqualsBothWays(setStringArray, genericArrayType);
    assertEquals(setStringArray.toString(), genericArrayType.toString());
  }

  public void testToString() {
    Assert.assertEquals("java.lang.String", MoreTypes.typeToString(String.class));
    assertEquals("java.util.Set<java.lang.String>[][]", MoreTypes.typeToString(setStringArray));
    assertEquals(
        "java.util.Map<java.lang.String, java.lang.Integer>",
        MoreTypes.typeToString(mapStringInteger));
    assertEquals(
        "java.util.List<java.util.Set<java.lang.String>[][]>",
        MoreTypes.typeToString(listSetStringArray));
    assertEquals(innerFloatDouble.toString(), MoreTypes.typeToString(innerFloatDouble));
  }

  static class Owning<A> {}

  /** Ensure that owning types are required when necessary, and forbidden otherwise. */
  public void testCanonicalizeRequiresOwnerTypes() {
    try {
      Types.newParameterizedType(Owning.class, String.class);
      fail();
    } catch (IllegalArgumentException expected) {
      assertContains(expected.getMessage(), "No owner type for enclosed " + Owning.class);
    }

    try {
      Types.newParameterizedTypeWithOwner(Object.class, Set.class, String.class);
      fail("Expected IllegalArgumentException");
    } catch (IllegalArgumentException expected) {
      assertContains(expected.getMessage(), "Owner type for unenclosed " + Set.class);
    }
  }

  @SuppressWarnings("UnusedDeclaration")
  class Inner<T1, T2> {}

  public void testInnerParameterizedEvenWithZeroArgs() {
    TypeLiteral<Outer<String>.Inner> type = new TypeLiteral<Outer<String>.Inner>() {};
    assertEqualsBothWays(outerInner, type.getType());

    ParameterizedType parameterizedType = (ParameterizedType) type.getType();
    assertEquals(0, parameterizedType.getActualTypeArguments().length);
    assertEquals(new TypeLiteral<Outer<String>>() {}.getType(), parameterizedType.getOwnerType());
    assertEquals(Outer.Inner.class, parameterizedType.getRawType());
  }

  static class Outer<T> {
    @SuppressWarnings("ClassCanBeStatic")
    class Inner {}
  }
}
