/*
 * Copyright 2019 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 com.google.turbine.processing;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static javax.lang.model.util.ElementFilter.typesIn;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.turbine.binder.Binder;
import com.google.turbine.binder.Binder.BindingResult;
import com.google.turbine.binder.ClassPathBinder;
import com.google.turbine.binder.Processing;
import com.google.turbine.binder.Processing.ProcessorInfo;
import com.google.turbine.diag.SourceFile;
import com.google.turbine.diag.TurbineDiagnostic;
import com.google.turbine.diag.TurbineError;
import com.google.turbine.lower.IntegrationTestSupport;
import com.google.turbine.parse.Parser;
import com.google.turbine.testing.TestClassPaths;
import com.google.turbine.tree.Tree;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ProcessingIntegrationTest {

  @SupportedAnnotationTypes("*")
  public static class CrashingProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      throw new RuntimeException("crash!");
    }
  }

  @Test
  public void crash() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== Test.java ===", //
            "@Deprecated",
            "class Test extends NoSuch {",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    Processing.ProcessorInfo.create(
                        ImmutableList.of(new CrashingProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    ImmutableList<String> messages =
        e.diagnostics().stream().map(TurbineDiagnostic::message).collect(toImmutableList());
    assertThat(messages).hasSize(2);
    assertThat(messages.get(0)).contains("could not resolve NoSuch");
    assertThat(messages.get(1)).contains("crash!");
  }

  @SupportedAnnotationTypes("*")
  public static class WarningProcessor extends AbstractProcessor {

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

    private boolean first = true;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (first) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "proc warning");
        try {
          JavaFileObject file = processingEnv.getFiler().createSourceFile("Gen.java");
          try (Writer writer = file.openWriter()) {
            writer.write("class Gen {}");
          }
        } catch (IOException e) {
          throw new UncheckedIOException(e);
        }
        first = false;
      }
      if (roundEnv.processingOver()) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "proc error");
      }
      return false;
    }
  }

  @Test
  public void warnings() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== Test.java ===", //
            "@Deprecated",
            "class Test {",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    Processing.ProcessorInfo.create(
                        ImmutableList.of(new WarningProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    ImmutableList<String> diags =
        e.diagnostics().stream().map(d -> d.message()).collect(toImmutableList());
    assertThat(diags).hasSize(2);
    assertThat(diags.get(0)).contains("proc warning");
    assertThat(diags.get(1)).contains("proc error");
  }

  @SupportedAnnotationTypes("*")
  public static class ResourceProcessor extends AbstractProcessor {

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

    private boolean first = true;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (first) {
        try {
          try (Writer writer = processingEnv.getFiler().createSourceFile("Gen").openWriter()) {
            writer.write("class Gen {}");
          }
          try (Writer writer =
              processingEnv
                  .getFiler()
                  .createResource(StandardLocation.SOURCE_OUTPUT, "", "source.txt")
                  .openWriter()) {
            writer.write("hello source output");
          }
          try (Writer writer =
              processingEnv
                  .getFiler()
                  .createResource(StandardLocation.CLASS_OUTPUT, "", "class.txt")
                  .openWriter()) {
            writer.write("hello class output");
          }
        } catch (IOException e) {
          throw new UncheckedIOException(e);
        }
        first = false;
      }
      return false;
    }
  }

  @Test
  public void resources() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== Test.java ===", //
            "@Deprecated",
            "class Test {",
            "}");
    BindingResult bound =
        Binder.bind(
            units,
            ClassPathBinder.bindClasspath(ImmutableList.of()),
            ProcessorInfo.create(
                ImmutableList.of(new ResourceProcessor()),
                getClass().getClassLoader(),
                ImmutableMap.of(),
                SourceVersion.latestSupported()),
            TestClassPaths.TURBINE_BOOTCLASSPATH,
            Optional.empty());

    assertThat(bound.generatedSources().keySet()).containsExactly("Gen.java", "source.txt");
    assertThat(bound.generatedClasses().keySet()).containsExactly("class.txt");

    // The requireNonNull calls are safe because of the keySet checks above.
    assertThat(requireNonNull(bound.generatedSources().get("source.txt")).source())
        .isEqualTo("hello source output");
    assertThat(new String(requireNonNull(bound.generatedClasses().get("class.txt")), UTF_8))
        .isEqualTo("hello class output");
  }

  @Test
  public void getAllAnnotations() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== A.java ===", //
            "import java.lang.annotation.Inherited;",
            "@Inherited",
            "@interface A {}",
            "=== B.java ===", //
            "@interface B {}",
            "=== One.java ===", //
            "@A @B class One {}",
            "=== Two.java ===", //
            "class Two extends One {}");
    BindingResult bound =
        Binder.bind(
            units,
            ClassPathBinder.bindClasspath(ImmutableList.of()),
            ProcessorInfo.create(
                ImmutableList.of(new ElementsAnnotatedWithProcessor()),
                getClass().getClassLoader(),
                ImmutableMap.of(),
                SourceVersion.latestSupported()),
            TestClassPaths.TURBINE_BOOTCLASSPATH,
            Optional.empty());

    assertThat(
            Splitter.on(System.lineSeparator())
                .omitEmptyStrings()
                .split(
                    new String(
                        bound.generatedClasses().entrySet().stream()
                            .filter(s -> s.getKey().equals("output.txt"))
                            .collect(onlyElement())
                            .getValue(),
                        UTF_8)))
        .containsExactly("A: One, Two", "B: One");
  }

  @SupportedAnnotationTypes("*")
  private static class ElementsAnnotatedWithProcessor extends AbstractProcessor {

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

    private boolean first = true;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (first) {
        try (PrintWriter writer =
            new PrintWriter(
                processingEnv
                    .getFiler()
                    .createResource(StandardLocation.CLASS_OUTPUT, "", "output.txt")
                    .openWriter(),
                /* autoFlush= */ true)) {
          printAnnotatedElements(roundEnv, writer, "A");
          printAnnotatedElements(roundEnv, writer, "B");
        } catch (IOException e) {
          throw new UncheckedIOException(e);
        }
        first = false;
      }
      return false;
    }

    private void printAnnotatedElements(
        RoundEnvironment roundEnv, PrintWriter writer, String annotation) {
      writer.println(
          annotation
              + ": "
              + roundEnv
                  .getElementsAnnotatedWith(
                      processingEnv.getElementUtils().getTypeElement(annotation))
                  .stream()
                  .map(e -> e.getSimpleName().toString())
                  .collect(joining(", ")));
    }
  }

  private static void logError(
      ProcessingEnvironment processingEnv,
      RoundEnvironment roundEnv,
      Class<?> processorClass,
      int round) {
    processingEnv
        .getMessager()
        .printMessage(
            Diagnostic.Kind.ERROR,
            String.format(
                "%d: %s {errorRaised=%s, processingOver=%s}",
                round,
                processorClass.getSimpleName(),
                roundEnv.errorRaised(),
                roundEnv.processingOver()));
  }

  @SupportedAnnotationTypes("*")
  public static class ErrorProcessor extends AbstractProcessor {

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

    int round = 0;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      int round = ++this.round;
      logError(processingEnv, roundEnv, getClass(), round);
      String name = "Gen" + round;
      try (Writer writer = processingEnv.getFiler().createSourceFile(name).openWriter()) {
        writer.write(String.format("class %s {}", name));
      } catch (IOException e) {
        throw new UncheckedIOException(e);
      }
      return false;
    }
  }

  @SupportedAnnotationTypes("*")
  public static class FinalRoundErrorProcessor extends AbstractProcessor {

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

    int round = 0;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      int round = ++this.round;
      if (roundEnv.processingOver()) {
        logError(processingEnv, roundEnv, getClass(), round);
      }
      return false;
    }
  }

  @Test
  public void errorsAndFinalRound() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== Test.java ===", //
            "@Deprecated",
            "class Test {",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    Processing.ProcessorInfo.create(
                        ImmutableList.of(new ErrorProcessor(), new FinalRoundErrorProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    ImmutableList<String> diags =
        e.diagnostics().stream().map(d -> d.message()).collect(toImmutableList());
    assertThat(diags)
        .containsExactly(
            "1: ErrorProcessor {errorRaised=false, processingOver=false}",
            "2: ErrorProcessor {errorRaised=true, processingOver=true}",
            "2: FinalRoundErrorProcessor {errorRaised=true, processingOver=true}")
        .inOrder();
  }

  @SupportedAnnotationTypes("*")
  public static class SuperTypeProcessor extends AbstractProcessor {
    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      TypeElement typeElement = processingEnv.getElementUtils().getTypeElement("T");
      processingEnv
          .getMessager()
          .printMessage(
              Diagnostic.Kind.ERROR,
              typeElement.getSuperclass()
                  + " "
                  + processingEnv.getTypeUtils().directSupertypes(typeElement.asType()));
      return false;
    }
  }

  @Test
  public void superType() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "@Deprecated",
            "class T extends S {",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    Processing.ProcessorInfo.create(
                        ImmutableList.of(new SuperTypeProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    ImmutableList<String> diags =
        e.diagnostics().stream().map(d -> d.message()).collect(toImmutableList());
    assertThat(diags).containsExactly("could not resolve S", "S [S]").inOrder();
  }

  @SupportedAnnotationTypes("*")
  public static class GenerateAnnotationProcessor extends AbstractProcessor {

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

    private boolean first = true;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (first) {
        try {
          JavaFileObject file = processingEnv.getFiler().createSourceFile("A");
          try (Writer writer = file.openWriter()) {
            writer.write("@interface A {}");
          }
        } catch (IOException e) {
          throw new UncheckedIOException(e);
        }
        first = false;
      }
      return false;
    }
  }

  @Test
  public void generatedAnnotationDefinition() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "@interface B {",
            "  A value() default @A;",
            "}",
            "@B(value = @A)",
            "class T {",
            "}");
    BindingResult bound =
        Binder.bind(
            units,
            ClassPathBinder.bindClasspath(ImmutableList.of()),
            ProcessorInfo.create(
                ImmutableList.of(new GenerateAnnotationProcessor()),
                getClass().getClassLoader(),
                ImmutableMap.of(),
                SourceVersion.latestSupported()),
            TestClassPaths.TURBINE_BOOTCLASSPATH,
            Optional.empty());
    assertThat(bound.generatedSources()).containsKey("A.java");
  }

  @SupportedAnnotationTypes("*")
  public static class GenerateQualifiedProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      String superType =
          processingEnv.getElementUtils().getTypeElement("T").getSuperclass().toString();
      processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, superType);
      return false;
    }
  }

  @Test
  public void qualifiedErrorType() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "class T extends G.I {",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        ImmutableList.of(new GenerateQualifiedProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(
            e.diagnostics().stream()
                .filter(d -> d.severity().equals(Diagnostic.Kind.NOTE))
                .map(d -> d.message()))
        .containsExactly("G.I");
  }

  @SupportedAnnotationTypes("*")
  public static class ElementValueInspector extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      TypeElement element = processingEnv.getElementUtils().getTypeElement("T");
      for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
        processingEnv
            .getMessager()
            .printMessage(
                Diagnostic.Kind.NOTE,
                String.format("@Deprecated(%s)", annotationMirror.getElementValues()),
                element,
                annotationMirror);
      }
      return false;
    }
  }

  @Test
  public void badElementValue() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "@Deprecated(noSuch = 42) class T {}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        ImmutableList.of(new ElementValueInspector()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(
            e.diagnostics().stream()
                .filter(d -> d.severity().equals(Diagnostic.Kind.ERROR))
                .map(d -> d.message()))
        .containsExactly("could not resolve element noSuch() in java.lang.Deprecated");
    assertThat(
            e.diagnostics().stream()
                .filter(d -> d.severity().equals(Diagnostic.Kind.NOTE))
                .map(d -> d.message()))
        .containsExactly("@Deprecated({})");
  }

  @SupportedAnnotationTypes("*")
  public static class RecordProcessor extends AbstractProcessor {
    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      for (Element e : roundEnv.getRootElements()) {
        processingEnv
            .getMessager()
            .printMessage(
                Diagnostic.Kind.ERROR,
                e.getKind() + " " + e + " " + ((TypeElement) e).getSuperclass());
        for (Element m : e.getEnclosedElements()) {
          processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m.getKind() + " " + m);
        }
      }
      return false;
    }
  }

  @Test
  public void recordProcessing() throws IOException {
    assumeTrue(Runtime.version().feature() >= 15);
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== R.java ===", //
            "record R<T>(@Deprecated T x, int... y) {}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        ImmutableList.of(new RecordProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(
            e.diagnostics().stream()
                .filter(d -> d.severity().equals(Diagnostic.Kind.ERROR))
                .map(d -> d.message()))
        .containsExactly(
            "RECORD R java.lang.Record",
            "RECORD_COMPONENT x",
            "RECORD_COMPONENT y",
            "CONSTRUCTOR R(T,int[])",
            "METHOD toString()",
            "METHOD hashCode()",
            "METHOD equals(java.lang.Object)",
            "METHOD x()",
            "METHOD y()");
  }

  @Test
  public void missingElementValue() {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "import java.lang.annotation.Retention;",
            "@Retention() @interface T {}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        // missing annotation arguments are not a recoverable error, annotation
                        // processing shouldn't happen
                        ImmutableList.of(new CrashingProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(e.diagnostics().stream().map(d -> d.message()))
        .containsExactly("missing required annotation argument: value");
  }

  private static ImmutableList<Tree.CompUnit> parseUnit(String... lines) {
    return IntegrationTestSupport.TestInput.parse(Joiner.on('\n').join(lines))
        .sources
        .entrySet()
        .stream()
        .map(e -> new SourceFile(e.getKey(), e.getValue()))
        .map(Parser::parse)
        .collect(toImmutableList());
  }

  @SupportedAnnotationTypes("*")
  public static class AllMethodsProcessor extends AbstractProcessor {
    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

      ImmutableList<ExecutableElement> methods =
          typesIn(roundEnv.getRootElements()).stream()
              .flatMap(t -> methodsIn(t.getEnclosedElements()).stream())
              .collect(toImmutableList());
      for (ExecutableElement a : methods) {
        for (ExecutableElement b : methods) {
          if (a.equals(b)) {
            continue;
          }
          ExecutableType ta = (ExecutableType) a.asType();
          ExecutableType tb = (ExecutableType) b.asType();
          boolean r = processingEnv.getTypeUtils().isSubsignature(ta, tb);
          processingEnv
              .getMessager()
              .printMessage(
                  Diagnostic.Kind.ERROR,
                  String.format(
                      "%s#%s%s <: %s#%s%s ? %s",
                      a.getEnclosingElement(),
                      a.getSimpleName(),
                      ta,
                      b.getEnclosingElement(),
                      b.getSimpleName(),
                      tb,
                      r));
        }
      }
      return false;
    }
  }

  @Test
  public void bound() {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== A.java ===", //
            "import java.util.List;",
            "class A<T> {",
            "  <U extends T> U f(List<U> list) {",
            "    return list.get(0);",
            "  }",
            "}",
            "class B extends A<String> {",
            "  @Override",
            "  <U extends String> U f(List<U> list) {",
            "    return super.f(list);",
            "  }",
            "}",
            "class C extends A<Object> {",
            "  @Override",
            "  <U> U f(List<U> list) {",
            "    return super.f(list);",
            "  }",
            "}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        ImmutableList.of(new AllMethodsProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(e.diagnostics().stream().map(d -> d.message()))
        .containsExactly(
            "A#f<U>(java.util.List<U>)U <: B#f<U>(java.util.List<U>)U ? false",
            "A#f<U>(java.util.List<U>)U <: C#f<U>(java.util.List<U>)U ? false",
            "B#f<U>(java.util.List<U>)U <: A#f<U>(java.util.List<U>)U ? false",
            "B#f<U>(java.util.List<U>)U <: C#f<U>(java.util.List<U>)U ? false",
            "C#f<U>(java.util.List<U>)U <: A#f<U>(java.util.List<U>)U ? false",
            "C#f<U>(java.util.List<U>)U <: B#f<U>(java.util.List<U>)U ? false");
  }

  @SupportedAnnotationTypes("*")
  public static class URIProcessor extends AbstractProcessor {
    @Override
    public SourceVersion getSupportedSourceVersion() {
      return SourceVersion.latestSupported();
    }

    private boolean first = true;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      if (!first) {
        return false;
      }
      first = false;
      try {
        FileObject output =
            processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "foo", "Bar");
        Path path = Paths.get(output.toUri());
        processingEnv
            .getMessager()
            .printMessage(Diagnostic.Kind.ERROR, output.toUri() + " - " + path);
      } catch (IOException e) {
        throw new UncheckedIOException(e);
      }
      return false;
    }
  }

  @Test
  public void uriProcessing() throws IOException {
    ImmutableList<Tree.CompUnit> units =
        parseUnit(
            "=== T.java ===", //
            "class T {}");
    TurbineError e =
        assertThrows(
            TurbineError.class,
            () ->
                Binder.bind(
                    units,
                    ClassPathBinder.bindClasspath(ImmutableList.of()),
                    ProcessorInfo.create(
                        ImmutableList.of(new URIProcessor()),
                        getClass().getClassLoader(),
                        ImmutableMap.of(),
                        SourceVersion.latestSupported()),
                    TestClassPaths.TURBINE_BOOTCLASSPATH,
                    Optional.empty()));
    assertThat(
            e.diagnostics().stream()
                .filter(d -> d.severity().equals(Diagnostic.Kind.ERROR))
                .map(d -> d.message()))
        .containsExactly("file:///foo/Bar - " + Paths.get(URI.create("file:///foo/Bar")));
  }
}
