/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.codegen.test.meta;

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaConstructor;
import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaParameterizedType;
import org.jboss.errai.codegen.meta.MetaType;
import org.jboss.errai.codegen.meta.MetaTypeVariable;
import org.jboss.errai.codegen.meta.MetaWildcardType;
import org.jboss.errai.codegen.test.model.ClassWithArrayGenerics;
import org.jboss.errai.codegen.test.model.ClassWithGenericCollections;
import org.jboss.errai.codegen.test.model.ClassWithGenericMethods;
import org.jboss.errai.codegen.test.model.HasManyConstructors;
import org.jboss.errai.codegen.test.model.ObjectWithNested;
import org.jboss.errai.codegen.test.model.ParameterizedClass;
import org.jboss.errai.codegen.test.model.TestInterface;
import org.jboss.errai.codegen.test.model.tree.Child;
import org.jboss.errai.codegen.test.model.tree.Grandparent;
import org.jboss.errai.codegen.test.model.tree.GrandparentInterface;
import org.jboss.errai.codegen.test.model.tree.GrandparentSuperInterface;
import org.jboss.errai.codegen.test.model.tree.IsolatedInterface;
import org.jboss.errai.codegen.test.model.tree.Parent;
import org.jboss.errai.codegen.test.model.tree.ParentInterface;
import org.jboss.errai.codegen.test.model.tree.ParentSuperInterface1;
import org.jboss.errai.codegen.test.model.tree.ParentSuperInterface2;
import org.jboss.errai.codegen.util.GenUtil;
import org.junit.Test;
import org.mvel2.util.NullType;

import com.google.common.collect.Lists;
import com.google.gwt.core.ext.typeinfo.NotFoundException;

/**
 * Subclassable container for the test cases that guarantee an implementation of
 * MetaClass conforms to the general contract. Each implementation of MetaClass
 * should have a corresponding subclass of this test. This way, every
 * implementation of MetaClass will be subjected to the same set of tests.
 *
 * @author Christian Sadilek <csadilek@redhat.com>
 * @author Jonathan Fuerth <jfuerth@redhat.com>
 * @author Mike Brock <cbrock@redhat.com>
 */
public abstract class AbstractMetaClassTest {

  /**
   * Requests the MetaClass impl from the subclass and checks that it's the
   * expected subtype of MetaClass before returning it.
   *
   * @param javaClass
   * @return
   */
  private MetaClass getMetaClass(final Class<?> javaClass) {
    final MetaClass impl = getMetaClassImpl(javaClass);
    assertEquals(getTypeOfMetaClassBeingTested(), impl.getClass());
    return impl;
  }

  /**
   * Returns a MetaClass object--of the type being tested--that represents {@code javaClass}.
   *
   * @param javaClass The Java class type being requested.
   * @return an instance of the subtype of MetaClass that's being tested.
   */
  protected abstract MetaClass getMetaClassImpl(Class<?> javaClass);

  /**
   * Returns the type of MetaClass that will be returned from {@link #getMetaClassImpl(Class)}.
   */
  protected abstract Class<? extends MetaClass> getTypeOfMetaClassBeingTested();

  @Test
  public void testInternalNameForOneDimensionalPrimitiveArray() {
   final String internalName = getMetaClass(char[].class).getInternalName();
   assertEquals("Wrong internal name generated for one-dimensional primitive array",
       "[C", internalName);
  }

  @Test
  public void testInternalNameForOneDimensionalObjectArray() {
   final String internalName = getMetaClass(String[].class).getInternalName();
   assertEquals("Wrong internal name generated for one-dimensional object array",
       "[Ljava/lang/String;", internalName);
  }

  @Test
  public void testInternalNameForMultiDimensionalPrimitiveArray() {
   final String internalName = getMetaClass(char[][].class).getInternalName();
   assertEquals("Wrong internal name generated for multidimensional primitive array",
       "[[C", internalName);
  }

  @Test
  public void testInternalNameForMultiDimensionalObjectArray() {
   final String internalName = getMetaClass(String[][].class).getInternalName();
   assertEquals("Wrong internal name generated for multidimensional object array",
       "[[Ljava/lang/String;", internalName);
  }

  @Test
  public void testObjectIsAssignableFromNull() throws Exception {
	  // This test checks the valid case:
	  // Object example = null;

	  final MetaClass metaObject = getMetaClass(Object.class);
	  final MetaClass metaNull = getMetaClass(NullType.class);

	  assertTrue(metaObject.isAssignableFrom(metaNull));
  }

  @Test
  public void testChildIsAssignableFromNull() throws Exception {
	  // This test checks the valid case:
	  // Child example = null;

	  final MetaClass metaChild = getMetaClass(Child.class);
	  final MetaClass metaNull = getMetaClass(NullType.class);

	  assertTrue(metaChild.isAssignableFrom(metaNull));
  }

  @Test
  public void testNullIsAssignableToChild() throws Exception {
	  // This test checks the valid case:
	  // Child example = null;

	  final MetaClass metaChild = getMetaClass(Child.class);
	  final MetaClass metaNull = getMetaClass(NullType.class);

	  assertTrue(metaNull.isAssignableTo(metaChild));
  }

  @Test
  public void testIsAssignableFromComparisonForNested() {
    final MetaClass interfaceClass = getMetaClass(TestInterface.class);
    final MetaClass metaHolderClass = getMetaClass(ObjectWithNested.class);

    // dig out the nested interface from the holder class and ensure it's what we were looking for
    final MetaClass nestedInterface = metaHolderClass.getDeclaredClasses()[0];
    assertEquals("MyNestedInterface", nestedInterface.getName());
    assertEquals(getTypeOfMetaClassBeingTested(), nestedInterface.getClass());

    assertTrue("MyNestedInterface should be assignable from TestInterface",
            interfaceClass.isAssignableFrom(nestedInterface));
  }

  @Test
  public void testChildIsAssignableFromChild() {
    // This test checks the valid case:
    // Child example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);

    assertTrue(metaChild.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsAssignableToChild() {
    // This test checks the valid case:
    // Child example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);

    assertTrue(metaChild.isAssignableTo(metaChild));
  }

  @Test
  public void testParentIsAssignableFromChild() {
    // This test checks the valid case:
    // Parent example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaParent = getMetaClass(Parent.class);

    assertTrue(metaParent.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromParent() {
    // This test checks the disallowed case:
    // Child child = new Parent();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaParent = getMetaClass(Parent.class);

    assertFalse(metaChild.isAssignableFrom(metaParent));
  }

  @Test
  public void testGrandParentIsAssignableFromChild() {
    // This test checks the valid case:
    // Grandparent example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGrandparent = getMetaClass(Grandparent.class);

    assertTrue(metaGrandparent.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromGrandParent() {
    // This test checks the disallowed case:
    // Child child = new Grandparent();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGrandparent = getMetaClass(Grandparent.class);

    assertFalse(metaChild.isAssignableFrom(metaGrandparent));
  }

  /**
   * This is a regression test for ERRAI-238.
   */
  @Test
  public void testUncleIsAssignableFromChild() {
    // This test checks the valid case:
    // ParentInterface example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncle = getMetaClass(ParentInterface.class);

    assertTrue(metaUncle.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromUncle() {
    // This test checks the disallowed case:
    // Child child = new ParentInterface() {};

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncle = getMetaClass(ParentInterface.class);

    assertFalse(metaChild.isAssignableFrom(metaUncle));
  }

  /**
   * This is a regression test for ERRAI-238.
   */
  @Test
  public void testUncleInLawIsAssignableFromChild() {
    // This test checks the valid case:
    // ParentSuperInterface1 example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncleInLaw = getMetaClass(ParentSuperInterface1.class);

    assertTrue(metaUncleInLaw.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromUncleInLaw() {
    // This test checks the disallowed case:
    // Child child = new ParentSuperInterface1() {};

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncleInLaw = getMetaClass(ParentSuperInterface1.class);

    assertFalse(metaChild.isAssignableFrom(metaUncleInLaw));
  }

  /**
   * This is a regression test for ERRAI-238.
   */
  @Test
  public void testUncleInLaw2IsAssignableFromChild() {
    // This test checks the valid case:
    // ParentSuperInterface2 example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncleInLaw = getMetaClass(ParentSuperInterface2.class);

    assertTrue(metaUncleInLaw.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromUncleInLaw2() {
    // This test checks the disallowed case:
    // Child child = new ParentSuperInterface2() {};

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncleInLaw = getMetaClass(ParentSuperInterface2.class);

    assertFalse(metaChild.isAssignableFrom(metaUncleInLaw));
  }

  /**
   * This is a regression test for ERRAI-238.
   */
  @Test
  public void testGreatUncleIsAssignableFromChild() {
    // This test checks the valid case:
    // GrandparentInterface example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGreatUncle = getMetaClass(GrandparentInterface.class);

    assertTrue(metaGreatUncle.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromGreatUncle() {
    // This test checks the disallowed case:
    // Child child = new GrandparentInterface() {};

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGreatUncle = getMetaClass(GrandparentInterface.class);

    assertFalse(metaChild.isAssignableFrom(metaGreatUncle));
  }

  /**
   * This is a regression test for ERRAI-238.
   */
  @Test
  public void testGreatUncleInLawIsAssignableFromChild() {
    // This test checks the valid case:
    //GrandparentSuperInterface example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGreatUncleInLaw = getMetaClass(GrandparentSuperInterface.class);

    assertTrue(metaGreatUncleInLaw.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromGreatUncleInLaw() {
    // This test checks the disallowed case:
    // Child child = new GrandparentSuperInterface() {};

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaGreatUncleInLaw = getMetaClass(GrandparentSuperInterface.class);

    assertFalse(metaChild.isAssignableFrom(metaGreatUncleInLaw));
  }

  @Test
  public void testObjectIsAssignableFromChild() {
    // This test checks the valid case:
    //Object example = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaObject = getMetaClass(Object.class);

    assertTrue(metaObject.isAssignableFrom(metaChild));
  }

  @Test
  public void testChildIsNotAssignableFromObject() {
    // This test checks the disallowed case:
    // Child child = new Object();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaObject = getMetaClass(Object.class);

    assertFalse(metaChild.isAssignableFrom(metaObject));
  }

  @Test
  public void testObjectIsAssignableFromIsolatedInterface() {
    // This test checks the valid case:
    // Object example = new IsolatedInterface() {};

    final MetaClass metaInterface = getMetaClass(IsolatedInterface.class);
    final MetaClass metaObject = getMetaClass(Object.class);

    assertTrue(metaObject.isAssignableFrom(metaInterface));
  }

  @Test
  public void testIsolatedInterfaceIsNotAssignableFromObject() {
    // This test checks the disallowed case:
    // IsolatedInterface ii = new Object();

    final MetaClass metaInterface = getMetaClass(IsolatedInterface.class);
    final MetaClass metaObject = getMetaClass(Object.class);

    assertFalse(metaInterface.isAssignableFrom(metaObject));
  }

  @Test
  public void testUncleIsAssignableToChild() {
    // This test checks the allowed case:
	// ParentInterface pi = new Child();

    final MetaClass metaChild = getMetaClass(Child.class);
    final MetaClass metaUncle = getMetaClass(ParentInterface.class);

    assertTrue(metaChild.isAssignableTo(metaUncle));
  }

  @Test
  public void testNoDuplicateMethodsInClassHierarchy() throws NotFoundException {
    final MetaClass child = getMetaClass(Child.class);

    final List<MetaMethod> foundMethods = new ArrayList<>();
    for (final MetaMethod m : child.getMethods()) {
      if (m.getName().equals("interfaceMethodOverriddenMultipleTimes")) {
        foundMethods.add(m);
      }
    }

    assertEquals("Only one copy of the method should have been found", 1, foundMethods.size());
  }

  @Test
  public void testSuperClass() throws Exception {
    final MetaClass child = getMetaClass(Child.class);
    final MetaClass parent = child.getSuperClass();

    assertEquals(getMetaClass(Parent.class), parent);
  }

  @Test
  public void testNamingMethods() throws Exception {
    final MetaClass child = getMetaClass(Child.class);

    assertEquals(Child.class.getSimpleName(), child.getName());
    assertEquals(Child.class.getName(), child.getFullyQualifiedName());
    assertEquals(Child.class.getName(), child.getCanonicalName());
    assertEquals("L" + Child.class.getName().replace('.', '/') + ";", child.getInternalName());
  }

  @Test
  public void testAccessModifiersForPublicTopLevelClass() throws Exception {
    final MetaClass child = getMetaClass(Child.class);

    assertTrue(child.isPublic());
    assertFalse(child.isProtected());
    assertFalse(child.isPrivate());

    assertTrue(child.isDefaultInstantiable());
  }

  // TODO: add private, pkg private, protected methods to Child, Parent, Grandparent, and test getDeclaredMethods()
//    System.out.println("--gwt methods--");
//    for (MetaMethod method : Child.class.getName().getDeclaredMethods()) {
//      System.out.println(method.toString());
//    }
//    assertEquals(new HashSet<MetaMethod>(Arrays.asList(Child.class.getName().getDeclaredMethods())),
//            new HashSet<MetaMethod>(Arrays.asList(javaMC.getDeclaredMethods())));

  @Test
  public void testGetInterfaces() throws Exception {
    final MetaClass grandparent = getMetaClass(Grandparent.class);
    assertEquals(1, grandparent.getInterfaces().length);
    assertEquals(
            Arrays.asList(getMetaClass(GrandparentInterface.class)),
            Arrays.asList(grandparent.getInterfaces()));
  }

  @Test
  public void testFieldWithStringTypeParam() throws Exception {
    final MetaClass metaClass = getMetaClass(ClassWithGenericCollections.class);
    final MetaField field = metaClass.getDeclaredField("hasStringParam");
    assertNotNull(field);

    assertEquals("Collection", field.getType().getName());
    assertEquals("java.util.Collection", field.getType().getFullyQualifiedName());
    assertEquals("<java.lang.String>", field.getType().getParameterizedType().toString());
    assertEquals("java.util.Collection<java.lang.String>", field.getType().getFullyQualifiedNameWithTypeParms());
    assertEquals("java.util.Collection", field.getType().getErased().getFullyQualifiedNameWithTypeParms());
    assertEquals(
            Arrays.asList(getMetaClass(String.class)),
            Arrays.asList(field.getType().getParameterizedType().getTypeParameters()));
  }

  @Test
  public void testFieldWithStringBoundedWildcardTypeParam() throws Exception {
    final MetaClass metaClass = getMetaClass(ClassWithGenericCollections.class);
    final MetaField field = metaClass.getDeclaredField("hasWildcardExtendsStringParam");
    assertNotNull(field);

    assertEquals("Collection", field.getType().getName());
    assertEquals("java.util.Collection", field.getType().getFullyQualifiedName());
    assertEquals("<? extends java.lang.String>", field.getType().getParameterizedType().toString());
    assertEquals("java.util.Collection<? extends java.lang.String>", field.getType().getFullyQualifiedNameWithTypeParms());
    assertEquals("java.util.Collection", field.getType().getErased().getFullyQualifiedNameWithTypeParms());

    assertEquals(1, field.getType().getParameterizedType().getTypeParameters().length);
    final MetaWildcardType typeParam = (MetaWildcardType) field.getType().getParameterizedType().getTypeParameters()[0];

    assertEquals("Should have no lower bound",
            Arrays.asList(),
            Arrays.asList(typeParam.getLowerBounds()));

    assertEquals("Upper bound should be java.lang.String",
            Arrays.asList(getMetaClass(String.class)),
            Arrays.asList(typeParam.getUpperBounds()));
  }

  @Test
  public void testFieldWithUnboundedTypeVarParam() throws Exception {
    final MetaClass metaClass = getMetaClass(ClassWithGenericCollections.class);
    final MetaField field = metaClass.getDeclaredField("hasUnboundedTypeVarFromClass");
    assertNotNull(field);

    assertEquals("Collection", field.getType().getName());
    assertEquals("java.util.Collection", field.getType().getFullyQualifiedName());
    assertEquals("java.util.Collection<T>", field.getType().getParameterizedType().getName());
    assertEquals("java.util.Collection<T>", field.getType().getFullyQualifiedNameWithTypeParms());
    assertEquals("java.util.Collection", field.getType().getErased().getFullyQualifiedNameWithTypeParms());

    assertEquals(1, field.getType().getParameterizedType().getTypeParameters().length);
    final MetaTypeVariable typeVar = (MetaTypeVariable) field.getType().getParameterizedType().getTypeParameters()[0];

    assertEquals("T", typeVar.getName());
    assertEquals("Should have no upper bound",
            Arrays.asList(getMetaClass(Object.class)),
            Arrays.asList(typeVar.getBounds()));
  }

  @Test
  public void testFieldWithSingleUpperBoundedTypeVarParam() throws Exception {
    final MetaClass metaClass = getMetaClass(ClassWithGenericCollections.class);
    final MetaMethod field = metaClass.getDeclaredMethod("hasSingleBoundedTypeVarFromSelf", new Class[] {});
    assertNotNull(field);

    final MetaTypeVariable returnType = (MetaTypeVariable) field.getGenericReturnType();
    assertEquals("B", returnType.getName());

    assertEquals("Should have a single upper bound",
            Arrays.asList(getMetaClass(Serializable.class)),
            Arrays.asList(returnType.getBounds()));
  }

  @Test
  public void testFieldWithTwoUpperBoundedTypeVarParam() throws Exception {
    final MetaClass metaClass = getMetaClass(ClassWithGenericCollections.class);
    final MetaMethod field = metaClass.getDeclaredMethod("hasDoubleBoundedTypeVarFromSelf", new Class[] {});
    assertNotNull(field);

    final MetaTypeVariable returnType = (MetaTypeVariable) field.getGenericReturnType();
    assertEquals("B", returnType.getName());

    assertEquals("Should have two upper bounds",
            Arrays.asList(getMetaClass(Collection.class), getMetaClass(Serializable.class)),
            Arrays.asList(returnType.getBounds()));
  }

  @Test
  public void testEraseNonGenericType() throws Exception {
    final MetaClass child = getMetaClass(Child.class);
    assertSame(child, child.getErased());
  }

  @Test
  public void testEraseParameterizedTopLevelType() throws Exception {
    final MetaClass parameterized = getMetaClass(ParameterizedClass.class);
    assertEquals("ParameterizedClass", parameterized.getName());
    assertEquals("org.jboss.errai.codegen.test.model.ParameterizedClass", parameterized.getFullyQualifiedName());
    assertNull(parameterized.getParameterizedType());

    // I think this would be correct, but right now we get the raw type name
    //assertEquals("org.jboss.errai.codegen.test.model.ParameterizedClass<T>", parameterized.getFullyQualifiedNameWithTypeParms());

    assertEquals("org.jboss.errai.codegen.test.model.ParameterizedClass", parameterized.getErased().getFullyQualifiedNameWithTypeParms());
  }

  @Test
  public void testMethodObjectReturnType() {
    final MetaClass c = getMetaClass(ClassWithGenericMethods.class);
    final MetaMethod method = c.getMethod("methodReturningObject", new Class[] {});
    assertEquals("java.lang.Object", method.getReturnType().getFullyQualifiedNameWithTypeParms());
    assertEquals(getTypeOfMetaClassBeingTested(), method.getReturnType().getClass());

    // the generic return type should be the same: plain old Object
    assertEquals(getMetaClass(Object.class), method.getGenericReturnType());
  }

  @Test
  public void testMethodReturnTypeWithWildcardParameter() {
    final MetaClass c = getMetaClass(ClassWithGenericMethods.class);
    final MetaMethod method = c.getMethod("methodReturningUnboundedWildcardCollection", new Class[] {});

    // TODO (ERRAI-459) decide whether it's correct to have the type param present or not
    // then adjust this assertion to strict equality rather than startsWith()
    assertTrue(method.getReturnType().getFullyQualifiedNameWithTypeParms().startsWith("java.util.Collection"));

    final MetaType genericReturnType = method.getGenericReturnType();
    assertNotNull(genericReturnType);
    assertTrue("Got unexpected return type type " + genericReturnType.getClass(),
            genericReturnType instanceof MetaParameterizedType);
    final MetaParameterizedType mpReturnType = (MetaParameterizedType) genericReturnType;
    assertEquals(1, mpReturnType.getTypeParameters().length);

    // Sole type parameter should be <?>
    assertTrue(mpReturnType.getTypeParameters()[0] instanceof MetaWildcardType);
    final MetaWildcardType typeParam = (MetaWildcardType) mpReturnType.getTypeParameters()[0];
    assertArrayEquals(new MetaType[] {}, typeParam.getLowerBounds());
    assertArrayEquals(new MetaType[] { getMetaClass(Object.class) }, typeParam.getUpperBounds());
  }

  @Test
  public void testMethodReturnTypeWithUpperBoundedWildcardParameter() {
    final MetaClass c = getMetaClass(ClassWithGenericMethods.class);
    final MetaMethod method = c.getMethod("methodReturningUpperBoundedWildcardCollection", new Class[] {});

    // TODO (ERRAI-459) decide whether it's correct to have the type param present or not
    // then adjust this assertion to strict equality rather than startsWith()
    assertTrue(method.getReturnType().getFullyQualifiedNameWithTypeParms().startsWith("java.util.Collection"));

    final MetaType genericReturnType = method.getGenericReturnType();
    assertNotNull(genericReturnType);
    assertTrue("Got unexpected return type type " + genericReturnType.getClass(),
            genericReturnType instanceof MetaParameterizedType);
    final MetaParameterizedType mpReturnType = (MetaParameterizedType) genericReturnType;
    assertEquals(1, mpReturnType.getTypeParameters().length);

    // Sole type parameter should be <? extends String>
    assertTrue(mpReturnType.getTypeParameters()[0] instanceof MetaWildcardType);
    final MetaWildcardType typeParam = (MetaWildcardType) mpReturnType.getTypeParameters()[0];
    assertArrayEquals(new MetaType[] {}, typeParam.getLowerBounds());
    assertArrayEquals(new MetaType[] { getMetaClass(String.class) }, typeParam.getUpperBounds());
  }

  @Test
  public void testMethodReturnTypeWithLowerBoundedWildcardParameter() {
    final MetaClass c = getMetaClass(ClassWithGenericMethods.class);
    final MetaMethod method = c.getMethod("methodReturningLowerBoundedWildcardCollection", new Class[] {});

    // TODO (ERRAI-459) decide whether it's correct to have the type param present or not
    // then adjust this assertion to strict equality rather than startsWith()
    assertTrue(method.getReturnType().getFullyQualifiedNameWithTypeParms().startsWith("java.util.Collection"));

    final MetaType genericReturnType = method.getGenericReturnType();
    assertNotNull(genericReturnType);
    assertTrue("Got unexpected return type type " + genericReturnType.getClass(),
            genericReturnType instanceof MetaParameterizedType);
    final MetaParameterizedType mpReturnType = (MetaParameterizedType) genericReturnType;
    assertEquals(1, mpReturnType.getTypeParameters().length);

    // Sole type parameter should be <? extends String>
    assertTrue(mpReturnType.getTypeParameters()[0] instanceof MetaWildcardType);
    final MetaWildcardType typeParam = (MetaWildcardType) mpReturnType.getTypeParameters()[0];
    assertArrayEquals(new MetaType[] { getMetaClass(String.class) }, typeParam.getLowerBounds());
    assertArrayEquals(new MetaType[] { getMetaClass(Object.class)}, typeParam.getUpperBounds());
  }

  @Test
  public void testGetMethods() {
    final MetaClass c = getMetaClass(Child.class);
    final MetaMethod[] methods = c.getMethods();

    assertNotNull(methods);

    final List<String> methodSignatures = new ArrayList<>();
    for(final MetaMethod m : methods) {
      methodSignatures.add(GenUtil.getMethodString(m));
    }

    final List<String> expectedMethods = new ArrayList<>();
    expectedMethods.add("protectedMethod([])");
    expectedMethods.add("interfaceMethodOverriddenMultipleTimes([])");
    expectedMethods.add("packagePrivateMethod([])");
    expectedMethods.add("finalize([])");
    expectedMethods.add("equals([java.lang.Object])");
    expectedMethods.add("toString([])");
    expectedMethods.add("notify([])");
    expectedMethods.add("wait([])");
    expectedMethods.add("clone([])");
    expectedMethods.add("notifyAll([])");
    expectedMethods.add("getClass([])");
    expectedMethods.add("wait([long])");
    expectedMethods.add("hashCode([])");
    expectedMethods.add("wait([long, int])");

    Collections.sort(expectedMethods);
    Collections.sort(methodSignatures);


    assertEquals(expectedMethods.toString(), methodSignatures.toString());
  }

  @Test
  public void testGetFields() {
    final List<String> expectedFields = Lists.newLinkedList();
    expectedFields.add(Child.class.getCanonicalName() + "." + "childPublic");
    expectedFields.add(Parent.class.getCanonicalName() + "." + "parentPublic");

    final ArrayList<String> actualFields = new ArrayList<>();
    for (final MetaField field : getMetaClass(Child.class).getFields()) {
      actualFields.add(field.getDeclaringClass().getCanonicalName() + "." + field.getName());
    }

    Collections.sort(expectedFields);
    Collections.sort(actualFields);

    assertEquals(expectedFields.toString(), actualFields.toString());
  }

  @Test
  public void testGetDeclaredFields() {
    final List<String> expectedFields = Lists.newLinkedList();
    expectedFields.add(Child.class.getCanonicalName() + "." + "childPrivate");
    expectedFields.add(Child.class.getCanonicalName() + "." + "childPackage");
    expectedFields.add(Child.class.getCanonicalName() + "." + "childProtected");
    expectedFields.add(Child.class.getCanonicalName() + "." + "childPublic");

    final ArrayList<String> actualFields = new ArrayList<>();
    for (final MetaField field : getMetaClass(Child.class).getDeclaredFields()) {
      actualFields.add(field.getDeclaringClass().getCanonicalName() + "." + field.getName());
    }

    Collections.sort(expectedFields);
    Collections.sort(actualFields);

    assertEquals(expectedFields.toString(), actualFields.toString());
  }

  @Test
  public void testGetConstructors() throws Exception {
    final MetaClass mc = getMetaClass(HasManyConstructors.class);
    final MetaConstructor[] ctors = mc.getConstructors();
    assertEquals("Expected only a single visible constructor to be returned.", 1, ctors.length);
  }

  @Test
  public void testGetConstructorOnlyFindsPublicConstructor() throws Exception {
    final MetaClass mc = getMetaClass(HasManyConstructors.class);
    assertNotNull(mc.getConstructor(new Class[0]));
    assertNull(mc.getConstructor(new Class[] {int.class}));
    assertNull(mc.getConstructor(new Class[] {String.class}));
    assertNull(mc.getConstructor(new Class[] {double.class}));
  }

  @Test
  public void testGetDeclaredConstructors() throws Exception {
    final MetaClass mc = getMetaClass(HasManyConstructors.class);
    final MetaConstructor[] ctors = mc.getDeclaredConstructors();
    assertEquals("Not all constructors were returned.", 4, ctors.length);
  }

  @Test
  public void testGetDeclaredConstructorFindsAllConstructors() throws Exception {
    final MetaClass mc = getMetaClass(HasManyConstructors.class);
    assertNotNull(mc.getDeclaredConstructor(new Class[0]));
    assertNotNull(mc.getDeclaredConstructor(new Class[] {int.class}));
    assertNotNull(mc.getDeclaredConstructor(new Class[] {String.class}));
    assertNotNull(mc.getDeclaredConstructor(new Class[] {double.class}));
  }

  @Test
  public void testHashCodeErrorWithGenericHavingArrayUpperBound() throws Exception {
    final MetaClass mc = getMetaClass(ClassWithArrayGenerics.class).getField("field").getType();
    // Precondition
    assertNotNull("Failed to find field with type under test.", mc);
    assertNotNull("Type should be parameterized", mc.getParameterizedType());
    final MetaClass superClassWithProblematicBound = mc.getSuperClass();
    try {
      superClassWithProblematicBound.hashCode();
    } catch (final Throwable t) {
      throw new AssertionError("An error occurred.", t);
    }
  }

}
