/*
 * Copyright (C) 2012 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 retroweibo.processor;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Files;

import junit.framework.TestCase;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

/**
 * Tests for {@link TypeSimplifier}.
 *
 * @author [email protected] (√Čamonn McManus)
 */
public class TypeSimplifierTest extends TestCase {
  private static final ImmutableMap<String, String> CLASS_TO_SOURCE = ImmutableMap.of(
      "Test",
          "public class Test {}\n",
      "MultipleBounds",
          "import java.util.List;\n"
          + "public class MultipleBounds<K extends List<V> & Comparable<K>, V> {}\n",
      "Erasure",
          "import java.util.List;\n"
          + "import java.util.Map;\n"
          + "@SuppressWarnings(\"rawtypes\")"
          + "public class Erasure<T> {\n"
          + "  int intNo; boolean booleanNo; int[] intArrayNo; String stringNo;\n"
          + "  String[] stringArrayNo; List rawListNo; List<?> listOfQueryNo;\n"
          + "  List<? extends Object> listOfQueryExtendsObjectNo;\n"
          + "  Map<?, ?> mapQueryToQueryNo;\n"
          + "\n"
          + "  List<String> listOfStringYes; List<? extends String> listOfQueryExtendsStringYes;\n"
          + "  List<? super String> listOfQuerySuperStringYes; List<T> listOfTypeVarYes;\n"
          + "  List<? extends T> listOfQueryExtendsTypeVarYes;\n"
          + "  List<? super T> listOfQuerySuperTypeVarYes;\n"
          + "}\n",
      "Wildcards",
          "import java.util.Map;\n"
          + "public abstract class Wildcards {\n"
          + "  abstract <T extends V, U extends T, V> Map<? extends T, ? super U> one();\n"
          + "  abstract <T extends V, U extends T, V> Map<? extends T, ? super U> two();\n"
          + "}\n"
  );
  private static final ImmutableMap<String, String> ERROR_CLASS_TO_SOURCE = ImmutableMap.of(
      "ExtendsUndefinedType",
          "public class ExtendsUndefinedType extends MissingType {}\n"
  );

  // This test is a bit unusual. The reason is that TypeSimplifier relies on interfaces such as
  // Types, TypeMirror, and TypeElement whose implementations are provided by the annotation
  // processing environment. While we could make fake or mock implementations of those interfaces,
  // the resulting test would be very verbose and would not obviously be testing the right thing.
  // Instead, we run the compiler with a simple annotation-processing environment that allows us
  // to capture the real implementations of these interfaces. Since those implementations are not
  // necessarily valid when the compiler has exited, we run all our test cases from within our
  // annotation processor, converting test failures into compiler errors. Then testTypeSimplifier()
  // passes if there were no compiler errors, and otherwise fails with a message that is a
  // concatenation of all the individual failures.
  public void testTypeSimplifier() throws Exception {
    doTestTypeSimplifierWithSources(new MainTestProcessor(), CLASS_TO_SOURCE);
  }

  public void testTypeSimplifierErrorTypes() throws IOException {
    doTestTypeSimplifierWithSources(new ErrorTestProcessor(), ERROR_CLASS_TO_SOURCE);
  }

  private void doTestTypeSimplifierWithSources(
      AbstractTestProcessor testProcessor, ImmutableMap<String, String> classToSource)
      throws IOException {
    File tmpDir = Files.createTempDir();
    for (String className : classToSource.keySet()) {
      File java = new File(tmpDir, className + ".java");
      Files.write(classToSource.get(className), java, Charsets.UTF_8);
    }
    try {
      doTestTypeSimplifier(testProcessor, tmpDir, classToSource);
    } finally {
      for (String className : classToSource.keySet()) {
        File java = new File(tmpDir, className + ".java");
        assertTrue(java.delete());
        new File(tmpDir, className + ".class").delete();
      }
      assertTrue(tmpDir.delete());
    }
  }

  private void doTestTypeSimplifier(
      AbstractTestProcessor testProcessor, File tmpDir, ImmutableMap<String, String> classToSource)
      throws IOException {
    JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> diagnosticCollector =
        new DiagnosticCollector<JavaFileObject>();
    StandardJavaFileManager fileManager =
        javac.getStandardFileManager(diagnosticCollector, null, null);

    StringWriter compilerOut = new StringWriter();

    List<String> options = ImmutableList.of(
        "-sourcepath", tmpDir.getPath(),
        "-d", tmpDir.getPath(),
        "-Xlint");
    javac.getTask(compilerOut, fileManager, diagnosticCollector, options, null, null);
    // This doesn't compile anything but communicates the paths to the JavaFileManager.

    ImmutableList.Builder<JavaFileObject> javaFilesBuilder = ImmutableList.builder();
    for (String className : classToSource.keySet()) {
      JavaFileObject sourceFile = fileManager.getJavaFileForInput(
          StandardLocation.SOURCE_PATH, className, Kind.SOURCE);
      javaFilesBuilder.add(sourceFile);
    }

    // Compile the empty source file to trigger the annotation processor.
    // (Annotation processors are somewhat misnamed because they run even on classes with no
    // annotations.)
    JavaCompiler.CompilationTask javacTask = javac.getTask(
        compilerOut, fileManager, diagnosticCollector, options,
        classToSource.keySet(), javaFilesBuilder.build());
    javacTask.setProcessors(ImmutableList.of(testProcessor));
    javacTask.call();
    List<Diagnostic<? extends JavaFileObject>> diagnostics =
        new ArrayList<Diagnostic<? extends JavaFileObject>>(diagnosticCollector.getDiagnostics());

    // In the ErrorTestProcessor case, the code being compiled contains a deliberate reference to an
    // undefined type, so that we can capture an instance of ErrorType. (Synthesizing one ourselves
    // leads to ClassCastException inside javac.) So remove any errors for that from the output, and
    // only fail if there were other errors.
    for (Iterator<Diagnostic<? extends JavaFileObject>> it = diagnostics.iterator();
         it.hasNext(); ) {
      Diagnostic<? extends JavaFileObject> diagnostic = it.next();
      if (diagnostic.getSource() != null
          && diagnostic.getSource().getName().contains("ExtendsUndefinedType")) {
        it.remove();
      }
    }
    // In the ErrorTestProcessor case, compilerOut.toString() will include the error for
    // ExtendsUndefinedType, which can safely be ignored, as well as stack traces for any failing
    // assertion.
    assertEquals(compilerOut.toString() + diagnosticCollector.getDiagnostics(),
        ImmutableList.of(), diagnostics);
  }

  // A type which is deliberately ambiguous with Map.Entry. Used to perform an ambiguity test below.
  static final class Entry {}

  private abstract static class AbstractTestProcessor extends AbstractProcessor {
    private boolean testsRan;
    Types typeUtil;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
      return ImmutableSet.of("*");
    }

    @Override
    public final SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latest();
    }

    @Override
    public final boolean process(
        Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (!testsRan) {
        testsRan = true;
        typeUtil = processingEnv.getTypeUtils();
        runTests();
      }
      return false;
    }

    private void runTests() {
      for (Method method : getClass().getMethods()) {
        if (method.getName().startsWith("test")) {
          try {
            method.invoke(this);
          } catch (Exception e) {
            Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e;
            StringWriter stringWriter = new StringWriter();
            cause.printStackTrace(new PrintWriter(stringWriter));
            processingEnv.getMessager().printMessage(
                Diagnostic.Kind.ERROR, stringWriter.toString());
          }
        }
      }
    }

    TypeElement typeElementOf(String name) {
      return processingEnv.getElementUtils().getTypeElement(name);
    }

    TypeMirror typeMirrorOf(String name) {
      return typeElementOf(name).asType();
    }

    TypeMirror baseWithoutContainedTypes() {
      return typeMirrorOf("java.lang.Object");
    }

    TypeMirror baseDeclaresEntry() {
      return typeMirrorOf("java.util.Map");
    }
  }

  private static class MainTestProcessor extends AbstractTestProcessor {
    private Set<TypeMirror> typeMirrorSet(TypeMirror... typeMirrors) {
      Set<TypeMirror> set = new TypeMirrorSet();
      for (TypeMirror typeMirror : typeMirrors) {
        assertTrue(set.add(typeMirror));
      }
      return set;
    }

    private TypeMirror objectMirror() {
      return typeMirrorOf("java.lang.Object");
    }

    private TypeMirror cloneReturnTypeMirror() {
      TypeElement object = typeElementOf("java.lang.Object");
      ExecutableElement clone = null;
      for (Element element : object.getEnclosedElements()) {
        if (element.getSimpleName().contentEquals("clone")) {
          clone = (ExecutableElement) element;
          break;
        }
      }
      return clone.getReturnType();
    }

    /**
     * This test shows why we need to have TypeMirrorSet. The mirror of java.lang.Object obtained
     * from {@link Elements#getTypeElement Elements.getTypeElement("java.lang.Object")} does not
     * compare equal to the mirror of the return type of Object.clone(), even though that is also
     * java.lang.Object and {@link Types#isSameType} considers them the same.
     *
     * <p>There's no requirement that this test pass and if it starts failing or doesn't work in
     * another test environment then we can delete it. The specification of
     * {@link TypeMirror#equals} explicitly says that it cannot be used for type equality, so even
     * if this particular case stops being a problem (which means this test would fail), we would
     * need TypeMirrorSet for complete correctness.
     */
    public void testQuirkyTypeMirrors() {
      TypeMirror objectMirror = objectMirror();
      TypeMirror cloneReturnTypeMirror = cloneReturnTypeMirror();
      assertFalse(objectMirror.equals(cloneReturnTypeMirror));
      assertTrue(typeUtil.isSameType(objectMirror, cloneReturnTypeMirror));
    }

    public void testTypeMirrorSet() {
      TypeMirror objectMirror = objectMirror();
      TypeMirror otherObjectMirror = cloneReturnTypeMirror();
      Set<TypeMirror> set = new TypeMirrorSet();
      assertEquals(0, set.size());
      assertTrue(set.isEmpty());
      boolean added = set.add(objectMirror);
      assertTrue(added);
      assertEquals(1, set.size());

      Set<TypeMirror> otherSet = typeMirrorSet(otherObjectMirror);
      assertEquals(set, otherSet);
      assertEquals(otherSet, set);
      assertEquals(set.hashCode(), otherSet.hashCode());

      assertFalse(set.add(otherObjectMirror));
      assertTrue(set.contains(otherObjectMirror));

      assertFalse(set.contains(null));
      assertFalse(set.contains("foo"));
      assertFalse(set.remove(null));
      assertFalse(set.remove("foo"));

      TypeElement list = typeElementOf("java.util.List");
      TypeMirror listOfObjectMirror = typeUtil.getDeclaredType(list, objectMirror);
      TypeMirror listOfOtherObjectMirror = typeUtil.getDeclaredType(list, otherObjectMirror);
      assertFalse(listOfObjectMirror.equals(listOfOtherObjectMirror));
      assertTrue(typeUtil.isSameType(listOfObjectMirror, listOfOtherObjectMirror));
      added = set.add(listOfObjectMirror);
      assertTrue(added);
      assertEquals(2, set.size());
      assertFalse(set.add(listOfOtherObjectMirror));
      assertTrue(set.contains(listOfOtherObjectMirror));

      boolean removed = set.remove(listOfOtherObjectMirror);
      assertTrue(removed);
      assertFalse(set.contains(listOfObjectMirror));

      set.removeAll(otherSet);
      assertTrue(set.isEmpty());
    }

    public void testTypeMirrorSetWildcardCapture() {
      // TODO(user): this test should really be in MoreTypesTest.
      // This test checks the assumption made by MoreTypes that you can find the
      // upper bounds of a TypeVariable tv like this:
      //   TypeParameterElement tpe = (TypeParameterElement) tv.asElement();
      //   List<? extends TypeMirror> bounds = tpe.getBounds();
      // There was some doubt as to whether this would be true in exotic cases involving
      // wildcard capture, but apparently it is.
      // The methods one and two here have identical signatures:
      //   abstract <T extends V, U extends T, V> Map<? extends T, ? super U> name();
      // Their return types should be considered equal by TypeMirrorSet. The capture of
      // each return type is different from the original return type, but the two captures
      // should compare equal to each other. We also add various other types like ? super U
      // to the set to ensure that the implied calls to the equals and hashCode visitors
      // don't cause a ClassCastException for TypeParameterElement.
      TypeElement wildcardsElement = typeElementOf("Wildcards");
      List<? extends ExecutableElement> methods =
          ElementFilter.methodsIn(wildcardsElement.getEnclosedElements());
      assertEquals(2, methods.size());
      ExecutableElement one = methods.get(0);
      ExecutableElement two = methods.get(1);
      assertEquals("one", one.getSimpleName().toString());
      assertEquals("two", two.getSimpleName().toString());
      TypeMirrorSet typeMirrorSet = new TypeMirrorSet();
      assertTrue(typeMirrorSet.add(one.getReturnType()));
      assertFalse(typeMirrorSet.add(two.getReturnType()));
      DeclaredType captureOne = (DeclaredType) typeUtil.capture(one.getReturnType());
      assertTrue(typeMirrorSet.add(captureOne));
      DeclaredType captureTwo = (DeclaredType) typeUtil.capture(two.getReturnType());
      assertFalse(typeMirrorSet.add(captureTwo));
      // Reminder: captureOne is Map<?#123 extends T, ?#456 super U>
      TypeVariable extendsT = (TypeVariable) captureOne.getTypeArguments().get(0);
      assertTrue(typeMirrorSet.add(extendsT));
      assertTrue(typeMirrorSet.add(extendsT.getLowerBound()));  // NoType
      for (TypeMirror bound : ((TypeParameterElement) extendsT.asElement()).getBounds()) {
        assertTrue(typeMirrorSet.add(bound));
      }
      TypeVariable superU = (TypeVariable) captureOne.getTypeArguments().get(1);
      assertTrue(typeMirrorSet.add(superU));
      //      assertTrue(typeMirrorSet.add(superU.getLowerBound()));
    }

    public void testPackageNameOfString() {
      assertEquals("java.lang", TypeSimplifier.packageNameOf(typeElementOf("java.lang.String")));
    }

    public void testPackageNameOfMapEntry() {
      assertEquals("java.util", TypeSimplifier.packageNameOf(typeElementOf("java.util.Map.Entry")));
    }

    public void testPackageNameOfDefaultPackage() {
      String aClassName = CLASS_TO_SOURCE.keySet().iterator().next();
      assertEquals("", TypeSimplifier.packageNameOf(typeElementOf(aClassName)));
    }

    public void testImportsForNoTypes() {
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", typeMirrorSet(), baseWithoutContainedTypes());
      assertEquals(ImmutableSet.of(), typeSimplifier.typesToImport());
    }

    public void testImportsForImplicitlyImportedTypes() {
      Set<TypeMirror> types = typeMirrorSet(
          typeMirrorOf("java.lang.String"),
          typeMirrorOf("javax.management.MBeanServer"),  // Same package, so no import.
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeUtil.getPrimitiveType(TypeKind.BOOLEAN)
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "javax.management", types, baseWithoutContainedTypes());
      assertEquals(ImmutableSet.of(), typeSimplifier.typesToImport());
    }

    public void testImportsForPlainTypes() {
      Set<TypeMirror> types = typeMirrorSet(
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeMirrorOf("java.lang.String"),
          typeMirrorOf("java.util.Map"),
          typeMirrorOf("java.util.Map.Entry"),
          typeMirrorOf("java.util.regex.Pattern"),
          typeMirrorOf("javax.management.MBeanServer"));
      List<String> expectedImports = ImmutableList.of(
          "java.util.Map",
          "java.util.Map.Entry",
          "java.util.regex.Pattern",
          "javax.management.MBeanServer"
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(expectedImports, ImmutableList.copyOf(typeSimplifier.typesToImport()));
    }

    public void testImportsForComplicatedTypes() {
      TypeElement list = typeElementOf("java.util.List");
      TypeElement map = typeElementOf("java.util.Map");
      Set<TypeMirror> types = typeMirrorSet(
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeMirrorOf("java.util.regex.Pattern"),
          typeUtil.getDeclaredType(list,  // List<Timer>
              typeMirrorOf("java.util.Timer")),
          typeUtil.getDeclaredType(map,   // Map<? extends Timer, ? super BigInteger>
              typeUtil.getWildcardType(typeMirrorOf("java.util.Timer"), null),
              typeUtil.getWildcardType(null, typeMirrorOf("java.math.BigInteger"))));
      // Timer is referenced twice but should obviously only be imported once.
      List<String> expectedImports = ImmutableList.of(
          "java.math.BigInteger",
          "java.util.List",
          "java.util.Map",
          "java.util.Timer",
          "java.util.regex.Pattern"
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(expectedImports, ImmutableList.copyOf(typeSimplifier.typesToImport()));
    }

    public void testImportsForArrayTypes() {
      TypeElement list = typeElementOf("java.util.List");
      TypeElement set = typeElementOf("java.util.Set");
      Set<TypeMirror> types = typeMirrorSet(
          typeUtil.getArrayType(typeUtil.getPrimitiveType(TypeKind.INT)),
          typeUtil.getArrayType(typeMirrorOf("java.util.regex.Pattern")),
          typeUtil.getArrayType(          // Set<Matcher[]>[]
              typeUtil.getDeclaredType(set,
                  typeUtil.getArrayType(typeMirrorOf("java.util.regex.Matcher")))),
          typeUtil.getDeclaredType(list,  // List<Timer[]>
              typeUtil.getArrayType(typeMirrorOf("java.util.Timer"))));
      // Timer is referenced twice but should obviously only be imported once.
      List<String> expectedImports = ImmutableList.of(
          "java.util.List",
          "java.util.Set",
          "java.util.Timer",
          "java.util.regex.Matcher",
          "java.util.regex.Pattern"
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(expectedImports, ImmutableList.copyOf(typeSimplifier.typesToImport()));
    }

    public void testImportsForDefaultPackage() {
      Set<TypeMirror> types = typeMirrorSet();
      for (String className : CLASS_TO_SOURCE.keySet()) {
        assertTrue(types.add(typeMirrorOf(className)));
        // These are all in the default package so they don't need to be imported.
        // But MultipleBounds references java.util.List so that will be imported.
      }
      types.addAll(typeMirrorSet(
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeMirrorOf("java.lang.String"),
          typeMirrorOf("java.util.Map"),
          typeMirrorOf("java.util.Map.Entry"),
          typeMirrorOf("java.util.regex.Pattern"),
          typeMirrorOf("javax.management.MBeanServer")));
      List<String> expectedImports = ImmutableList.of(
          "java.util.List",
          "java.util.Map",
          "java.util.Map.Entry",
          "java.util.regex.Pattern",
          "javax.management.MBeanServer"
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "", types, baseWithoutContainedTypes());
      assertEquals(expectedImports, ImmutableList.copyOf(typeSimplifier.typesToImport()));
    }

    public void testImportsForAmbiguousNames() {
      Set<TypeMirror> types = typeMirrorSet(
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeMirrorOf("java.awt.List"),
          typeMirrorOf("java.lang.String"),
          typeMirrorOf("java.util.List"),
          typeMirrorOf("java.util.Map")
      );
      List<String> expectedImports = ImmutableList.of(
          "java.util.Map"
      );
      TypeSimplifier typeSimplifier
          = new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(expectedImports, ImmutableList.copyOf(typeSimplifier.typesToImport()));
    }

    public void testSimplifyJavaLangString() {
      TypeMirror string = typeMirrorOf("java.lang.String");
      Set<TypeMirror> types = typeMirrorSet(string);
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals("String", typeSimplifier.simplify(string));
    }

    public void testSimplifyJavaLangThreadState() {
      TypeMirror threadState = typeMirrorOf("java.lang.Thread.State");
      Set<TypeMirror> types = typeMirrorSet(threadState);
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals("Thread.State", typeSimplifier.simplify(threadState));
    }

    public void testSimplifyAmbiguousNames() {
      TypeMirror javaAwtList = typeMirrorOf("java.awt.List");
      TypeMirror javaUtilList = typeMirrorOf("java.util.List");
      Set<TypeMirror> types = typeMirrorSet(javaAwtList, javaUtilList);
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(javaAwtList.toString(), typeSimplifier.simplify(javaAwtList));
      assertEquals(javaUtilList.toString(), typeSimplifier.simplify(javaUtilList));
    }

    public void testSimplifyAmbiguityFromWithinClass() {
      TypeMirror otherEntry = typeMirrorOf(TypeSimplifierTest.class.getCanonicalName() + ".Entry");
      Set<TypeMirror> types = typeMirrorSet(otherEntry);
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseDeclaresEntry());
      assertEquals(otherEntry.toString(), typeSimplifier.simplify(otherEntry));
    }

    public void testSimplifyJavaLangNamesake() {
      TypeMirror javaLangDouble = typeMirrorOf("java.lang.Double");
      TypeMirror awtDouble = typeMirrorOf("java.awt.geom.Arc2D.Double");
      Set<TypeMirror> types = typeMirrorSet(javaLangDouble, awtDouble);
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      assertEquals(javaLangDouble.toString(), typeSimplifier.simplify(javaLangDouble));
      assertEquals(awtDouble.toString(), typeSimplifier.simplify(awtDouble));
    }

    public void testSimplifyComplicatedTypes() {
      TypeElement list = typeElementOf("java.util.List");
      TypeElement map = typeElementOf("java.util.Map");
      TypeMirror string = typeMirrorOf("java.lang.String");
      TypeMirror integer = typeMirrorOf("java.lang.Integer");
      TypeMirror pattern = typeMirrorOf("java.util.regex.Pattern");
      TypeMirror timer = typeMirrorOf("java.util.Timer");
      TypeMirror bigInteger = typeMirrorOf("java.math.BigInteger");
      Set<TypeMirror> types = typeMirrorSet(
          typeUtil.getPrimitiveType(TypeKind.INT),
          typeUtil.getArrayType(typeUtil.getPrimitiveType(TypeKind.BYTE)),
          pattern,
          typeUtil.getArrayType(pattern),
          typeUtil.getArrayType(typeUtil.getArrayType(pattern)),
          typeUtil.getDeclaredType(list, typeUtil.getWildcardType(null, null)),
          typeUtil.getDeclaredType(list, timer),
          typeUtil.getDeclaredType(map, string, integer),
          typeUtil.getDeclaredType(map,
              typeUtil.getWildcardType(timer, null), typeUtil.getWildcardType(null, bigInteger)));
      Set<String> expectedSimplifications = ImmutableSet.of(
          "int",
          "byte[]",
          "Pattern",
          "Pattern[]",
          "Pattern[][]",
          "List<?>",
          "List<Timer>",
          "Map<String, Integer>",
          "Map<? extends Timer, ? super BigInteger>"
      );
      TypeSimplifier typeSimplifier =
          new TypeSimplifier(typeUtil, "foo.bar", types, baseWithoutContainedTypes());
      Set<String> actualSimplifications = new HashSet<String>();
      for (TypeMirror type : types) {
        actualSimplifications.add(typeSimplifier.simplify(type));
      }
      assertEquals(expectedSimplifications, actualSimplifications);
    }

    public void testSimplifyMultipleBounds() {
      TypeElement multipleBoundsElement = typeElementOf("MultipleBounds");
      TypeMirror multipleBoundsMirror = multipleBoundsElement.asType();
      TypeSimplifier typeSimplifier = new TypeSimplifier(typeUtil, "",
          typeMirrorSet(multipleBoundsMirror), baseWithoutContainedTypes());
      assertEquals(ImmutableSet.of("java.util.List"), typeSimplifier.typesToImport());
      assertEquals("MultipleBounds<K, V>", typeSimplifier.simplify(multipleBoundsMirror));
      assertEquals("<K extends List<V> & Comparable<K>, V>",
          typeSimplifier.formalTypeParametersString(multipleBoundsElement));
    }

    // Test TypeSimplifier.isCastingUnchecked. We do this by examining the fields of the Erasure
    // class that is defined in CLASS_TO_SOURCE. A field whose name ends with Yes has a type where
    // isCastingUnchecked should return true, and one whose name ends with No has a type where
    // isCastingUnchecked should return false.
    public void testIsCastingUnchecked() {
      TypeElement erasureClass = typeElementOf("Erasure");
      List<VariableElement> fields = ElementFilter.fieldsIn(erasureClass.getEnclosedElements());
      for (VariableElement field : fields) {
        String fieldName = field.getSimpleName().toString();
        boolean expectUnchecked;
        if (fieldName.endsWith("Yes")) {
          expectUnchecked = true;
        } else if (fieldName.endsWith("No")) {
          expectUnchecked = false;
        } else {
          throw new AssertionError("Fields in Erasure class must end with Yes or No: " + fieldName);
        }
        TypeMirror fieldType = field.asType();
        boolean actualUnchecked = TypeSimplifier.isCastingUnchecked(fieldType);
        assertEquals("Unchecked-cast status for " + fieldType, expectUnchecked, actualUnchecked);
      }
    }
  }

  private static class ErrorTestProcessor extends AbstractTestProcessor {
    public void testErrorTypes() {
      TypeElement extendsUndefinedType =
          processingEnv.getElementUtils().getTypeElement("ExtendsUndefinedType");
      ErrorType errorType = (ErrorType) extendsUndefinedType.getSuperclass();
      TypeMirror javaLangObject = typeMirrorOf("java.lang.Object");
      TypeElement list = typeElementOf("java.util.List");
      TypeMirror listOfError = typeUtil.getDeclaredType(list, errorType);
      TypeMirror queryExtendsError = typeUtil.getWildcardType(errorType, null);
      TypeMirror listOfQueryExtendsError = typeUtil.getDeclaredType(list, queryExtendsError);
      TypeMirror querySuperError = typeUtil.getWildcardType(null, errorType);
      TypeMirror listOfQuerySuperError = typeUtil.getDeclaredType(list, querySuperError);
      TypeMirror arrayOfError = typeUtil.getArrayType(errorType);
      TypeMirror[] typesWithErrors = {
          errorType, listOfError, listOfQueryExtendsError, listOfQuerySuperError, arrayOfError
      };
      for (TypeMirror typeWithError : typesWithErrors) {
        try {
          new TypeSimplifier(typeUtil, "foo.bar", ImmutableSet.of(typeWithError), javaLangObject);
          fail("Expected exception for type: " + typeWithError);
        } catch (MissingTypeException expected) {
        }
      }
    }
  }
}