/*
 * Copyright 2019 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 com.google.common.collect.ImmutableMap;
import com.google.common.truth.Expect;
import com.google.testing.compile.CompilationRule;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class TypeVariablesTest {
  @ClassRule public static final CompilationRule compilationRule = new CompilationRule();
  @Rule public final Expect expect = Expect.create();

  private static Elements elementUtils;
  private static Types typeUtils;

  @BeforeClass
  public static void setUpClass() {
    elementUtils = compilationRule.getElements();
    typeUtils = compilationRule.getTypes();
  }

  abstract static class Source1 {
    abstract String getFoo();
  }

  abstract static class Target1 {}

  @Test
  public void noTypeParameters() {
    TypeElement source1 = elementUtils.getTypeElement(Source1.class.getCanonicalName());
    TypeElement target1 = elementUtils.getTypeElement(Target1.class.getCanonicalName());
    List<ExecutableElement> sourceMethods = ElementFilter.methodsIn(source1.getEnclosedElements());
    Map<ExecutableElement, TypeMirror> types =
        TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source1, target1);
    assertThat(types).containsExactly(sourceMethods.get(0), sourceMethods.get(0).getReturnType());
  }

  abstract static class Source2<T> {
    abstract List<T> getFoo();
  }

  abstract static class Target2<T> {
    abstract void setFoo(List<T> list);
  }

  @Test
  public void simpleTypeParameter() {
    TypeElement source2 = elementUtils.getTypeElement(Source2.class.getCanonicalName());
    TypeElement target2 = elementUtils.getTypeElement(Target2.class.getCanonicalName());
    List<ExecutableElement> sourceMethods = ElementFilter.methodsIn(source2.getEnclosedElements());
    Map<ExecutableElement, TypeMirror> types =
        TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source2, target2);
    List<ExecutableElement> targetMethods = ElementFilter.methodsIn(target2.getEnclosedElements());
    TypeMirror setFooParameter = targetMethods.get(0).getParameters().get(0).asType();
    ExecutableElement getFoo = sourceMethods.get(0);
    TypeMirror originalGetFooReturn = getFoo.getReturnType();
    TypeMirror rewrittenGetFooReturn = types.get(getFoo);
    assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse();
    assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue();
  }

  abstract static class Source3<T extends Comparable<T>, U> {
    abstract Map<T, ? extends U> getFoo();
  }

  abstract static class Target3<T extends Comparable<T>, U> {
    abstract void setFoo(Map<T, ? extends U> list);
  }

  @Test
  public void hairyTypeParameters() {
    TypeElement source3 = elementUtils.getTypeElement(Source3.class.getCanonicalName());
    TypeElement target3 = elementUtils.getTypeElement(Target3.class.getCanonicalName());
    List<ExecutableElement> sourceMethods = ElementFilter.methodsIn(source3.getEnclosedElements());
    Map<ExecutableElement, TypeMirror> types =
        TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, sourceMethods, source3, target3);
    List<ExecutableElement> targetMethods = ElementFilter.methodsIn(target3.getEnclosedElements());
    TypeMirror setFooParameter = targetMethods.get(0).getParameters().get(0).asType();
    ExecutableElement getFoo = sourceMethods.get(0);
    TypeMirror originalGetFooReturn = getFoo.getReturnType();
    TypeMirror rewrittenGetFooReturn = types.get(getFoo);
    assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse();
    assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue();
  }

  abstract static class Outer<T, U extends T> {
    abstract Map<T, U> getFoo();
    abstract List<? extends T> getBar();

    abstract static class Inner<T, U extends T> {
      abstract void setFoo(Map<T, U> foo);
      abstract void setBar(List<? extends T> bar);
    }
  }

  @Test
  public void nestedClasses() {
    TypeElement outer = elementUtils.getTypeElement(Outer.class.getCanonicalName());
    TypeElement inner = elementUtils.getTypeElement(Outer.Inner.class.getCanonicalName());
    List<ExecutableElement> outerMethods = ElementFilter.methodsIn(outer.getEnclosedElements());
    Map<ExecutableElement, TypeMirror> types =
        TypeVariables.rewriteReturnTypes(elementUtils, typeUtils, outerMethods, outer, inner);
    List<ExecutableElement> innerMethods = ElementFilter.methodsIn(inner.getEnclosedElements());
    ExecutableElement getFoo = methodNamed(outerMethods, "getFoo");
    ExecutableElement getBar = methodNamed(outerMethods, "getBar");
    ExecutableElement setFoo = methodNamed(innerMethods, "setFoo");
    ExecutableElement setBar = methodNamed(innerMethods, "setBar");
    TypeMirror setFooParameter = setFoo.getParameters().get(0).asType();
    TypeMirror originalGetFooReturn = getFoo.getReturnType();
    TypeMirror rewrittenGetFooReturn = types.get(getFoo);
    assertThat(typeUtils.isAssignable(setFooParameter, originalGetFooReturn)).isFalse();
    assertThat(typeUtils.isAssignable(setFooParameter, rewrittenGetFooReturn)).isTrue();
    TypeMirror setBarParameter = setBar.getParameters().get(0).asType();
    TypeMirror originalGetBarReturn = getBar.getReturnType();
    TypeMirror rewrittenGetBarReturn = types.get(getBar);
    assertThat(typeUtils.isAssignable(setBarParameter, originalGetBarReturn)).isFalse();
    assertThat(typeUtils.isAssignable(setBarParameter, rewrittenGetBarReturn)).isTrue();
  }

  @Test
  public void canAssignStaticMethodResult() {
    TypeElement immutableMap = elementUtils.getTypeElement(ImmutableMap.class.getCanonicalName());
    TypeElement string = elementUtils.getTypeElement(String.class.getCanonicalName());
    TypeElement integer = elementUtils.getTypeElement(Integer.class.getCanonicalName());
    TypeElement number = elementUtils.getTypeElement(Number.class.getCanonicalName());
    TypeMirror immutableMapStringNumber =
        typeUtils.getDeclaredType(immutableMap, string.asType(), number.asType());
    TypeMirror immutableMapStringInteger =
        typeUtils.getDeclaredType(immutableMap, string.asType(), integer.asType());
    TypeElement map = elementUtils.getTypeElement(Map.class.getCanonicalName());
    TypeMirror erasedMap = typeUtils.erasure(map.asType());
    // If the target type is ImmutableMap<String, Number> then we should be able to use
    //   static <K, V> ImmutableMap<K, V> ImmutableMap.copyOf(Map<? extends K, ? extends V>)
    // with a parameter of type ImmutableMap<String, Integer>.
    List<ExecutableElement> immutableMapMethods =
        ElementFilter.methodsIn(immutableMap.getEnclosedElements());
    ExecutableElement copyOf = methodNamed(immutableMapMethods, "copyOf", erasedMap);
    expect.that(
        TypeVariables.canAssignStaticMethodResult(
            copyOf, immutableMapStringInteger, immutableMapStringNumber, typeUtils))
        .isTrue();
    expect.that(
        TypeVariables.canAssignStaticMethodResult(
            copyOf, immutableMapStringNumber, immutableMapStringInteger, typeUtils))
        .isFalse();
  }

  private static ExecutableElement methodNamed(List<ExecutableElement> methods, String name) {
    return methods.stream().filter(m -> m.getSimpleName().contentEquals(name)).findFirst().get();
  }

  private static ExecutableElement methodNamed(
      List<ExecutableElement> methods, String name, TypeMirror erasedParameterType) {
    return methods.stream()
        .filter(m -> m.getSimpleName().contentEquals(name))
        .filter(m -> m.getParameters().size() == 1)
        .filter(m -> typeUtils.isSameType(
                    erasedParameterType, typeUtils.erasure(m.getParameters().get(0).asType())))
        .findFirst()
        .get();
  }
}
