/*
 * Copyright 2018 The Error Prone Authors.
 *
 * 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.errorprone.bugpatterns.apidiff;

import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.CLASS;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.errorprone.BaseErrorProneJavaCompiler;
import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.SeverityLevel;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.bugpatterns.apidiff.ApiDiff.ClassMemberKey;
import com.google.errorprone.bugpatterns.apidiff.ApiDiffProto.Diff;
import com.google.errorprone.bugpatterns.apidiff.CompilationBuilderHelpers.CompilationBuilder;
import com.google.errorprone.bugpatterns.apidiff.CompilationBuilderHelpers.CompilationResult;
import com.google.errorprone.bugpatterns.apidiff.CompilationBuilderHelpers.SourceBuilder;
import com.google.errorprone.scanner.ErrorProneScanner;
import com.google.errorprone.scanner.ScannerSupplier;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** {@link ApiDiffChecker}Test. */
@RunWith(JUnit4.class)
public class ApiDiffCheckerTest {

  @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
  private final JavacFileManager fileManager =
      new JavacFileManager(new Context(), false, StandardCharsets.UTF_8);

  @Retention(CLASS)
  @Target({ANNOTATION_TYPE, CONSTRUCTOR, FIELD, METHOD, TYPE})
  public @interface RequiresNewApiVersion {}

  private final CompilationTestHelper compilationHelper;

  /** An {@link ApiDiffChecker} for testing. */
  @BugPattern(name = "SampleChecker", severity = SeverityLevel.ERROR, summary = "")
  private static class SampleApiDiffChecker extends ApiDiffChecker {
    SampleApiDiffChecker(ApiDiff apiDiff) {
      super(apiDiff);
    }
  }

  /** An {@link ApiDiffChecker} that recognizes only {@link RequiresNewApiVersion} annotations. */
  @BugPattern(
      name = "AnnotationChecker",
      severity = SeverityLevel.ERROR,
      summary = "",
      suppressionAnnotations = {RequiresNewApiVersion.class, SuppressWarnings.class})
  public static class AnnotationOnlyApiDiffChecker extends ApiDiffChecker {
    public AnnotationOnlyApiDiffChecker() {
      super(ApiDiff.fromProto(Diff.getDefaultInstance()), RequiresNewApiVersion.class);
    }
  }

  public ApiDiffCheckerTest() {
    compilationHelper =
        CompilationTestHelper.newInstance(AnnotationOnlyApiDiffChecker.class, getClass());
  }

  @Test
  public void newDerivedMethod() throws Exception {
    ApiDiff diff =
        ApiDiff.fromMembers(
            Collections.emptySet(),
            ImmutableSetMultimap.of("lib/Derived", ClassMemberKey.create("g", "()V")));

    Path originalJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Base.java", //
                        "package lib;",
                        "public class Base {",
                        "  public void f() {}",
                        "}")
                    .addSourceLines(
                        "Derived.java", //
                        "package lib;",
                        "public class Derived extends Base {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    Path newJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setClasspath(Collections.singleton(originalJar))
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Derived.java", //
                        "package lib;",
                        "public class Derived extends Base {",
                        "  public void f() {}",
                        "  public void g() {}",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    BaseErrorProneJavaCompiler errorProneCompiler =
        new BaseErrorProneJavaCompiler(
            ScannerSupplier.fromScanner(new ErrorProneScanner(new SampleApiDiffChecker(diff))));

    CompilationResult result =
        new CompilationBuilder(errorProneCompiler, tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Test.java",
                        "import lib.*;", //
                        "class Test {",
                        "  void f() {",
                        "    new Derived().f();",
                        "    new Derived().g();",
                        "  }",
                        "}")
                    .build())
            .setClasspath(Arrays.asList(newJar, originalJar))
            .compile();

    assertThat(getOnlyElement(result.diagnostics()).getMessage(Locale.ENGLISH))
        .contains("g() is not available in lib.Derived");
  }

  @Test
  public void addedSuperAndMethod() throws Exception {
    ApiDiff diff =
        ApiDiff.fromMembers(
            ImmutableSet.of("lib/A"),
            ImmutableSetMultimap.of("lib/B", ClassMemberKey.create("f", "()V")));

    Path originalJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    Path newJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "interface A {",
                        "  default void f() {}",
                        "}")
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B implements A {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    BaseErrorProneJavaCompiler errorProneCompiler =
        new BaseErrorProneJavaCompiler(
            ScannerSupplier.fromScanner(new ErrorProneScanner(new SampleApiDiffChecker(diff))));

    CompilationResult result =
        new CompilationBuilder(errorProneCompiler, tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Test.java",
                        "import lib.*;", //
                        "class Test {",
                        "  void f(B b) {",
                        "    b.f();",
                        "  }",
                        "}")
                    .build())
            .setClasspath(Arrays.asList(newJar, originalJar))
            .compile();

    // This should be an error (the inherited A.f() is not backwards compatible), but we don't
    // detect newly added methods in newly added super types.
    assertThat(result.diagnostics()).isEmpty();
  }

  @Test
  public void movedMethodToNewSuper() throws Exception {
    ApiDiff diff = ApiDiff.fromMembers(ImmutableSet.of("lib/A"), ImmutableSetMultimap.of());

    Path originalJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B {",
                        "  public void f() {}",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    Path newJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "interface A {",
                        "  default void f() {}",
                        "}")
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B implements A {}")
                    .build())
            .compileOutputToJarOrDie();

    BaseErrorProneJavaCompiler errorProneCompiler =
        new BaseErrorProneJavaCompiler(
            ScannerSupplier.fromScanner(new ErrorProneScanner(new SampleApiDiffChecker(diff))));

    CompilationResult result =
        new CompilationBuilder(errorProneCompiler, tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Test.java",
                        "import lib.*;", //
                        "class Test {",
                        "  void f(B b) {",
                        "    b.f();",
                        "  }",
                        "}")
                    .build())
            .setClasspath(Arrays.asList(newJar, originalJar))
            .compile();

    assertThat(result.diagnostics()).isEmpty();
  }

  @Test
  public void movedToSuperMethodFromMiddle() throws Exception {
    ApiDiff diff =
        ApiDiff.fromMembers(
            ImmutableSet.of(), ImmutableSetMultimap.of("lib/A", ClassMemberKey.create("f", "()V")));

    Path originalJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "public class A {",
                        "}")
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B extends A {",
                        "  public void f() {}",
                        "}")
                    .addSourceLines(
                        "C.java", //
                        "package lib;",
                        "public class C extends B {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    Path newJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "public class A {",
                        "  public void f() {}",
                        "}")
                    .addSourceLines(
                        "B.java", //
                        "package lib;",
                        "public class B extends A {",
                        "}")
                    .addSourceLines(
                        "C.java", //
                        "package lib;",
                        "public class C extends B {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    BaseErrorProneJavaCompiler errorProneCompiler =
        new BaseErrorProneJavaCompiler(
            ScannerSupplier.fromScanner(new ErrorProneScanner(new SampleApiDiffChecker(diff))));

    CompilationResult result =
        new CompilationBuilder(errorProneCompiler, tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Test.java",
                        "import lib.*;", //
                        "class Test {",
                        "  void f(C c) {",
                        "    c.f();",
                        "  }",
                        "}")
                    .build())
            .setClasspath(Arrays.asList(newJar, originalJar))
            .compile();

    // This is actually OK, but we see it as a call to the newly-added A.f, and don't consider that
    // B.f is available in the old version of the API. It's not clear how to avoid this false
    // positive.
    assertThat(result.diagnostics()).hasSize(1);
    assertThat(getOnlyElement(result.diagnostics()).getMessage(Locale.ENGLISH))
        .contains("lib.A#f() is not available in lib.C");
  }

  @Test
  public void subType() throws Exception {
    ApiDiff diff =
        ApiDiff.fromMembers(
            ImmutableSet.of(), ImmutableSetMultimap.of("lib/A", ClassMemberKey.create("f", "()V")));

    Path originalJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "public class A {",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    Path newJar =
        new CompilationBuilder(JavacTool.create(), tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "A.java", //
                        "package lib;",
                        "public class A {",
                        "  public void f() {}",
                        "}")
                    .build())
            .compileOutputToJarOrDie();

    BaseErrorProneJavaCompiler errorProneCompiler =
        new BaseErrorProneJavaCompiler(
            ScannerSupplier.fromScanner(new ErrorProneScanner(new SampleApiDiffChecker(diff))));

    CompilationResult result =
        new CompilationBuilder(errorProneCompiler, tempFolder.newFolder(), fileManager)
            .setSources(
                new SourceBuilder(tempFolder.newFolder())
                    .addSourceLines(
                        "Test.java",
                        "import lib.A;", //
                        "class Test {",
                        "  void g() {",
                        "    new A() {}.f();",
                        "  }",
                        "}")
                    .build())
            .setClasspath(Arrays.asList(newJar, originalJar))
            .compile();

    assertThat(result.diagnostics()).hasSize(1);
    assertThat(getOnlyElement(result.diagnostics()).getMessage(Locale.ENGLISH))
        .contains("lib.A#f() is not available in <anonymous Test$1>");
  }

  @Test
  public void positiveAnnotatedClass() {
    compilationHelper
        .addSourceLines(
            "Lib.java",
            "package my.lib;",
            "import"
                + " com.google.errorprone.bugpatterns.apidiff.ApiDiffCheckerTest.RequiresNewApiVersion;",
            "@RequiresNewApiVersion",
            "public final class Lib {}")
        .addSourceLines(
            "Test.java",
            "import my.lib.Lib;",
            "class Test {",
            "  // BUG: Diagnostic contains: Lib",
            "  Lib l;",
            "}")
        .doTest();
  }

  @Test
  public void positiveAnnotatedClassSuppressedBySameAnnotation() {
    compilationHelper
        .addSourceLines(
            "Lib.java",
            "package my.lib;",
            "import"
                + " com.google.errorprone.bugpatterns.apidiff.ApiDiffCheckerTest.RequiresNewApiVersion;",
            "@RequiresNewApiVersion",
            "public final class Lib {}")
        .addSourceLines(
            "Test.java",
            "import"
                + " com.google.errorprone.bugpatterns.apidiff.ApiDiffCheckerTest.RequiresNewApiVersion;",
            "import my.lib.Lib;",
            "class Test {",
            "  @RequiresNewApiVersion",
            "  Lib l;",
            "}")
        .doTest();
  }

  @Test
  public void positiveAnnotatedMethod() {
    compilationHelper
        .addSourceLines(
            "Lib.java",
            "package my.lib;",
            "import"
                + " com.google.errorprone.bugpatterns.apidiff.ApiDiffCheckerTest.RequiresNewApiVersion;",
            "public final class Lib {",
            "  @RequiresNewApiVersion",
            "  public void foo() {}",
            "}")
        .addSourceLines(
            "Test.java",
            "import my.lib.Lib;",
            "class Test {",
            "  void bar() {",
            "    // BUG: Diagnostic contains: foo",
            "    new Lib().foo();",
            "  }",
            "}")
        .doTest();
  }

  @Test
  public void positiveAnnotatedField() {
    compilationHelper
        .addSourceLines(
            "Lib.java",
            "package my.lib;",
            "import"
                + " com.google.errorprone.bugpatterns.apidiff.ApiDiffCheckerTest.RequiresNewApiVersion;",
            "public final class Lib {",
            "  @RequiresNewApiVersion",
            "  public static final int FOO = 1;",
            "}")
        .addSourceLines(
            "Test.java",
            "import my.lib.Lib;",
            "class Test {",
            "  void bar() {",
            "    // BUG: Diagnostic contains: FOO",
            "    System.out.println(Lib.FOO);",
            "  }",
            "}")
        .doTest();
  }
}
