/*
 * Copyright (C) 2012 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 com.google.testing.compile.CompilationRule;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.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 org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

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

  private Types typeUtils;
  private Elements elementUtils;

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

  private static class Erasure<T> {
    int intNo;
    boolean booleanNo;
    int[] intArrayNo;
    String stringNo;
    String[] stringArrayNo;
    @SuppressWarnings("rawtypes")
    List rawListNo;
    List<?> listOfQueryNo;
    List<? extends Object> listOfQueryExtendsObjectNo;
    Map<?, ?> mapQueryToQueryNo;

    List<String> listOfStringYes;
    List<? extends String> listOfQueryExtendsStringYes;
    List<? super String> listOfQuerySuperStringYes;
    List<T> listOfTypeVarYes;
    List<? extends T> listOfQueryExtendsTypeVarYes;
    List<? super T> listOfQuerySuperTypeVarYes;
  }

  private abstract static class Wildcards {
    abstract <T extends V, U extends T, V> Map<? extends T, ? super U> one();
    abstract <T extends V, U extends T, V> Map<? extends T, ? super U> two();
  }

  /**
   * 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 must 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.
   */
  @Test
  public void testQuirkyTypeMirrors() {
    TypeMirror objectMirror = objectMirror();
    TypeMirror cloneReturnTypeMirror = cloneReturnTypeMirror();
    assertThat(objectMirror).isNotEqualTo(cloneReturnTypeMirror);
    assertThat(typeUtils.isSameType(objectMirror, cloneReturnTypeMirror)).isTrue();
  }

  @Test
  public void testTypeMirrorSet() {
    // Test the TypeMirrorSet methods. Resist the temptation to rewrite these in terms of
    // Truth operations! For example, don't change assertThat(set.size()).isEqualTo(0) into
    // assertThat(set).isEmpty(), because then we wouldn't be testing size().
    TypeMirror objectMirror = objectMirror();
    TypeMirror otherObjectMirror = cloneReturnTypeMirror();
    Set<TypeMirror> set = new TypeMirrorSet();
    assertThat(set.size()).isEqualTo(0);
    assertThat(set.isEmpty()).isTrue();
    boolean added = set.add(objectMirror);
    assertThat(added).isTrue();
    assertThat(set.size()).isEqualTo(1);

    Set<TypeMirror> otherSet = typeMirrorSet(otherObjectMirror);
    assertThat(otherSet).isEqualTo(set);
    assertThat(set).isEqualTo(otherSet);
    assertThat(otherSet.hashCode()).isEqualTo(set.hashCode());

    assertThat(set.add(otherObjectMirror)).isFalse();
    assertThat(set.contains(otherObjectMirror)).isTrue();

    assertThat(set.contains(null)).isFalse();
    assertThat(set.contains((Object) "foo")).isFalse();
    assertThat(set.remove(null)).isFalse();
    assertThat(set.remove((Object) "foo")).isFalse();

    TypeElement list = typeElementOf(java.util.List.class);
    TypeMirror listOfObjectMirror = typeUtils.getDeclaredType(list, objectMirror);
    TypeMirror listOfOtherObjectMirror = typeUtils.getDeclaredType(list, otherObjectMirror);
    assertThat(listOfObjectMirror.equals(listOfOtherObjectMirror)).isFalse();
    assertThat(typeUtils.isSameType(listOfObjectMirror, listOfOtherObjectMirror)).isTrue();
    added = set.add(listOfObjectMirror);
    assertThat(added).isTrue();
    assertThat(set.size()).isEqualTo(2);
    assertThat(set.add(listOfOtherObjectMirror)).isFalse();
    assertThat(set.contains(listOfOtherObjectMirror)).isTrue();

    boolean removed = set.remove(listOfOtherObjectMirror);
    assertThat(removed).isTrue();
    assertThat(set.contains(listOfObjectMirror)).isFalse();

    set.removeAll(otherSet);
    assertThat(set.isEmpty()).isTrue();
  }

  @Test
  public void testTypeMirrorSetWildcardCapture() {
    // TODO(emcmanus): 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.class);
    List<? extends ExecutableElement> methods =
        ElementFilter.methodsIn(wildcardsElement.getEnclosedElements());
    assertThat(methods).hasSize(2);
    ExecutableElement one = methods.get(0);
    ExecutableElement two = methods.get(1);
    assertThat(one.getSimpleName().toString()).isEqualTo("one");
    assertThat(two.getSimpleName().toString()).isEqualTo("two");
    TypeMirrorSet typeMirrorSet = new TypeMirrorSet();
    assertThat(typeMirrorSet.add(one.getReturnType())).isTrue();
    assertThat(typeMirrorSet.add(two.getReturnType())).isFalse();
    DeclaredType captureOne = (DeclaredType) typeUtils.capture(one.getReturnType());
    assertThat(typeMirrorSet.add(captureOne)).isTrue();
    DeclaredType captureTwo = (DeclaredType) typeUtils.capture(two.getReturnType());
    assertThat(typeMirrorSet.add(captureTwo)).isFalse();
    // Reminder: captureOne is Map<?#123 extends T, ?#456 super U>
    TypeVariable extendsT = (TypeVariable) captureOne.getTypeArguments().get(0);
    assertThat(typeMirrorSet.add(extendsT)).isTrue();
    assertThat(typeMirrorSet.add(extendsT.getLowerBound())).isTrue();  // NoType
    for (TypeMirror bound : ((TypeParameterElement) extendsT.asElement()).getBounds()) {
      assertThat(typeMirrorSet.add(bound)).isTrue();
    }
    TypeVariable superU = (TypeVariable) captureOne.getTypeArguments().get(1);
    assertThat(typeMirrorSet.add(superU)).isTrue();
    assertThat(typeMirrorSet.add(superU.getLowerBound())).isTrue();
  }

  @Test
  public void testPackageNameOfString() {
    assertThat(TypeSimplifier.packageNameOf(typeElementOf(java.lang.String.class)))
        .isEqualTo("java.lang");
  }

  @Test
  public void testPackageNameOfMapEntry() {
    assertThat(TypeSimplifier.packageNameOf(typeElementOf(java.util.Map.Entry.class)))
        .isEqualTo("java.util");
  }

  // Test TypeSimplifier.isCastingUnchecked. We do this by examining the fields of the Erasure
  // class. 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.
  @Test
  public void testIsCastingUnchecked() {
    TypeElement erasureClass = typeElementOf(Erasure.class);
    List<VariableElement> fields = ElementFilter.fieldsIn(erasureClass.getEnclosedElements());
    assertThat(fields).isNotEmpty();
    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);
      assertThat(actualUnchecked)
          .named("Unchecked-cast status for " + fieldType)
          .isEqualTo(expectUnchecked);
    }
  }

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

  private TypeMirror objectMirror() {
    return typeMirrorOf(Object.class);
  }

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

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

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