/*
 * Copyright 2017 Google LLC
 *
 * 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.auto.common.MoreTypes;
import com.google.auto.value.processor.MissingTypes.MissingTypeException;
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.DeclaredType;
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.typeParametersString(multipleBoundsElement.getTypeParameters()) + "}";
    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);
  }

  @SuppressWarnings("ClassCanBeStatic")
  static class Outer<T extends Number> {
    class InnerWithoutTypeParam {}
    class Middle<U> {
      class InnerWithTypeParam<V> {}
    }
  }

  @Test
  public void testOuterParameterizedInnerNot() {
    TypeElement outerElement = typeElementOf(Outer.class);
    DeclaredType doubleMirror = typeMirrorOf(Double.class);
    DeclaredType outerOfDoubleMirror = typeUtils.getDeclaredType(outerElement, doubleMirror);
    TypeElement innerWithoutTypeParamElement = typeElementOf(Outer.InnerWithoutTypeParam.class);
    DeclaredType parameterizedInnerWithoutTypeParam =
        typeUtils.getDeclaredType(outerOfDoubleMirror, innerWithoutTypeParamElement);
    String encoded = TypeEncoder.encode(parameterizedInnerWithoutTypeParam);
    String myPackage = getClass().getPackage().getName();
    String decoded =
        TypeEncoder.decode(
            encoded, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
    String expected = "TypeEncoderTest.Outer<Double>.InnerWithoutTypeParam";
    assertThat(decoded).isEqualTo(expected);
  }

  @Test
  public void testOuterParameterizedInnerAlso() {
    TypeElement outerElement = typeElementOf(Outer.class);
    DeclaredType doubleMirror = typeMirrorOf(Double.class);
    DeclaredType outerOfDoubleMirror = typeUtils.getDeclaredType(outerElement, doubleMirror);
    TypeElement middleElement = typeElementOf(Outer.Middle.class);
    DeclaredType stringMirror = typeMirrorOf(String.class);
    DeclaredType middleOfStringMirror =
        typeUtils.getDeclaredType(outerOfDoubleMirror, middleElement, stringMirror);
    TypeElement innerWithTypeParamElement = typeElementOf(Outer.Middle.InnerWithTypeParam.class);
    DeclaredType integerMirror = typeMirrorOf(Integer.class);
    DeclaredType parameterizedInnerWithTypeParam =
        typeUtils.getDeclaredType(middleOfStringMirror, innerWithTypeParamElement, integerMirror);
    String encoded = TypeEncoder.encode(parameterizedInnerWithTypeParam);
    String myPackage = getClass().getPackage().getName();
    String decoded =
        TypeEncoder.decode(
            encoded, elementUtils, typeUtils, myPackage, baseWithoutContainedTypes());
    String expected = "TypeEncoderTest.Outer<Double>.Middle<String>.InnerWithTypeParam<Integer>";
    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 DeclaredType typeMirrorOf(Class<?> c) {
    return MoreTypes.asDeclared(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();
    }
  }
}
