/*
 * Copyright (C) 2014 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.squareup.javapoet;

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 javax.lang.model.util.ElementFilter.fieldsIn;
import static org.junit.Assert.*;

import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
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.TypeVisitor;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.JavaFileObject;

import org.junit.Test;

public abstract class AbstractTypesTest {
  protected abstract Elements getElements();
  protected abstract Types getTypes();

  private TypeElement getElement(Class<?> clazz) {
    return getElements().getTypeElement(clazz.getCanonicalName());
  }

  private TypeMirror getMirror(Class<?> clazz) {
    return getElement(clazz).asType();
  }

  @Test public void getBasicTypeMirror() {
    assertThat(TypeName.get(getMirror(Object.class)))
        .isEqualTo(ClassName.get(Object.class));
    assertThat(TypeName.get(getMirror(Charset.class)))
        .isEqualTo(ClassName.get(Charset.class));
    assertThat(TypeName.get(getMirror(AbstractTypesTest.class)))
        .isEqualTo(ClassName.get(AbstractTypesTest.class));
  }

  @Test public void getParameterizedTypeMirror() {
    DeclaredType setType =
        getTypes().getDeclaredType(getElement(Set.class), getMirror(Object.class));
    assertThat(TypeName.get(setType))
        .isEqualTo(ParameterizedTypeName.get(ClassName.get(Set.class), ClassName.OBJECT));
  }

  @Test public void errorTypes() {
    JavaFileObject hasErrorTypes =
        JavaFileObjects.forSourceLines(
            "com.squareup.tacos.ErrorTypes",
            "package com.squareup.tacos;",
            "",
            "@SuppressWarnings(\"hook-into-compiler\")",
            "class ErrorTypes {",
            "  Tacos tacos;",
            "  Ingredients.Guacamole guacamole;",
            "}");
    Compilation compilation = javac().withProcessors(new AbstractProcessor() {
      @Override
      public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        TypeElement classFile =
            processingEnv.getElementUtils().getTypeElement("com.squareup.tacos.ErrorTypes");
        List<VariableElement> fields = fieldsIn(classFile.getEnclosedElements());
        ErrorType topLevel = (ErrorType) fields.get(0).asType();
        ErrorType member = (ErrorType) fields.get(1).asType();

        assertThat(TypeName.get(topLevel)).isEqualTo(ClassName.get("", "Tacos"));
        assertThat(TypeName.get(member)).isEqualTo(ClassName.get("Ingredients", "Guacamole"));
        return false;
      }

      @Override
      public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton("*");
      }
    }).compile(hasErrorTypes);

    assertThat(compilation).failed();
  }

  static class Parameterized<
      Simple,
      ExtendsClass extends Number,
      ExtendsInterface extends Runnable,
      ExtendsTypeVariable extends Simple,
      Intersection extends Number & Runnable,
      IntersectionOfInterfaces extends Runnable & Serializable> {}

  @Test public void getTypeVariableTypeMirror() {
    List<? extends TypeParameterElement> typeVariables =
        getElement(Parameterized.class).getTypeParameters();

    // Members of converted types use ClassName and not Class<?>.
    ClassName number = ClassName.get(Number.class);
    ClassName runnable = ClassName.get(Runnable.class);
    ClassName serializable = ClassName.get(Serializable.class);

    assertThat(TypeName.get(typeVariables.get(0).asType()))
        .isEqualTo(TypeVariableName.get("Simple"));
    assertThat(TypeName.get(typeVariables.get(1).asType()))
        .isEqualTo(TypeVariableName.get("ExtendsClass", number));
    assertThat(TypeName.get(typeVariables.get(2).asType()))
        .isEqualTo(TypeVariableName.get("ExtendsInterface", runnable));
    assertThat(TypeName.get(typeVariables.get(3).asType()))
        .isEqualTo(TypeVariableName.get("ExtendsTypeVariable", TypeVariableName.get("Simple")));
    assertThat(TypeName.get(typeVariables.get(4).asType()))
        .isEqualTo(TypeVariableName.get("Intersection", number, runnable));
    assertThat(TypeName.get(typeVariables.get(5).asType()))
        .isEqualTo(TypeVariableName.get("IntersectionOfInterfaces", runnable, serializable));
    assertThat(((TypeVariableName) TypeName.get(typeVariables.get(4).asType())).bounds)
        .containsExactly(number, runnable);
  }

  static class Recursive<T extends Map<List<T>, Set<T[]>>> {}

  @Test
  public void getTypeVariableTypeMirrorRecursive() {
    TypeMirror typeMirror = getElement(Recursive.class).asType();
    ParameterizedTypeName typeName = (ParameterizedTypeName) TypeName.get(typeMirror);
    String className = Recursive.class.getCanonicalName();
    assertThat(typeName.toString()).isEqualTo(className + "<T>");

    TypeVariableName typeVariableName = (TypeVariableName) typeName.typeArguments.get(0);

    try {
      typeVariableName.bounds.set(0, null);
      fail("Expected UnsupportedOperationException");
    } catch (UnsupportedOperationException expected) {
    }

    assertThat(typeVariableName.toString()).isEqualTo("T");
    assertThat(typeVariableName.bounds.toString())
        .isEqualTo("[java.util.Map<java.util.List<T>, java.util.Set<T[]>>]");
  }

  @Test public void getPrimitiveTypeMirror() {
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.BOOLEAN)))
        .isEqualTo(TypeName.BOOLEAN);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.BYTE)))
        .isEqualTo(TypeName.BYTE);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.SHORT)))
        .isEqualTo(TypeName.SHORT);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.INT)))
        .isEqualTo(TypeName.INT);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.LONG)))
        .isEqualTo(TypeName.LONG);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.CHAR)))
        .isEqualTo(TypeName.CHAR);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.FLOAT)))
        .isEqualTo(TypeName.FLOAT);
    assertThat(TypeName.get(getTypes().getPrimitiveType(TypeKind.DOUBLE)))
        .isEqualTo(TypeName.DOUBLE);
  }

  @Test public void getArrayTypeMirror() {
    assertThat(TypeName.get(getTypes().getArrayType(getMirror(Object.class))))
        .isEqualTo(ArrayTypeName.of(ClassName.OBJECT));
  }

  @Test public void getVoidTypeMirror() {
    assertThat(TypeName.get(getTypes().getNoType(TypeKind.VOID)))
        .isEqualTo(TypeName.VOID);
  }

  @Test public void getNullTypeMirror() {
    try {
      TypeName.get(getTypes().getNullType());
      fail();
    } catch (IllegalArgumentException expected) {
    }
  }

  @Test public void parameterizedType() throws Exception {
    ParameterizedTypeName type = ParameterizedTypeName.get(Map.class, String.class, Long.class);
    assertThat(type.toString()).isEqualTo("java.util.Map<java.lang.String, java.lang.Long>");
  }

  @Test public void arrayType() throws Exception {
    ArrayTypeName type = ArrayTypeName.of(String.class);
    assertThat(type.toString()).isEqualTo("java.lang.String[]");
  }

  @Test public void wildcardExtendsType() throws Exception {
    WildcardTypeName type = WildcardTypeName.subtypeOf(CharSequence.class);
    assertThat(type.toString()).isEqualTo("? extends java.lang.CharSequence");
  }

  @Test public void wildcardExtendsObject() throws Exception {
    WildcardTypeName type = WildcardTypeName.subtypeOf(Object.class);
    assertThat(type.toString()).isEqualTo("?");
  }

  @Test public void wildcardSuperType() throws Exception {
    WildcardTypeName type = WildcardTypeName.supertypeOf(String.class);
    assertThat(type.toString()).isEqualTo("? super java.lang.String");
  }

  @Test public void wildcardMirrorNoBounds() throws Exception {
    WildcardType wildcard = getTypes().getWildcardType(null, null);
    TypeName type = TypeName.get(wildcard);
    assertThat(type.toString()).isEqualTo("?");
  }

  @Test public void wildcardMirrorExtendsType() throws Exception {
    Types types = getTypes();
    Elements elements = getElements();
    TypeMirror charSequence = elements.getTypeElement(CharSequence.class.getName()).asType();
    WildcardType wildcard = types.getWildcardType(charSequence, null);
    TypeName type = TypeName.get(wildcard);
    assertThat(type.toString()).isEqualTo("? extends java.lang.CharSequence");
  }

  @Test public void wildcardMirrorSuperType() throws Exception {
    Types types = getTypes();
    Elements elements = getElements();
    TypeMirror string = elements.getTypeElement(String.class.getName()).asType();
    WildcardType wildcard = types.getWildcardType(null, string);
    TypeName type = TypeName.get(wildcard);
    assertThat(type.toString()).isEqualTo("? super java.lang.String");
  }

  @Test public void typeVariable() throws Exception {
    TypeVariableName type = TypeVariableName.get("T", CharSequence.class);
    assertThat(type.toString()).isEqualTo("T"); // (Bounds are only emitted in declaration.)
  }

  @Test public void box() throws Exception {
    assertThat(TypeName.INT.box()).isEqualTo(ClassName.get(Integer.class));
    assertThat(TypeName.VOID.box()).isEqualTo(ClassName.get(Void.class));
    assertThat(ClassName.get(Integer.class).box()).isEqualTo(ClassName.get(Integer.class));
    assertThat(ClassName.get(Void.class).box()).isEqualTo(ClassName.get(Void.class));
    assertThat(TypeName.OBJECT.box()).isEqualTo(TypeName.OBJECT);
    assertThat(ClassName.get(String.class).box()).isEqualTo(ClassName.get(String.class));
  }

  @Test public void unbox() throws Exception {
    assertThat(TypeName.INT).isEqualTo(TypeName.INT.unbox());
    assertThat(TypeName.VOID).isEqualTo(TypeName.VOID.unbox());
    assertThat(ClassName.get(Integer.class).unbox()).isEqualTo(TypeName.INT.unbox());
    assertThat(ClassName.get(Void.class).unbox()).isEqualTo(TypeName.VOID.unbox());
    try {
      TypeName.OBJECT.unbox();
      fail();
    } catch (UnsupportedOperationException expected) {
    }
    try {
      ClassName.get(String.class).unbox();
      fail();
    } catch (UnsupportedOperationException expected) {
    }
  }
}