/*
 * Copyright (C) 2017 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.auto.value.processor;

import static com.google.common.truth.Truth.assertThat;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import static java.util.stream.Collectors.joining;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.CompilationRule;
import com.google.testing.compile.JavaFileObjects;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for {@link TypeEncoder}.
 *
 * @author emcmanus@google.com (Éamonn McManus)
 */
@RunWith(JUnit4.class)
public class TypeEncoderTest {
  @Rule public final CompilationRule compilationRule = new CompilationRule();

  private Types typeUtils;
  private Elements elementUtils;

  @Before
  public void setUp() {
    typeUtils = compilationRule.getTypes();
    elementUtils = compilationRule.getElements();
  }

  /**
   * Assert that the fake program returned by fakeProgramForTypes has the given list of imports and
   * the given list of spellings. Here, "spellings" means the way each type is referenced in the
   * decoded program, for example {@code Timer} if {@code java.util.Timer} can be imported, or
   * {@code java.util.Timer} if not.
   *
   * <p>We construct a fake program that references each of the given types in turn.
   * TypeEncoder.decode doesn't have any real notion of Java syntax, so our program just consists of
   * START and END markers around the {@code `import`} tag, followed by each type in braces, as
   * encoded by TypeEncoder.encode. Once decoded, the program should consist of the appropriate
   * imports (inside START...END) and each type in braces, spelled appropriately.
   *
   * @param fakePackage the package that TypeEncoder should consider the fake program to be in.
   *     Classes in the same package don't usually need to be imported.
   */
  private void assertTypeImportsAndSpellings(
      Set<TypeMirror> types, String fakePackage, List<String> imports, List<String> spellings) {
    String fakeProgram = "START\n`import`\nEND\n"
        + types.stream().map(TypeEncoder::encode).collect(joining("}\n{", "{", "}"));
    String decoded = TypeEncoder.decode(
        fakeProgram, elementUtils, typeUtils, fakePackage, baseWithoutContainedTypes());
    String expected = "START\n"
        + imports.stream().map(s -> "import " + s + ";\n").collect(joining())
        + "\nEND\n"
        + spellings.stream().collect(joining("}\n{", "{", "}"));
    assertThat(decoded).isEqualTo(expected);
  }

  private static class MultipleBounds<K extends List<V> & Comparable<K>, V> {}

  @Test
  public void testImportsForNoTypes() {
    assertTypeImportsAndSpellings(
        typeMirrorSet(),
        "foo.bar",
        ImmutableList.of(),
        ImmutableList.of());
  }

  @Test
  public void testImportsForImplicitlyImportedTypes() {
    Set<TypeMirror> types = typeMirrorSet(
        typeMirrorOf(java.lang.String.class),
        typeMirrorOf(javax.management.MBeanServer.class),  // Same package, so no import.
        typeUtils.getPrimitiveType(TypeKind.INT),
        typeUtils.getPrimitiveType(TypeKind.BOOLEAN)
    );
    assertTypeImportsAndSpellings(
        types,
        "javax.management",
        ImmutableList.of(),
        ImmutableList.of("String", "MBeanServer", "int", "boolean"));
  }

  @Test
  public void testImportsForPlainTypes() {
    Set<TypeMirror> types = typeMirrorSet(
        typeUtils.getPrimitiveType(TypeKind.INT),
        typeMirrorOf(java.lang.String.class),
        typeMirrorOf(java.net.Proxy.class),
        typeMirrorOf(java.net.Proxy.Type.class),
        typeMirrorOf(java.util.regex.Pattern.class),
        typeMirrorOf(javax.management.MBeanServer.class));
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(
            "java.net.Proxy", "java.util.regex.Pattern", "javax.management.MBeanServer"),
        ImmutableList.of(
            "int", "String", "Proxy", "Proxy.Type", "Pattern", "MBeanServer"));
  }

  @Test
  public void testImportsForComplicatedTypes() {
    TypeElement list = typeElementOf(java.util.List.class);
    TypeElement map = typeElementOf(java.util.Map.class);
    Set<TypeMirror> types = typeMirrorSet(
        typeUtils.getPrimitiveType(TypeKind.INT),
        typeMirrorOf(java.util.regex.Pattern.class),
        typeUtils.getDeclaredType(list,  // List<Timer>
            typeMirrorOf(java.util.Timer.class)),
        typeUtils.getDeclaredType(map,   // Map<? extends Timer, ? super BigInteger>
            typeUtils.getWildcardType(typeMirrorOf(java.util.Timer.class), null),
            typeUtils.getWildcardType(null, typeMirrorOf(java.math.BigInteger.class))));
    // Timer is referenced twice but should obviously only be imported once.
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(
            "java.math.BigInteger",
            "java.util.List",
            "java.util.Map",
            "java.util.Timer",
            "java.util.regex.Pattern"),
        ImmutableList.of(
            "int",
            "Pattern",
            "List<Timer>",
            "Map<? extends Timer, ? super BigInteger>"));
  }

  @Test
  public void testImportsForArrayTypes() {
    TypeElement list = typeElementOf(java.util.List.class);
    TypeElement set = typeElementOf(java.util.Set.class);
    Set<TypeMirror> types = typeMirrorSet(
        typeUtils.getArrayType(typeUtils.getPrimitiveType(TypeKind.INT)),
        typeUtils.getArrayType(typeMirrorOf(java.util.regex.Pattern.class)),
        typeUtils.getArrayType(          // Set<Matcher[]>[]
            typeUtils.getDeclaredType(set,
                typeUtils.getArrayType(typeMirrorOf(java.util.regex.Matcher.class)))),
        typeUtils.getDeclaredType(list,  // List<Timer[]>
            typeUtils.getArrayType(typeMirrorOf(java.util.Timer.class))));
    // Timer is referenced twice but should obviously only be imported once.
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(
            "java.util.List",
            "java.util.Set",
            "java.util.Timer",
            "java.util.regex.Matcher",
            "java.util.regex.Pattern"),
        ImmutableList.of(
            "int[]",
            "Pattern[]",
            "Set<Matcher[]>[]",
            "List<Timer[]>"));
  }

  @Test
  public void testImportNestedType() {
    Set<TypeMirror> types = typeMirrorSet(typeMirrorOf(java.net.Proxy.Type.class));
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of("java.net.Proxy"),
        ImmutableList.of("Proxy.Type"));
  }

  @Test
  public void testImportsForAmbiguousNames() {
    TypeMirror wildcard = typeUtils.getWildcardType(null, null);
    Set<TypeMirror> types = typeMirrorSet(
        typeUtils.getPrimitiveType(TypeKind.INT),
        typeMirrorOf(java.awt.List.class),
        typeMirrorOf(java.lang.String.class),
        typeUtils.getDeclaredType(  // List<?>
            typeElementOf(java.util.List.class), wildcard),
        typeUtils.getDeclaredType(  // Map<?, ?>
            typeElementOf(java.util.Map.class), wildcard, wildcard));
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(
            "java.util.Map"
        ),
        ImmutableList.of(
            "int",
            "java.awt.List",
            "String",
            "java.util.List<?>",
            "Map<?, ?>"));
  }

  @Test
  public void testSimplifyJavaLangString() {
    Set<TypeMirror> types = typeMirrorSet(
        typeMirrorOf(java.lang.String.class));
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(),
        ImmutableList.of("String"));
  }

  @Test
  public void testSimplifyJavaLangThreadState() {
    Set<TypeMirror> types = typeMirrorSet(
        typeMirrorOf(java.lang.Thread.State.class));
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(),
        ImmutableList.of("Thread.State"));
  }

  @Test
  public void testSimplifyJavaLangNamesake() {
    TypeMirror javaLangType = typeMirrorOf(java.lang.RuntimePermission.class);
    TypeMirror notJavaLangType = typeMirrorOf(
        com.google.auto.value.processor.testclasses.RuntimePermission.class);
    Set<TypeMirror> types = typeMirrorSet(
        javaLangType,
        notJavaLangType);
    assertTypeImportsAndSpellings(
        types,
        "foo.bar",
        ImmutableList.of(),
        ImmutableList.of(
            javaLangType.toString(),
            notJavaLangType.toString()));
  }

  @Test
  public void testSimplifyComplicatedTypes() {
    // This test constructs a set of types and feeds them to TypeEncoder. Then it verifies that
    // the resultant rewrites of those types are what we would expect.
    TypeElement list = typeElementOf(java.util.List.class);
    TypeElement map = typeElementOf(java.util.Map.class);
    TypeMirror string = typeMirrorOf(java.lang.String.class);
    TypeMirror integer = typeMirrorOf(java.lang.Integer.class);
    TypeMirror pattern = typeMirrorOf(java.util.regex.Pattern.class);
    TypeMirror timer = typeMirrorOf(java.util.Timer.class);
    TypeMirror bigInteger = typeMirrorOf(java.math.BigInteger.class);
    ImmutableMap<TypeMirror, String> typeMap = ImmutableMap.<TypeMirror, String>builder()
        .put(typeUtils.getPrimitiveType(TypeKind.INT), "int")
        .put(typeUtils.getArrayType(typeUtils.getPrimitiveType(TypeKind.BYTE)), "byte[]")
        .put(pattern, "Pattern")
        .put(typeUtils.getArrayType(pattern), "Pattern[]")
        .put(typeUtils.getArrayType(typeUtils.getArrayType(pattern)), "Pattern[][]")
        .put(typeUtils.getDeclaredType(list, typeUtils.getWildcardType(null, null)), "List<?>")
        .put(typeUtils.getDeclaredType(list, timer), "List<Timer>")
        .put(typeUtils.getDeclaredType(map, string, integer), "Map<String, Integer>")
        .put(typeUtils.getDeclaredType(map,
              typeUtils.getWildcardType(timer, null), typeUtils.getWildcardType(null, bigInteger)),
            "Map<? extends Timer, ? super BigInteger>")
        .build();
    assertTypeImportsAndSpellings(
        typeMap.keySet(),
        "foo.bar",
        ImmutableList.of(
            "java.math.BigInteger",
            "java.util.List",
            "java.util.Map",
            "java.util.Timer",
            "java.util.regex.Pattern"),
        ImmutableList.copyOf(typeMap.values()));
  }

  @Test
  public void testSimplifyMultipleBounds() {
    TypeElement multipleBoundsElement = typeElementOf(MultipleBounds.class);
    TypeMirror multipleBoundsMirror = multipleBoundsElement.asType();
    String text = "`import`\n";
    text += "{" + TypeEncoder.encode(multipleBoundsMirror) + "}";
    text += "{" + TypeEncoder.formalTypeParametersString(multipleBoundsElement) + "}";
    String myPackage = getClass().getPackage().getName();
    String decoded =
        TypeEncoder.decode(text, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
    String expected = "import java.util.List;\n\n"
        + "{TypeEncoderTest.MultipleBounds<K, V>}"
        + "{<K extends List<V> & Comparable<K>, V>}";
    assertThat(decoded).isEqualTo(expected);
  }

  private static Set<TypeMirror> typeMirrorSet(TypeMirror... typeMirrors) {
    Set<TypeMirror> set = new TypeMirrorSet();
    for (TypeMirror typeMirror : typeMirrors) {
      assertThat(set.add(typeMirror)).isTrue();
    }
    return set;
  }

  private TypeElement typeElementOf(Class<?> c) {
    return elementUtils.getTypeElement(c.getCanonicalName());
  }

  private TypeMirror typeMirrorOf(Class<?> c) {
    return typeElementOf(c).asType();
  }

  /**
   * Returns a "base type" for TypeSimplifier that does not contain any nested types. The point
   * being that every {@code TypeSimplifier} has a base type that the class being generated is
   * going to extend, and if that class has nested types they will be in scope, and therefore a
   * possible source of ambiguity.
   */
  private TypeMirror baseWithoutContainedTypes() {
    return typeMirrorOf(Object.class);
  }

  // This test checks that we correctly throw MissingTypeException if there is an ErrorType anywhere
  // inside a type we are asked to simplify. There's no way to get an ErrorType from typeUtils or
  // elementUtils, so we need to fire up the compiler with an erroneous source file and use an
  // annotation processor to capture the resulting ErrorType. Then we can run tests within that
  // annotation processor, and propagate any failures out of this test.
  @Test
  public void testErrorTypes() {
    JavaFileObject source = JavaFileObjects.forSourceString(
        "ExtendsUndefinedType", "class ExtendsUndefinedType extends UndefinedParent {}");
    Compilation compilation = javac()
        .withProcessors(new ErrorTestProcessor())
        .compile(source);
    assertThat(compilation).failed();
    assertThat(compilation).hadErrorContaining("UndefinedParent");
    assertThat(compilation).hadErrorCount(1);
  }

  @SupportedAnnotationTypes("*")
  private static class ErrorTestProcessor extends AbstractProcessor {
    Types typeUtils;
    Elements elementUtils;

    @Override
    public boolean process(
        Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (roundEnv.processingOver()) {
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        test();
      }
      return false;
    }

    private void test() {
      TypeElement extendsUndefinedType = elementUtils.getTypeElement("ExtendsUndefinedType");
      ErrorType errorType = (ErrorType) extendsUndefinedType.getSuperclass();
      TypeElement list = elementUtils.getTypeElement("java.util.List");
      TypeMirror listOfError = typeUtils.getDeclaredType(list, errorType);
      TypeMirror queryExtendsError = typeUtils.getWildcardType(errorType, null);
      TypeMirror listOfQueryExtendsError = typeUtils.getDeclaredType(list, queryExtendsError);
      TypeMirror querySuperError = typeUtils.getWildcardType(null, errorType);
      TypeMirror listOfQuerySuperError = typeUtils.getDeclaredType(list, querySuperError);
      TypeMirror arrayOfError = typeUtils.getArrayType(errorType);
      testErrorType(errorType);
      testErrorType(listOfError);
      testErrorType(listOfQueryExtendsError);
      testErrorType(listOfQuerySuperError);
      testErrorType(arrayOfError);
    }

    @SuppressWarnings("MissingFail")  // error message gets converted into assertion failure
    private void testErrorType(TypeMirror typeWithError) {
      try {
        TypeEncoder.encode(typeWithError);
        processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR, "Expected exception for type: " + typeWithError);
      } catch (MissingTypeException expected) {
      }
    }

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