/*
 * Copyright 2014 Google Inc. All rights reserved.
 *
 * 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 org.inferred.freebuilder.processor.source.testing;

import static com.google.common.collect.Iterables.getOnlyElement;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import static javax.lang.model.util.ElementFilter.methodsIn;

import com.google.common.reflect.TypeToken;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.List;
import java.util.Map;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;

/** Tests for {@link Model}. */
@RunWith(JUnit4.class)
public class ModelTest {

  @Rule public final ExpectedException thrown = ExpectedException.none();

  private Model model;

  @Before
  public void setup() {
    model = Model.create();
  }

  @Test
  public void typeUtils() {
    PrimitiveType booleanType = model.typeUtils().getPrimitiveType(TypeKind.BOOLEAN);
    assertEquals("boolean", booleanType.toString());
  }

  @Test
  public void elementUtils() {
    String oneHundredAndOne = model.elementUtils().getConstantExpression(101L);
    assertEquals("101L", oneHundredAndOne);
  }

  @Test
  public void environment() {
    assertSame(model.elementUtils(), model.environment().getElementUtils());
  }

  @Test
  public void stressTestElementUtils() {
    for (int i = 0; i < 50; i++) {
      Model newModel = Model.create();
      Elements elementUtils = newModel.elementUtils();
      TypeElement stringType = elementUtils.getTypeElement("java.lang.String");
      assertNotNull("String type element was null on iteration #" + (i + 1), stringType);
      newModel.destroy();
    }
  }

  @Test
  public void newElementWithMarker() {
    TypeElement type = (TypeElement) model.newElementWithMarker(
        "package foo.bar;",
        "public class MyType {",
        "  ---> public class MyInnerType {",
        "    public void doNothing() { }",
        "  }",
        "}");
    assertEquals(ElementKind.CLASS, type.getKind());
    assertEquals(NestingKind.MEMBER, type.getNestingKind());
    assertEquals("MyInnerType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType.MyInnerType", type.toString());
    assertEquals("doNothing",
        getOnlyElement(methodsIn(type.getEnclosedElements())).getSimpleName().toString());
  }

  @Test
  public void newElementWithMarker_noElementIdentified() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Code must identify the element to be returned using '--->'");
    model.newElementWithMarker(
        "package foo.bar;",
        "public class MyType {",
        "  public class MyInnerType {",
        "    public void doNothing() { }",
        "  }",
        "}");
  }

  @Test
  public void newElementAnnotatedWith() {
    TypeElement type = (TypeElement) model.newElementAnnotatedWith(
        Deprecated.class,
        "package foo.bar;",
        "public class MyType {",
        "  @Deprecated public class MyInnerType {",
        "    public void doNothing() { }",
        "  }",
        "}");
    assertEquals(ElementKind.CLASS, type.getKind());
    assertEquals(NestingKind.MEMBER, type.getNestingKind());
    assertEquals("MyInnerType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType.MyInnerType", type.toString());
    assertEquals("doNothing",
        getOnlyElement(methodsIn(type.getEnclosedElements())).getSimpleName().toString());
  }

  @Test
  public void newElementAnnotatedWith_noElementIdentified() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("no annotated element");
    model.newElementAnnotatedWith(
        Deprecated.class,
        "package foo.bar;",
        "public class MyType {",
        "  public class MyInnerType {",
        "    public void doNothing() { }",
        "  }",
        "}");
  }

  @Test
  public void newElementAnnotatedWith_twoElementsIdentified() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Multiple elements annotated with @java.lang.Deprecated found");
    model.newElementAnnotatedWith(
        Deprecated.class,
        "package foo.bar;",
        "public class MyType {",
        "  @Deprecated public class MyInnerType {",
        "    @Deprecated public void doNothing() { }",
        "  }",
        "}");
  }

  @Test
  public void newElementAnnotatedWith_compilationError() {
    thrown.expect(CompilationException.class);
    model.newElementAnnotatedWith(
        Deprecated.class,
        "package foo.bar;",
        "public class MyType {",
        "  public class MyInnerType {",
        "    public void doNothing() {",
        "  }",
        "}");
  }

  @Test
  public void newType_class() {
    TypeElement type = model.newType(
        "package foo.bar;",
        "public class MyType {",
        "  public void doNothing() { }",
        "}");
    assertEquals(ElementKind.CLASS, type.getKind());
    assertEquals(NestingKind.TOP_LEVEL, type.getNestingKind());
    assertEquals("MyType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType", type.toString());
    assertEquals("doNothing",
        getOnlyElement(methodsIn(type.getEnclosedElements())).getSimpleName().toString());
  }

  @Test
  public void newType_interface() {
    TypeElement type = model.newType(
        "package foo.bar;",
        "public interface MyType {",
        "  public void doNothing();",
        "}");
    assertEquals(ElementKind.INTERFACE, type.getKind());
    assertEquals(NestingKind.TOP_LEVEL, type.getNestingKind());
    assertEquals("MyType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType", type.toString());
    assertEquals("doNothing",
        getOnlyElement(methodsIn(type.getEnclosedElements())).getSimpleName().toString());
  }

  @Test
  public void newType_enum() {
    TypeElement type = model.newType(
        "package foo.bar;",
        "public enum MyType { A, B; }");
    assertEquals(ElementKind.ENUM, type.getKind());
    assertEquals(NestingKind.TOP_LEVEL, type.getNestingKind());
    assertEquals("MyType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType", type.toString());
  }

  @Test
  public void newType_annotation() {
    TypeElement type = model.newType(
        "package foo.bar;",
        "public @interface MyType {",
        "  String param();",
        "}");
    assertEquals(ElementKind.ANNOTATION_TYPE, type.getKind());
    assertEquals(NestingKind.TOP_LEVEL, type.getNestingKind());
    assertEquals("MyType", type.getSimpleName().toString());
    assertEquals("foo.bar.MyType", type.toString());
    assertEquals("param",
        getOnlyElement(methodsIn(type.getEnclosedElements())).getSimpleName().toString());
  }

  @Test
  public void typeMirror_string_nonGenericType() {
    TypeMirror t1 = model.typeMirror("String");
    assertEquals("java.lang.String", t1.toString());
    TypeMirror t2 = model.typeMirror("String");
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_string_genericType() {
    TypeMirror t1 = model.typeMirror("java.util.List<Integer>");
    assertEquals("java.util.List<java.lang.Integer>", t1.toString());
    TypeMirror t2 = model.typeMirror("java.util.List<Integer>");
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_string_arrayOfGenericsType() {
    TypeMirror t1 = model.typeMirror("java.util.List<String>[]");
    assertEquals("java.util.List<java.lang.String>[]", t1.toString());
    TypeMirror t2 = model.typeMirror("java.util.List<String>[]");
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_string_genericOfArraysType() {
    TypeMirror t1 = model.typeMirror("java.util.List<String[]>");
    assertEquals("java.util.List<java.lang.String[]>", t1.toString());
    TypeMirror t2 = model.typeMirror("java.util.List<String[]>");
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_string_nestedGenericType() {
    TypeMirror t1 = model.typeMirror("java.util.Map<String,java.util.List<Integer>>");
    assertEquals(
        "java.util.Map<java.lang.String,java.util.List<java.lang.Integer>>", t1.toString());
    TypeMirror t2 = model.typeMirror("java.util.Map<String,java.util.List<Integer>>");
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_string_argumentSubstitution() {
    TypeMirror t1 = model.typeMirror("java.util.Map<Integer, String>");
    assertEquals("java.util.Map<java.lang.Integer,java.lang.String>", t1.toString());
    TypeMirror t2 = model.typeMirror(
        "java.util.Map<%1, %2>", model.typeMirror("Integer"), model.typeMirror("String"));
    assertEquals("java.util.Map<java.lang.Integer,java.lang.String>", t2.toString());
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_string_rawTypeSubstitution() {
    TypeMirror t = model.typeMirror(
        "%1<%2>", model.typeMirror("java.util.List"), model.typeMirror("Integer"));
    assertEquals("java.util.List<java.lang.Integer>", t.toString());
  }

  @Test
  public void typeMirror_string_rawTypeSubstitutionError() {
    TypeMirror stringList = model.typeMirror("java.util.List<String>");
    TypeMirror integer = model.typeMirror("Integer");
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage(
        "Expected raw type, got 'java.util.List<java.lang.String>'");
    model.typeMirror("%1<%2>", stringList, integer);
  }

  @Test
  public void typeMirror_string_wrongNumberOfGenericParameters() {
    TypeMirror stringList = model.typeMirror("java.util.List<String>");
    TypeMirror integer = model.typeMirror("Integer");
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Incorrect number of arguments for java.util.List (expected 1, got 2)");
    model.typeMirror("java.util.List<%1, %2>", stringList, integer);
  }

  @Test
  public void typeMirror_string_illegalSyntax() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Invalid type string");
    model.typeMirror("java.util.List<String><String>");
  }

  @Test
  public void typeMirror_string_illegalSyntax2() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Invalid type string");
    model.typeMirror("java.util.List, java.util.List");
  }

  @Test
  public void typeMirror_string_illegalSyntax3() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Invalid type string");
    model.typeMirror("java.util.List[]<String>");
  }

  @Test
  public void typeMirror_string_illegalSyntax4() {
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("Invalid type string");
    model.typeMirror("java.util.List[][]");
  }

  @Test
  public void typeMirror_class_void() {
    TypeMirror t1 = model.typeMirror(void.class);
    assertEquals("void", t1.toString());
    TypeMirror t2 = model.typeMirror(void.class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_primitiveType() {
    TypeMirror t1 = model.typeMirror(int.class);
    assertEquals("int", t1.toString());
    TypeMirror t2 = model.typeMirror(int.class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_primitiveArray() {
    TypeMirror t1 = model.typeMirror(int[].class);
    assertEquals("int[]", t1.toString());
    TypeMirror t2 = model.typeMirror(int[].class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_nonGenericType() {
    TypeMirror t1 = model.typeMirror(String.class);
    assertEquals("java.lang.String", t1.toString());
    TypeMirror t2 = model.typeMirror(String.class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_nonGenericArray() {
    TypeMirror t1 = model.typeMirror(String[].class);
    assertEquals("java.lang.String[]", t1.toString());
    TypeMirror t2 = model.typeMirror(String[].class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_compilationBetweenInstances() {
    TypeMirror t1 = model.typeMirror("java.lang.String");
    TypeMirror t2 = model.typeMirror(String.class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_genericType() {
    TypeMirror t1 = model.typeMirror(List.class);
    assertEquals("java.util.List", t1.toString());
    TypeMirror t2 = model.typeMirror(List.class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_class_genericArray() {
    TypeMirror t1 = model.typeMirror(List[].class);
    assertEquals("java.util.List[]", t1.toString());
    TypeMirror t2 = model.typeMirror(List[].class);
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_typeToken_nonGenericType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<String>() {});
    assertEquals("java.lang.String", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<String>() {});
    assertEquals(t1, t2);
  }

  @Test
  public void typeMirror_typeToken_genericType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<Integer>>() {});
    assertEquals("java.util.List<java.lang.Integer>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<Integer>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @SuppressWarnings("rawtypes")
  @Test
  public void typeMirror_typeToken_rawType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List>() {});
    assertEquals("java.util.List", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_wildcard() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<?>>() {});
    assertEquals("java.util.List<?>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<?>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_wildcardBoundedAboveType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<? super Number>>() {});
    assertEquals("java.util.List<? super java.lang.Number>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<? super Number>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_wildcardBoundedBelow() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<? extends Number>>() {});
    assertEquals("java.util.List<? extends java.lang.Number>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<? extends Number>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_arrayOfGenericsType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<String>[]>() {});
    assertEquals("java.util.List<java.lang.String>[]", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<String>[]>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_genericOfArraysType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<List<String[]>>() {});
    assertEquals("java.util.List<java.lang.String[]>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<List<String[]>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirror_typeToken_nestedGenericType() {
    TypeMirror t1 = model.typeMirror(new TypeToken<Map<String, List<Integer>>>() {});
    assertEquals(
        "java.util.Map<java.lang.String,java.util.List<java.lang.Integer>>", t1.toString());
    TypeMirror t2 = model.typeMirror(new TypeToken<Map<String, List<Integer>>>() {});
    assertTrue("Same type", model.typeUtils().isSameType(t1, t2));
  }

  @Test
  public void typeMirrorEqualsMethodReturnType() {
    ExecutableElement method = (ExecutableElement) model.newElementWithMarker(
        "package example.com;",
        "interface Foo {",
        "  ---> String method();",
        "}");
    TypeMirror stringType = model.typeMirror("String");
    assertTrue("Same type", model.typeUtils().isSameType(stringType, method.getReturnType()));
  }

  @After
  public void teardown() {
    model.destroy();
  }
}
