/*
 * Copyright 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.gwt.dev.javac.typemodel;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JWildcardType.BoundType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.dev.javac.TypeOracleTestingUtils;
import com.google.gwt.dev.javac.testing.impl.JavaResourceBase;
import com.google.gwt.dev.javac.typemodel.test.Base;
import com.google.gwt.dev.javac.typemodel.test.Derived;
import com.google.gwt.dev.javac.typemodel.test.GenericClass;
import com.google.gwt.dev.javac.typemodel.test.GenericClass.GenericInnerClass;
import com.google.gwt.dev.javac.typemodel.test.MyCustomList;
import com.google.gwt.dev.javac.typemodel.test.MyIntegerList;
import com.google.gwt.dev.javac.typemodel.test.MyList;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Test for {@link JParameterizedType}.
 */
public class JParameterizedTypeTest extends JDelegatingClassTypeTestBase {
  /**
   * Helper for verifying parameterized substitutions.
   */
  static class ParameterizedSubstitution implements Substitution {
    private final JParameterizedType parameterizedType;

    public ParameterizedSubstitution(JParameterizedType parameterizedType) {
      this.parameterizedType = parameterizedType;
    }

    @Override
    public JClassType getSubstitution(JClassType type) {
      return type.getSubstitutedType(parameterizedType);
    }
  }

  private static TreeLogger createTreeLogger() {
    final boolean logToConsole = false;
    return logToConsole ? new PrintWriterTreeLogger() : TreeLogger.NULL;
  }

  private final JClassType integerType;
  private final ModuleContext moduleContext = new ModuleContext(createTreeLogger(),
      "com.google.gwt.dev.javac.typemodel.TypeOracleTest");

  public JParameterizedTypeTest() throws UnableToCompleteException,
      NotFoundException {
    integerType = moduleContext.getOracle().getType(Integer.class.getName());
  }

  @Override
  public void testFindNestedType() {
    // TODO: complete this test method
  }

  /**
   * Checks that GenericClass<Integer> ends up with the correct substitutions.
   *
   * @throws NotFoundException
   */
  public void testGenericClass_Integer() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType genericType = getGenericTestType();
    JClassType type = oracle.getParameterizedType(genericType,
        new JClassType[]{integerType});
    JParameterizedType parameterizedType = type.isParameterized();

    validateTypeSubstitution(genericType, parameterizedType,
        new ParameterizedSubstitution(parameterizedType));
  }

  public void testGenericClass_LowerBoundWildcard() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType genericType = getGenericTestType();
    JWildcardType lowerBoundWildcard = oracle.getWildcardType(BoundType.SUPER,
        integerType);

    JClassType type = oracle.getParameterizedType(genericType,
        new JClassType[]{lowerBoundWildcard});
    JParameterizedType parameterizedType = type.isParameterized();

    validateTypeSubstitution(genericType, parameterizedType,
        new ParameterizedSubstitution(parameterizedType));
  }

  public void testGenericClass_UnboundWildcard() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType genericType = getGenericTestType();
    JWildcardType upperBoundWildcard = oracle.getWildcardType(
        BoundType.EXTENDS, oracle.getJavaLangObject());

    JClassType type = oracle.getParameterizedType(genericType,
        new JClassType[]{upperBoundWildcard});
    JParameterizedType parameterizedType = type.isParameterized();

    validateTypeSubstitution(genericType, parameterizedType,
        new ParameterizedSubstitution(parameterizedType));
  }

  public void testGenericClass_UpperBoundWildcard() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType genericType = getGenericTestType();
    JWildcardType upperBoundWildcard = oracle.getWildcardType(
        BoundType.EXTENDS, integerType);

    JClassType type = oracle.getParameterizedType(genericType,
        new JClassType[]{upperBoundWildcard});
    JParameterizedType parameterizedType = type.isParameterized();

    validateTypeSubstitution(genericType, parameterizedType,
        new ParameterizedSubstitution(parameterizedType));
  }

  /**
   * Test method for
   * {@link com.google.gwt.core.ext.typeinfo.JParameterizedType#getEnclosingType()}
   * .
   *
   * @throws NotFoundException
   */
  @Override
  public void testGetEnclosingType() throws NotFoundException {
    JParameterizedType testType = getTestType();

    // Check that GenericClass<Integer> is not nested
    assertNull(testType.getEnclosingType());

    /*
     * Check that GenericClass<Integer>.GenericInnerClass<Boolean> has //
     * GenericClass<Integer> as its // enclosing type
     */
    JParameterizedType parameterizedInnerClass = getInnerParameterizedType();

    assertEquals(testType, parameterizedInnerClass.getEnclosingType());
  }

  @Override
  public void testGetInheritableMethods() throws NotFoundException {
    // Tested via testOverridableMethods_Base, testOverridableMethods_Derived,
    // testOverridableMethods_Derived_Integer
  }

  @Override
  public void testGetNestedType() {
    // TODO: complete this test method
  }

  /**
   * Test method for
   * {@link com.google.gwt.core.ext.typeinfo.JParameterizedType#getNestedTypes()}
   * .
   *
   * @throws NotFoundException
   */
  @Override
  public void testGetNestedTypes() throws NotFoundException {
    JParameterizedType cut = getTestType();
    JParameterizedType innerCut = getInnerParameterizedType();

    // Check that inner parameterized types don't appear in the parent's nested
    // type set
    assertEquals(0, cut.getNestedTypes().length);

    try {
      cut.getNestedType(innerCut.getSimpleSourceName());
      fail("Type " + cut.getQualifiedSourceName()
          + " should report that it has no nested types");
    } catch (NotFoundException ex) {
      // Expected to get here
    }
  }

  @Override
  public void testGetOverridableMethods() throws NotFoundException {
    // Tested via testOverridableMethods_Base, testOverridableMethods_Derived,
    // testOverridableMethods_Derived_Integer
  }

  /**
   * Tests the subtypes of MyList<Integer>. These should be:
   * <ul>
   * <li><code>MyIntegerList</code></li>
   * <li><code>MyCustomList&lt;? extends Serializable, Integer&gt;</code></li>
   * </ul>
   *
   * @throws NotFoundException
   */
  @Override
  public void testGetSubtypes() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType genericType = oracle.getType(MyList.class.getName()).isGenericType();

    JParameterizedType parameterizedType = oracle.getParameterizedType(
        genericType, new JClassType[]{integerType});
    JClassType[] actualSubtypes = parameterizedType.getSubtypes();

    JGenericType myCustomListType = oracle.getType(MyCustomList.class.getName()).isGenericType();
    JParameterizedType parameterizedMyCustomList = oracle.getParameterizedType(
        myCustomListType, new JClassType[]{
            oracle.getWildcardType(BoundType.EXTENDS,
                oracle.getType(Serializable.class.getName())), integerType});
    JClassType[] expected = {
        oracle.getType(MyIntegerList.class.getName()),
        parameterizedMyCustomList};

    validateEquals(expected, actualSubtypes);
  }

  @Override
  public void testIsAssignableFrom() throws NotFoundException {
    assertType("List").isAssignableFrom("List");
    assertType("List").isAssignableFrom("List<?>");
    assertType("List").isAssignableFrom("List<? extends Number>");
    assertType("List").isAssignableFrom("List<String>");
    assertType("List").isAssignableFrom("ExtendsRawList");

    assertType("List<?>").isAssignableFrom("List");
    assertType("List<?>").isAssignableFrom("List<?>");
    assertType("List<?>").isAssignableFrom("List<? extends Number>");
    assertType("List<?>").isAssignableFrom("List<String>");
    assertType("List<?>").isAssignableFrom("ExtendsRawList");

    assertType("List<? extends Number>").isAssignableFrom("List");
    assertType("List<? extends Number>").isAssignableFrom("List<Integer>");
    assertType("List<? extends Number>").isAssignableFrom("List<? extends Integer>");
    assertType("List<? extends Number>").isNOTAssignableFrom("List<?>");
    assertType("List<? extends Number>").isNOTAssignableFrom("List<Object>");

    assertType("List<? extends Integer>").isAssignableFrom("List<? extends Integer>");
    assertType("List<? extends Integer>").isNOTAssignableFrom("List<? extends Number>");

    assertType("List<? super Number>").isAssignableFrom("List");
    assertType("List<? super Number>").isAssignableFrom("List<Object>");
    assertType("List<? super Number>").isAssignableFrom("List<Number>");
    assertType("List<? super Number>").isAssignableFrom("List<? super Number>");
    assertType("List<? super Number>").isNOTAssignableFrom("List<Integer>");
    assertType("List<? super Number>").isNOTAssignableFrom("List<String>");
    assertType("List<? super Number>").isNOTAssignableFrom("List<?>");
    assertType("List<? super Number>").isNOTAssignableFrom("List<? super Integer>");

    assertType("List<? super Integer>").isAssignableFrom("List<? super Object>");
    assertType("List<? super Integer>").isAssignableFrom("List<? super Number>");

    assertType("List<Object>").isAssignableFrom("List");
    assertType("List<Object>").isAssignableFrom("List<Object>");
    assertType("List<String>").isAssignableFrom("ExtendsRawList");
    assertType("List<Object>").isNOTAssignableFrom("List<String>");
    assertType("List<String>").isNOTAssignableFrom("List<Object>");

    assertType("List<List>").isAssignableFrom("List");
    assertType("List<List>").isAssignableFrom("List<List>");
    assertType("List<List>").isNOTAssignableFrom("List<List<?>>");
    assertType("List<List>").isNOTAssignableFrom("List<List<String>>");
    assertType("List<List>").isNOTAssignableFrom("List<? extends List<String>>");

    assertType("List<List<?>>").isAssignableFrom("List");
    assertType("List<List<?>>").isAssignableFrom("List<List<?>>");
    assertType("List<List<?>>").isAssignableFrom("List<List<? extends Object>>");
    assertType("List<List<?>>").isNOTAssignableFrom("List<List>");
    assertType("List<List<?>>").isNOTAssignableFrom("List<List<String>>");

    assertType("List<List<String>>").isAssignableFrom("List");
    assertType("List<List<String>>").isAssignableFrom("List<List<String>>");
    assertType("List<List<String>>").isNOTAssignableFrom("List<List>");
    assertType("List<List<String>>").isNOTAssignableFrom("List<List<?>>");
    assertType("List<List<String>>").isNOTAssignableFrom("List<List<Object>>");

    assertType("List<Collection<String>>").isNOTAssignableFrom("List<List<String>>");

    assertType("List<? extends Collection<String>>").isAssignableFrom("List<List<String>>");
    assertType("List<List<? extends Number>>").isNOTAssignableFrom("List<List<Integer>>");

    assertType("Map<?, ?>").isAssignableFrom("Map");
    assertType("Map<?, ?>").isAssignableFrom("Map<String, String>");
    assertType("Map<?, ?>").isAssignableFrom("Map<String, Integer>");

    assertType("Map<?, String>").isAssignableFrom("Map");
    assertType("Map<?, String>").isAssignableFrom("Map<String, String>");
    assertType("Map<?, String>").isNOTAssignableFrom("Map<String, Integer>");

    assertType("Map<String, String>").isAssignableFrom("Map");
    assertType("Map<String, String>").isAssignableFrom("Map<String, String>");
    assertType("Map<String, String>").isNOTAssignableFrom("Map<String, Integer>");
  }

  @Override
  public void testIsAssignableTo() throws NotFoundException {
    // This is covered as part of testIsAssignableFrom
  }

  public void testOverridableMethods_Base() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JClassType type = oracle.getType(Base.class.getName());
    JGenericType genericType = type.isGenericType();
    assertNotNull(genericType);

    List<JMethod> expected = new ArrayList<JMethod>(
        Arrays.asList(type.getOverloads("m")));
    List<JMethod> actual = new ArrayList<JMethod>(
        Arrays.asList(type.getInheritableMethods()));
    validateInheritableOrOverridableMethods(expected, actual, true);

    actual = new ArrayList<JMethod>(Arrays.asList(type.getOverridableMethods()));
    validateInheritableOrOverridableMethods(expected, actual, true);
  }

  public void testOverridableMethods_Derived() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JClassType type = oracle.getType(Derived.class.getName());
    JGenericType genericType = type.isGenericType();
    assertNotNull(genericType);

    JClassType supertype = type.getSuperclass();
    JParameterizedType paramType = supertype.isParameterized();
    // JGenericType genericSuperType = paramType.getBaseType().isGenericType();
    assertNotNull(paramType);

    List<JMethod> expected = new ArrayList<JMethod>();
    expected.addAll(Arrays.asList(genericType.getOverloads("m")));
    expected.add(paramType.getMethod("m",
        new JType[]{paramType.getTypeArgs()[0]}));

    List<JMethod> actual = new ArrayList<JMethod>(
        Arrays.asList(type.getInheritableMethods()));
    validateInheritableOrOverridableMethods(expected, actual, true);

    actual = new ArrayList<JMethod>(Arrays.asList(type.getOverridableMethods()));
    validateInheritableOrOverridableMethods(expected, actual, true);
  }

  public void testOverridableMethods_Derived_Integer() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JClassType type = oracle.getType(Derived.class.getName());
    JGenericType genericType = type.isGenericType();
    assertNotNull(genericType);

    JParameterizedType paramType = oracle.getParameterizedType(genericType,
        new JClassType[]{integerType});

    List<JMethod> expected = new ArrayList<JMethod>();
    expected.addAll(Arrays.asList(paramType.getOverloads("m")));

    List<JMethod> actual = new ArrayList<JMethod>(
        Arrays.asList(paramType.getInheritableMethods()));
    validateInheritableOrOverridableMethods(expected, actual, true);

    actual = new ArrayList<JMethod>(
        Arrays.asList(paramType.getOverridableMethods()));

    validateInheritableOrOverridableMethods(expected, actual, true);
  }

  /**
   * Returns the <code>TypeOracle</code> type for {@link GenericClass}.
   *
   * @return <code>TypeOracle</code> type for {@link GenericClass}
   * @throws NotFoundException
   */
  protected JGenericType getGenericTestType() throws NotFoundException {
    TypeOracle oracle = moduleContext.getOracle();
    JClassType type = oracle.getType(GenericClass.class.getName());
    assertNotNull(type.isGenericType());
    return type.isGenericType();
  }

  @Override
  protected Substitution getSubstitution() throws NotFoundException {
    return new ParameterizedSubstitution(getTestType());
  }

  @Override
  protected JParameterizedType getTestType() throws NotFoundException {
    JGenericType type = getGenericTestType();

    return moduleContext.getOracle().getParameterizedType(type, null,
        new JClassType[]{integerType});
  }

  /**
   * Returns the type for GenericClass<Integer>.GenericInnerClass<Boolean>.
   *
   * @throws NotFoundException
   * @return type for GenericClass<Integer>.GenericInnerClass<Boolean>
   */
  private JParameterizedType getInnerParameterizedType()
      throws NotFoundException {
    JParameterizedType cut = getTestType();
    TypeOracle oracle = moduleContext.getOracle();
    JGenericType innerGenericClass = cut.getBaseType().getNestedType(
        GenericInnerClass.class.getSimpleName()).isGenericType();

    JClassType booleanType = oracle.getType(Boolean.class.getName());

    /*
     * Check that GenericClass<Integer>.GenericInnerClass<Boolean> has
     * GenericClass<Integer> as its enclosing type
     */
    //
    JParameterizedType parameterizedInnerClass = oracle.getParameterizedType(
        innerGenericClass, cut, new JClassType[]{booleanType});

    return parameterizedInnerClass;
  }

  private void validateInheritableOrOverridableMethods(List<JMethod> expected,
      List<JMethod> actual, boolean addObjectMethods) {
    Set<JMethod> expectedMethods = new HashSet<JMethod>();
    expectedMethods.addAll(expected);
    if (addObjectMethods) {
      TypeOracle oracle = moduleContext.getOracle();
      expectedMethods.addAll(
          Arrays.asList(oracle.getJavaLangObject().getOverridableMethods()));
    }

    for (JMethod method : actual) {
      assertEquals("Method " + method.getReadableDeclaration() + " from type "
          + method.getEnclosingType().getQualifiedSourceName()
          + " was not expected", true, expectedMethods.remove(method));
    }

    assertTrue(expectedMethods.isEmpty());
  }

  private static TypeAssignabilityAsserter assertType(final String type) {
    return new TypeAssignabilityAsserter(type);
  }

  // TODO(goktug): make this utilized in more tests
  private static class TypeAssignabilityAsserter {
    private String type;

    public TypeAssignabilityAsserter(String type) {
      this.type = type;
    }

    public void isAssignableFrom(String from) {
      assertTrue(isAssignableFromTo(from, type));
    }

    public void isNOTAssignableFrom(String from) {
      assertFalse(isAssignableFromTo(from, type));
    }

    private boolean isAssignableFromTo(final String fromType, final String toType) {

      // Compile the code snippet to extract the types
      final String helperClassName = "ParameterizedTestHelper";

      Set<Resource> resources = new HashSet<Resource>();
      resources.addAll(Arrays.asList(JavaResourceBase.getStandardResources()));
      resources.add(JavaResourceBase.createMockJavaResource(helperClassName,
          "import java.util.*;",
          "public class " + helperClassName + " {",
          fromType + " from() { return null; }",
          toType + " to() { return null; }",
          "}"));
      resources.add(JavaResourceBase.createMockJavaResource("ExtendsRawComparable",
          "public interface ExtendsRawComparable extends Comparable {}"));
      resources.add(JavaResourceBase.createMockJavaResource("NonRecursiveComparable",
          "public interface NonRecursiveComparable extends Comparable<Number> {}"));
      resources.add(JavaResourceBase.createMockJavaResource("ExtendsRawList", "import java.util.*;",
          "public interface ExtendsRawList extends List {}"));

      JClassType to;
      JClassType from;
      try {
        // Compile and get helper type
        JClassType helperType = (JClassType) TypeOracleTestingUtils.buildTypeOracle(
            createTreeLogger(), resources).getType(helperClassName);

        to = (JClassType) helperType.getMethod("to", TypeOracle.NO_JCLASSES).getReturnType();
        from = (JClassType) helperType.getMethod("from", TypeOracle.NO_JCLASSES).getReturnType();
      } catch (NotFoundException e) {
        throw new AssertionError("Possible compilation error. Enable logToConsole for more info");
      }

      // Check the assignability
      boolean assignableFrom = to.isAssignableFrom(from);
      boolean assignableTo = from.isAssignableTo(to);
      assertEquals(assignableFrom, assignableTo);
      return assignableFrom;
    }
  }
}
