/*
 * Copyright (C) 2016 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.common;

import static com.google.common.base.StandardSystemProperty.JAVA_SPECIFICATION_VERSION;
import static com.google.common.truth.Truth.assertThat;
import static javax.lang.model.util.ElementFilter.methodsIn;

import com.google.common.base.Charsets;
import com.google.common.base.Converter;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Range;
import com.google.common.io.Files;
import com.google.common.truth.Expect;
import com.google.testing.compile.CompilationRule;
import java.io.File;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
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.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.model.Statement;

/**
 * Tests that the {@link Overrides} class has behaviour consistent with javac. We test this in
 * two ways: once with {@link Overrides.ExplicitOverrides} using javac's own {@link Elements} and
 * {@link Types}, and once with it using the version of those objects from the Eclipse compiler
 * (ecj).
 *
 * @author emcmanus@google.com (Éamonn McManus)
 */
@RunWith(Parameterized.class)
public class OverridesTest {
  @Parameterized.Parameters(name = "{0}")
  public static List<Object[]> data() {
    List<Object[]> compilerTypes = new ArrayList<Object[]>();
    compilerTypes.add(new Object[] {CompilerType.JAVAC});
    if (!"9".equals(JAVA_SPECIFICATION_VERSION.value())) {
      // TODO(emcmanus): make this test pass with ECJ on Java 9.
      // Currently it complains because it can't find java.lang.Object. Probably we need a newer
      // version of ECJ.
      compilerTypes.add(new Object[] {CompilerType.ECJ});
    }
    return compilerTypes;
  }

  @Rule public CompilationRule compilation = new CompilationRule();
  @Rule public EcjCompilationRule ecjCompilation = new EcjCompilationRule();
  @Rule public Expect expect = Expect.create();

  public enum CompilerType {
    JAVAC {
      @Override
      void initUtils(OverridesTest test) {
        test.typeUtils = test.compilation.getTypes();
        test.elementUtils = test.compilation.getElements();
      }
    },
    ECJ {
      @Override
      void initUtils(OverridesTest test) {
        test.typeUtils = test.ecjCompilation.types;
        test.elementUtils = test.ecjCompilation.elements;
      }
    };

    abstract void initUtils(OverridesTest test);
  }
  private final CompilerType compilerType;

  private Types typeUtils;
  private Elements elementUtils;
  private Elements javacElementUtils;
  private Overrides javacOverrides;
  private Overrides.ExplicitOverrides explicitOverrides;

  public OverridesTest(CompilerType compilerType) {
    this.compilerType = compilerType;
  }

  @Before
  public void initializeTestElements() {
    javacElementUtils = compilation.getElements();
    javacOverrides = new Overrides.NativeOverrides(javacElementUtils);
    compilerType.initUtils(this);
    explicitOverrides = new Overrides.ExplicitOverrides(typeUtils);
  }

  static class TypesForInheritance {
    interface One {
      void m();
      void m(String x);
      void n();
    }

    interface Two {
      void m();
      void m(int x);
    }

    static class Parent {
      public void m() {}
    }

    static class ChildOfParent extends Parent {}

    static class ChildOfOne implements One {
      @Override public void m() {}
      @Override public void m(String x) {}
      @Override public void n() {}
    }

    static class ChildOfOneAndTwo implements One, Two {
      @Override public void m() {}
      @Override public void m(String x) {}
      @Override public void m(int x) {}
      @Override public void n() {}
    }

    static class ChildOfParentAndOne extends Parent implements One {
      @Override public void m() {}
      @Override public void m(String x) {}
      @Override public void n() {}
    }

    static class ChildOfParentAndOneAndTwo extends Parent implements One, Two {
      @Override public void m(String x) {}
      @Override public void m(int x) {}
      @Override public void n() {}
    }

    abstract static class AbstractChildOfOne implements One {}

    abstract static class AbstractChildOfOneAndTwo implements One, Two {}

    abstract static class AbstractChildOfParentAndOneAndTwo extends Parent implements One, Two {}
  }

  static class MoreTypesForInheritance {
    interface Key {}

    interface BindingType {}

    interface ContributionType {}

    interface HasKey {
      Key key();
    }

    interface HasBindingType {
      BindingType bindingType();
    }

    interface HasContributionType {
      ContributionType contributionType();
    }

    abstract static class BindingDeclaration implements HasKey {
      abstract Optional<Element> bindingElement();
      abstract Optional<TypeElement> contributingModule();
    }

    abstract static class MultibindingDeclaration
        extends BindingDeclaration implements HasBindingType, HasContributionType {
      @Override public abstract Key key();
      @Override public abstract ContributionType contributionType();
      @Override public abstract BindingType bindingType();
    }
  }

  static class TypesForVisibility {
    public abstract static class PublicGrandparent {
      public abstract String foo();
    }

    private static class PrivateParent extends PublicGrandparent {
      @Override
      public String foo() {
        return "foo";
      }
    }

    static class Child extends PrivateParent {}
  }

  static class TypesForGenerics {
    interface XCollection<E> {
      boolean add(E x);
    }

    interface XList<E> extends XCollection<E> {
      @Override public boolean add(E x);
    }

    static class StringList implements XList<String> {
      @Override public boolean add(String x) {
        return false;
      }
    }
  }

  @SuppressWarnings("rawtypes")
  static class TypesForRaw {
    static class RawParent {
      void frob(List x) {}
    }

    static class RawChildOfRaw extends RawParent {
      @Override void frob(List x) {}
    }

    static class NonRawParent {
      void frob(List<String> x) {}
    }

    static class RawChildOfNonRaw extends NonRawParent {
      @Override void frob(List x) {}
    }
  }

  @Test
  public void overridesInheritance() {
    checkOverridesInContainedClasses(TypesForInheritance.class);
  }

  @Test
  public void overridesMoreInheritance() {
    checkOverridesInContainedClasses(MoreTypesForInheritance.class);
  }

  @Test
  public void overridesVisibility() {
    checkOverridesInContainedClasses(TypesForVisibility.class);
  }

  @Test
  public void overridesGenerics() {
    checkOverridesInContainedClasses(TypesForGenerics.class);
  }

  @Test
  public void overridesRaw() {
    checkOverridesInContainedClasses(TypesForRaw.class);
  }

  // Test a tricky diamond inheritance hierarchy:
  //               Collection
  //              /          \
  // AbstractCollection     List
  //              \          /
  //              AbstractList
  // This also tests that we do the right thing with generics, since naively the TypeMirror
  // that you get for List<E> will not appear to be a subtype of the one you get for Collection<E>
  // since the two Es are not the same.
  @Test
  public void overridesDiamond() {
    checkOverridesInSet(ImmutableSet.<Class<?>>of(
        Collection.class, List.class, AbstractCollection.class, AbstractList.class));
  }

  private void checkOverridesInContainedClasses(Class<?> container) {
    checkOverridesInSet(ImmutableSet.copyOf(container.getDeclaredClasses()));
  }

  private void checkOverridesInSet(ImmutableSet<Class<?>> testClasses) {
    assertThat(testClasses).isNotEmpty();
    ImmutableSet.Builder<TypeElement> testTypesBuilder = ImmutableSet.builder();
    for (Class<?> testClass : testClasses) {
      testTypesBuilder.add(getTypeElement(testClass));
    }
    ImmutableSet<TypeElement> testTypes = testTypesBuilder.build();
    ImmutableSet.Builder<ExecutableElement> testMethodsBuilder = ImmutableSet.builder();
    for (TypeElement testType : testTypes) {
      testMethodsBuilder.addAll(methodsIn(testType.getEnclosedElements()));
    }
    ImmutableSet<ExecutableElement> testMethods = testMethodsBuilder.build();
    for (TypeElement in : testTypes) {
      TypeElement javacIn = javacType(in);
      List<ExecutableElement> inMethods = methodsIn(elementUtils.getAllMembers(in));
      for (ExecutableElement overrider : inMethods) {
        ExecutableElement javacOverrider = javacMethod(overrider);
        for (ExecutableElement overridden : testMethods) {
          ExecutableElement javacOverridden = javacMethod(overridden);
          boolean javacSays = javacOverrides.overrides(javacOverrider, javacOverridden, javacIn);
          boolean weSay = explicitOverrides.overrides(overrider, overridden, in);
          if (javacSays != weSay) {
            expect.fail(
                "%s.%s overrides %s.%s in %s: javac says %s, we say %s",
                overrider.getEnclosingElement(), overrider,
                overridden.getEnclosingElement(), overridden,
                in,
                javacSays, weSay);
          }
        }
      }
    }
  }

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

  private ExecutableElement getMethod(TypeElement in, String name, TypeKind... parameterTypeKinds) {
    ExecutableElement found = null;
    methods:
    for (ExecutableElement method : methodsIn(in.getEnclosedElements())) {
      if (method.getSimpleName().contentEquals(name)
          && method.getParameters().size() == parameterTypeKinds.length) {
        for (int i = 0; i < parameterTypeKinds.length; i++) {
          if (method.getParameters().get(i).asType().getKind() != parameterTypeKinds[i]) {
            continue methods;
          }
        }
        assertThat(found).isNull();
        found = method;
      }
    }
    assertThat(found).isNotNull();
    return found;
  }

  // These skeletal parallels to the real collection classes ensure that the test is independent
  // of the details of those classes, for example whether List<E> redeclares add(E) even though
  // it also inherits it from Collection<E>.

  private interface XCollection<E> {
    boolean add(E e);
  }

  private interface XList<E> extends XCollection<E> {}

  private abstract static class XAbstractCollection<E> implements XCollection<E> {
    @Override
    public boolean add(E e) {
      return false;
    }
  }

  private abstract static class XAbstractList<E>
      extends XAbstractCollection<E> implements XList<E> {
    @Override
    public boolean add(E e) {
      return true;
    }
  }

  private abstract static class XStringList extends XAbstractList<String> {}

  private abstract static class XAbstractStringList implements XList<String> {}

  private abstract static class XNumberList<E extends Number> extends XAbstractList<E> {}

  // Parameter of add(E) in StringList is String.
  // That means that we successfully recorded E[AbstractList] = String and E[List] = E[AbstractList]
  // and String made it all the way through.
  @Test
  public void methodParameters_StringList() {
    TypeElement xAbstractList = getTypeElement(XAbstractList.class);
    TypeElement xStringList = getTypeElement(XStringList.class);
    TypeElement string = getTypeElement(String.class);

    ExecutableElement add = getMethod(xAbstractList, "add", TypeKind.TYPEVAR);
    List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xStringList);
    List<TypeMirror> expectedParams = ImmutableList.of(string.asType());
    assertTypeListsEqual(params, expectedParams);
  }

  // Parameter of add(E) in AbstractStringList is String.
  // That means that we successfully recorded E[List] = String and E[Collection] = E[List].
  @Test
  public void methodParameters_AbstractStringList() {
    TypeElement xCollection = getTypeElement(XCollection.class);
    TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
    TypeElement string = getTypeElement(String.class);

    ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);

    List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xAbstractStringList);
    List<TypeMirror> expectedParams = ImmutableList.of(string.asType());
    assertTypeListsEqual(params, expectedParams);
  }

  // Parameter of add(E) in NumberList is Number.
  // That means that we successfully recorded E[AbstractList] = Number and on from
  // there, with Number being used because it is the erasure of <E extends Number>.
  @Test
  public void methodParams_NumberList() {
    TypeElement xCollection = getTypeElement(XCollection.class);
    TypeElement xNumberList = getTypeElement(XNumberList.class);
    TypeElement number = getTypeElement(Number.class);

    ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);

    List<TypeMirror> params = explicitOverrides.erasedParameterTypes(add, xNumberList);
    List<TypeMirror> expectedParams = ImmutableList.of(number.asType());
    assertTypeListsEqual(params, expectedParams);
  }

  // This is derived from a class that provoked a StackOverflowError in an earlier version.
  private abstract static class StringToRangeConverter<T extends Comparable<T>>
      extends Converter<String, Range<T>> {
    @Override
    protected String doBackward(Range<T> b) {
      return null;
    }
  }

  @Test
  public void methodParams_RecursiveBound() {
    TypeElement stringToRangeConverter = getTypeElement(StringToRangeConverter.class);
    TypeElement range = getTypeElement(Range.class);
    ExecutableElement valueConverter =
        getMethod(stringToRangeConverter, "doBackward", TypeKind.DECLARED);
    List<TypeMirror> params =
        explicitOverrides.erasedParameterTypes(valueConverter, stringToRangeConverter);
    List<TypeMirror> expectedParams =
        ImmutableList.<TypeMirror>of(typeUtils.erasure(range.asType()));
    assertTypeListsEqual(params, expectedParams);
  }

  @Test
  public void methodFromSuperclasses() {
    TypeElement xAbstractCollection = getTypeElement(XAbstractCollection.class);
    TypeElement xAbstractList = getTypeElement(XAbstractList.class);
    TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
    TypeElement xStringList = getTypeElement(XStringList.class);

    ExecutableElement add = getMethod(xAbstractCollection, "add", TypeKind.TYPEVAR);

    ExecutableElement addInAbstractStringList =
        explicitOverrides.methodFromSuperclasses(xAbstractStringList, add);
    assertThat(addInAbstractStringList).isNull();

    ExecutableElement addInStringList =
        explicitOverrides.methodFromSuperclasses(xStringList, add);
    assertThat(addInStringList.getEnclosingElement()).isEqualTo(xAbstractList);
  }

  @Test
  public void methodFromSuperinterfaces() {
    TypeElement xCollection = getTypeElement(XCollection.class);
    TypeElement xAbstractList = getTypeElement(XAbstractList.class);
    TypeElement xAbstractStringList = getTypeElement(XAbstractStringList.class);
    TypeElement xNumberList = getTypeElement(XNumberList.class);
    TypeElement xList = getTypeElement(XList.class);

    ExecutableElement add = getMethod(xCollection, "add", TypeKind.TYPEVAR);

    ExecutableElement addInAbstractStringList =
        explicitOverrides.methodFromSuperinterfaces(xAbstractStringList, add);
    assertThat(addInAbstractStringList.getEnclosingElement()).isEqualTo(xCollection);

    ExecutableElement addInNumberList =
        explicitOverrides.methodFromSuperinterfaces(xNumberList, add);
    assertThat(addInNumberList.getEnclosingElement()).isEqualTo(xAbstractList);

    ExecutableElement addInList =
        explicitOverrides.methodFromSuperinterfaces(xList, add);
    assertThat(addInList.getEnclosingElement()).isEqualTo(xCollection);
  }

  private void assertTypeListsEqual(List<TypeMirror> actual, List<TypeMirror> expected) {
    assertThat(actual.size()).isEqualTo(expected.size());
    for (int i = 0; i < actual.size(); i++) {
      assertThat(typeUtils.isSameType(actual.get(i), expected.get(i))).isTrue();
    }
  }

  // TODO(emcmanus): replace this with something from compile-testing when that's available.
  /**
   * An equivalent to {@link CompilationRule} that uses ecj (the Eclipse compiler) instead of javac.
   * If the parameterized test is not selecting ecj then this rule has no effect.
   */
  public class EcjCompilationRule implements TestRule {
    Elements elements;
    Types types;

    @Override
    public Statement apply(Statement base, Description description) {
      if (!compilerType.equals(CompilerType.ECJ)) {
        return base;
      }
      return new EcjCompilationStatement(base);
    }
  }

  private class EcjCompilationStatement extends Statement {
    private final Statement statement;

    EcjCompilationStatement(Statement base) {
      this.statement = base;
    }

    @Override
    public void evaluate() throws Throwable {
      File tmpDir = File.createTempFile("OverridesTest", "dir");
      tmpDir.delete();
      tmpDir.mkdir();
      File dummySourceFile = new File(tmpDir, "Dummy.java");
      try {
        Files.write("class Dummy {}", dummySourceFile, Charsets.UTF_8);
        evaluate(dummySourceFile);
      } finally {
        dummySourceFile.delete();
        tmpDir.delete();
      }
    }

    private void evaluate(File dummySourceFile) throws Throwable {
      JavaCompiler compiler = new EclipseCompiler();
      StandardJavaFileManager fileManager =
          compiler.getStandardFileManager(null, null, Charsets.UTF_8);
      Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjects(dummySourceFile);
      JavaCompiler.CompilationTask task =
          compiler.getTask(null, null, null, null, null, sources);
      EcjTestProcessor processor = new EcjTestProcessor(statement);
      task.setProcessors(ImmutableList.of(processor));
      assertThat(task.call()).isTrue();
      processor.maybeThrow();
    }
  }

  @SupportedAnnotationTypes("*")
  private class EcjTestProcessor extends AbstractProcessor {
    private final Statement statement;
    private Throwable thrown;

    EcjTestProcessor(Statement statement) {
      this.statement = statement;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latest();
    }

    @Override
    public boolean process(
        Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (roundEnv.processingOver()) {
        ecjCompilation.elements = processingEnv.getElementUtils();
        ecjCompilation.types = processingEnv.getTypeUtils();
        try {
          statement.evaluate();
        } catch (Throwable t) {
          thrown = t;
        }
      }
      return false;
    }

    void maybeThrow() throws Throwable {
      if (thrown != null) {
        throw thrown;
      }
    }
  }

  private TypeElement javacType(TypeElement type) {
    return javacElementUtils.getTypeElement(type.getQualifiedName().toString());
  }

  private ExecutableElement javacMethod(ExecutableElement method) {
    if (elementUtils == javacElementUtils) {
      return method;
    }
    TypeElement containingType = MoreElements.asType(method.getEnclosingElement());
    TypeElement javacContainingType = javacType(containingType);
    List<ExecutableElement> candidates = new ArrayList<ExecutableElement>();
    methods:
    for (ExecutableElement javacMethod : methodsIn(javacContainingType.getEnclosedElements())) {
      if (javacMethod.getSimpleName().contentEquals(method.getSimpleName())
          && javacMethod.getParameters().size() == method.getParameters().size()) {
        for (int i = 0; i < method.getParameters().size(); i++) {
          VariableElement parameter = method.getParameters().get(i);
          VariableElement javacParameter = javacMethod.getParameters().get(i);
          if (!erasedToString(parameter.asType()).equals(erasedToString(javacParameter.asType()))) {
            continue methods;
          }
        }
        candidates.add(javacMethod);
      }
    }
    if (candidates.size() == 1) {
      return candidates.get(0);
    } else {
      throw new IllegalStateException(
          "Expected one javac method matching " + method + " but found " + candidates);
    }
  }

  private static String erasedToString(TypeMirror type) {
    return ERASED_STRING_TYPE_VISITOR.visit(type);
  }

  private static final TypeVisitor<String, Void> ERASED_STRING_TYPE_VISITOR =
      new SimpleTypeVisitor6<String, Void>() {
    @Override
    protected String defaultAction(TypeMirror e, Void p) {
      return e.toString();
    }

    @Override
    public String visitArray(ArrayType t, Void p) {
      return visit(t.getComponentType()) + "[]";
    }

    @Override
    public String visitDeclared(DeclaredType t, Void p) {
      return MoreElements.asType(t.asElement()).getQualifiedName().toString();
    }

    @Override
    public String visitTypeVariable(TypeVariable t, Void p) {
      return visit(t.getUpperBound());
    }
  };
}
