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

import static com.google.common.truth.Truth.assertThat;

import com.google.testing.compile.JavaFileObjects;
import java.util.List;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for {@link AnnotatedApiUsageChecker} via the {@link BetaChecker} concrete class.
 *
 * @author Colin Decker
 */

@RunWith(JUnit4.class)
public class BetaCheckerTest {

  private final TestCompiler compiler = new TestCompiler(BetaChecker.class);

  /**
   * Equivalent to the real @Beta annotation from Guava.
   */
  public static final JavaFileObject BETA = JavaFileObjects.forSourceLines(
      "com.google.common.annotations.Beta",
      "package com.google.common.annotations;",
      "",
      "import java.lang.annotation.ElementType;",
      "import java.lang.annotation.Retention;",
      "import java.lang.annotation.RetentionPolicy;",
      "import java.lang.annotation.Target;",
      "",
      "@Retention(RetentionPolicy.CLASS)",
      "@Target({",
      "    ElementType.ANNOTATION_TYPE,",
      "    ElementType.CONSTRUCTOR,",
      "    ElementType.FIELD,",
      "    ElementType.METHOD,",
      "    ElementType.TYPE})",
      "public @interface Beta {}");

  /**
   * Class in a com.google.common package that is annotated with @Beta.
   */
  public static final JavaFileObject ANNOTATED_CLASS = JavaFileObjects.forSourceLines(
      "com.google.common.foo.AnnotatedClass",
      "package com.google.common.foo;",
      "",
      "import com.google.common.annotations.Beta;",
      "",
      "@Beta",
      "public class AnnotatedClass {",
      "",
      "  public static final String STATIC_FIELD = \"foo\";",
      "",
      "  public static String staticMethod() {",
      "    return \"foo\";",
      "  }",
      "",
      " public final String instanceField = \"foo\";",
      "",
      "  public String instanceMethod() {",
      "    return \"foo\";",
      "  }",
      "}");

  @Test
  public void testCleanClass() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(BETA,
        JavaFileObjects.forSourceLines(
            "example.Test",
            "package example;",
            "",
            "import java.util.Arrays;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(Arrays.asList(args));",
            "  }",
            "}")
    );

    assertThat(diagnostics).isEmpty();
  }

  @Test
  public void testAnnotatedClass_asParameter() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines(
            "example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedClass annotated) {", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 6);
  }

  @Test
  public void testAnnotatedClass_asTypeArgument() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines(
            "example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "import java.util.List;",
            "",
            "public class Test {",
            "  public static void foo(List<AnnotatedClass> stuff) {", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 8);
  }

  @Test
  public void testAnnotatedClass_extending() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines(
            "example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test extends AnnotatedClass {", // error
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 5);
  }

  @Test
  public void testAnnotatedClass_extending_generatedSuperCallIsNotMatched() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines(
            "example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test extends AnnotatedClass {", // error
            "",
            "  private final String foo;",
            "",
            "  public Test(String foo) {", // no error for implicit super() here
            "    this.foo = foo;",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 5);
  }

  @Test
  public void testAnnotatedClass_asBoundInGenericType() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test<T extends AnnotatedClass> {", // error
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 5);
  }

  @Test
  public void testAnnotatedClass_asTypeArgInImplementedGenericInterface() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "import java.util.List;",
            "",
            "public abstract class Test implements List<AnnotatedClass> {", // error
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedClass_asBoundInGenericMethod() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static <T extends AnnotatedClass> T foo(T t) {", // error
            "    return null;",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 6);
  }

  @Test
  public void testAnnotatedClass_asBoundInGenericParameter() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "import java.util.List;",
            "",
            "public class Test {",
            "  public static void foo(List<? super AnnotatedClass> list) {", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 8);
  }

  @Test
  public void testAnnotatedClass_instantiation() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(new AnnotatedClass());", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedClass_staticMethodCall() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    AnnotatedClass.staticMethod();", // 2 errors
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7, 7);
  }

  @Test
  public void testAnnotatedClass_instanceMethodCall() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedClass a) {", // error
            "    a.instanceMethod();", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 6, 7);
  }

  @Test
  public void testAnnotatedClass_staticMethodCall_fromStaticImport() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static com.google.common.foo.AnnotatedClass.staticMethod;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    staticMethod();", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedClass_staticMethodCall_fromInstanceVariable() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedClass a) {", // error
            "    a.staticMethod();", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 6, 7);
  }

  @Test
  public void testAnnotatedClass_fullyQualifiedReferences() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "public class Test {",
            "  public static void foo(com.google.common.foo.AnnotatedClass parameter) {", // error
            "    com.google.common.foo.AnnotatedClass c = ", // error
            "        new com.google.common.foo.AnnotatedClass();", // error
            "    String s = com.google.common.foo.AnnotatedClass.staticMethod();", // 2 errors
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 4, 5, 6, 7, 7);
  }

  @Test
  public void testAnnotatedClass_importsAreNotMatched() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static com.google.common.foo.AnnotatedClass.staticMethod;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "",
            "public class Test {",
            "  public static void foo() {",
            "    AnnotatedClass c = new AnnotatedClass();", // 2 errors
            "    String s = staticMethod();", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 9, 9, 10);
  }

  @Test
  public void testAnnotatedClass_methodReference() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static java.util.stream.Collectors.joining;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "import java.util.List;",
            "",
            "public class Test {",
            "  public static void foo(List<? extends AnnotatedClass> list) {", // error
            "    String s = list.stream()",
            "        .map(AnnotatedClass::instanceMethod)", // error
            "        .collect(joining(", "));",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 9, 11);
  }

  @Test
  public void testAnnotatedClass_constructorReference() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_CLASS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedClass;",
            "import java.util.Optional;",
            "",
            "public class Test {",
            "  public static void foo(Optional<AnnotatedClass> optional) {", // error
            "    String s = optional",
            "        .orElseGet(AnnotatedClass::new)", // error
            "        .toString();",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7, 9);
  }

  /**
   * A class in a com.google.common package with some members that are annotated @Beta and some
   * that aren't.
   */
  public static final JavaFileObject ANNOTATED_MEMBERS = JavaFileObjects.forSourceLines(
      "com.google.common.foo.AnnotatedMembers",
      "package com.google.common.foo;",
      "",
      "import com.google.common.annotations.Beta;",
      "",
      "public class AnnotatedMembers {",
      "  public static final String STATIC_FIELD = \"foo\";",
      "",
      "  @Beta",
      "  public static final String ANNOTATED_STATIC_FIELD = \"foo\";",
      "",
      "  public final String instanceField = \"foo\";",
      "",
      "  @Beta",
      "  public final String annotatedInstanceField = \"foo\";",
      "",
      "  @Beta",
      "  public AnnotatedMembers() {}",
      "",
      "  public static String staticMethod() {",
      "    return \"foo\";",
      "  }",
      "",
      "  @Beta",
      "  public static String annotatedStaticMethod() {",
      "    return \"foo\";",
      "  }",
      "",
      "  public String instanceMethod() {",
      "    return \"foo\";",
      "  }",
      "",
      "  @Beta",
      "  public String annotatedInstanceMethod() {",
      "    return \"foo\";",
      "  }",
      "}");

  @Test
  public void testNonAnnotatedMembers() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedMembers instance) {",
            "    String a = AnnotatedMembers.staticMethod();",
            "    String b = instance.instanceMethod();",
            "    String c = AnnotatedMembers.STATIC_FIELD;",
            "    String d = instance.instanceField;",
            "    String e = instance.staticMethod();",
            "    String f = instance.STATIC_FIELD;",
            "  }",
            "}")
    );

    assertThat(diagnostics).isEmpty();
  }

  @Test
  public void testAnnotatedMembers_staticField() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(AnnotatedMembers.ANNOTATED_STATIC_FIELD);", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_instanceField() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedMembers instance) {",
            "    System.out.println(instance.annotatedInstanceField);", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_staticMethod() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(AnnotatedMembers.annotatedStaticMethod());", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_instanceMethod() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "",
            "public class Test {",
            "  public static void foo(AnnotatedMembers instance) {",
            "    System.out.println(instance.annotatedInstanceMethod());", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_staticField_fromStaticImport() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static com.google.common.foo.AnnotatedMembers.ANNOTATED_STATIC_FIELD;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(ANNOTATED_STATIC_FIELD);", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_staticMethod_fromStaticImport() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static com.google.common.foo.AnnotatedMembers.annotatedStaticMethod;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(annotatedStaticMethod());", // error
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 7);
  }

  @Test
  public void testAnnotatedMembers_methodReference() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import static java.util.stream.Collectors.joining;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "import java.util.List;",
            "",
            "public class Test {",
            "  public static void foo(List<? extends AnnotatedMembers> list) {",
            "    String s = list.stream()",
            "        .map(AnnotatedMembers::annotatedInstanceMethod)", // error
            "        .collect(joining(", "));",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 11);
  }

  @Test
  public void testAnnotatedMembers_constructorReference() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_MEMBERS,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedMembers;",
            "import java.util.Optional;",
            "",
            "public class Test {",
            "  public static void foo(Optional<AnnotatedMembers> optional) {",
            "    String s = optional",
            "        .orElseGet(AnnotatedMembers::new)", // error
            "        .toString();",
            "  }",
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 9);
  }

  /**
   * An @Beta interface in a com.google.common package.
   */
  private static final JavaFileObject ANNOTATED_INTERFACE = JavaFileObjects.forSourceLines(
      "com.google.common.foo.AnnotatedInterface",
      "package com.google.common.foo;",
      "",
      "import com.google.common.annotations.Beta;",
      "",
      "@Beta",
      "public interface AnnotatedInterface {",
      "}");

  @Test
  public void testAnnotatedInterface_implementing() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_INTERFACE,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedInterface;",
            "",
            "public class Test implements AnnotatedInterface {", // error
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 5);
  }

  @Test
  public void testAnnotatedInterface_extending() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, ANNOTATED_INTERFACE,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.foo.AnnotatedInterface;",
            "",
            "public interface Test extends AnnotatedInterface {", // error
            "}")
    );

    compiler.assertErrorsOnLines("example/Test.java", diagnostics, 5);
  }

  /**
   * These checkers only match usages in packages matching a specific regex, to avoid giving errors
   * on code with the annotations in other libraries or even in the user's code. The @Beta checker
   * should only match usages in packages under com.google.common.
   */
  private static final JavaFileObject CLASS_IN_OTHER_PACKAGE = JavaFileObjects.forSourceLines(
      "foo.ClassInOtherPackage",
      "package foo;",
      "",
      "import com.google.common.annotations.Beta;",
      "",
      "@Beta",
      "public class ClassInOtherPackage {}");

  @Test
  public void testUsageByPackage_nonMatchingPackage_doesNotMatch() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, CLASS_IN_OTHER_PACKAGE,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import foo.ClassInOtherPackage;",
            "",
            "public class Test {",
            "  public static void main(String[] args) {",
            "    System.out.println(new ClassInOtherPackage());",
            "  }",
            "}")
    );

    assertThat(diagnostics).isEmpty();
  }

  private static final JavaFileObject IGNORED_TYPE = JavaFileObjects.forSourceLines(
      "com.google.common.cache.Cache",
      "package com.google.common.cache;",
      "",
      "import com.google.common.annotations.Beta;",
      "",
      "@Beta",
      "public interface Cache<K, V> {",
      "  V get(K key);",
      "}");

  @Test
  public void testUsage_ignoredType() {
    List<Diagnostic<? extends JavaFileObject>> diagnostics = compiler.compile(
        BETA, IGNORED_TYPE,
        JavaFileObjects.forSourceLines("example.Test",
            "package example;",
            "",
            "import com.google.common.cache.Cache;",
            "",
            "public class Test {",
            "  public static void test(Cache<String, String> cache) {",
            "    System.out.println(cache.get(\"hello\"));",
            "  }",
            "}")
    );

    assertThat(diagnostics).isEmpty();
  }
}