/*
 * Copyright 2014 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.auto.value.processor;

import static com.google.common.truth.Truth.assertAbout;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.testing.compile.JavaFileObjects;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.tools.JavaFileObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests to ensure annotations are kept on AutoValue generated classes
 *
 * @author jmcampanini
 */
@RunWith(JUnit4.class)
public class PropertyAnnotationsTest {
  private static final String TEST_ANNOTATION =
      "@PropertyAnnotationsTest.TestAnnotation";
  private static final String TEST_ARRAY_ANNOTATION =
      "@PropertyAnnotationsTest.TestArrayAnnotation";

  public enum TestEnum {
    A,
    B;

    @Override
    public String toString() {
      // used to prove that the method we determine the value does not use the `toString()` method
      // of the enum
      return "not the same value";
    }
  }

  public @interface TestAnnotation {
    byte testByte() default 1;

    short testShort() default 2;

    int testInt() default 3;

    long testLong() default 4L;

    float testFloat() default 5.6f;

    double testDouble() default 7.8d;

    char testChar() default 'a';

    String testString() default "10";

    boolean testBoolean() default false;

    Class<?> testClass() default TestEnum.class;

    TestEnum testEnum() default TestEnum.A;

    OtherAnnotation testAnnotation() default @OtherAnnotation(foo = 23, bar = "baz");
  }

  public @interface OtherAnnotation {
    int foo() default 123;

    String bar() default "bar";
  }

  public @interface TestArrayAnnotation {
    byte[] testBytes() default {1, 2};

    short[] testShorts() default {3, 4};

    int[] testInts() default {5, 6};

    long[] testLongs() default {7L, 8L};

    float[] testFloats() default {9.1f, 2.3f};

    double[] testDoubles() default {4.5d, 6.7d};

    char[] testChars() default {'a', 'b'};

    String[] testStrings() default {"cde", "fgh"};

    boolean[] testBooleans() default {true, false};

    Class<?>[] testClasses() default {TestEnum.class, TestEnum.class};

    TestEnum[] testEnums() default {TestEnum.A, TestEnum.B};

    OtherAnnotation[] testAnnotations() default {
      @OtherAnnotation(foo = 999), @OtherAnnotation(bar = "baz")
    };
  }

  @Inherited
  public @interface InheritedAnnotation {}

  private static class InputFileBuilder {
    Iterable<String> imports = ImmutableList.of();
    List<String> annotations = new ArrayList<>();
    List<String> innerTypes = new ArrayList<>();

    InputFileBuilder setImports(Iterable<String> imports) {
      this.imports = imports;
      return this;
    }

    InputFileBuilder addAnnotations(String... annotations) {
      this.annotations.addAll(ImmutableList.copyOf(annotations));
      return this;
    }

    InputFileBuilder addAnnotations(Iterable<String> annotations) {
      this.annotations.addAll(ImmutableList.copyOf(annotations));
      return this;
    }

    InputFileBuilder addInnerTypes(String... innerTypes) {
      this.innerTypes.addAll(ImmutableList.copyOf(innerTypes));
      return this;
    }

    JavaFileObject build() {
      ImmutableList<String> list =
          ImmutableList.<String>builder()
              .add("package foo.bar;", "", "import com.google.auto.value.AutoValue;")
              .addAll(imports)
              .add("", "@AutoValue", "public abstract class Baz {")
              .addAll(annotations)
              .add(
                  "  public abstract int buh();",
                  "",
                  "  public static Baz create(int buh) {",
                  "    return new AutoValue_Baz(buh);",
                  "  }")
              .addAll(innerTypes)
              .add("}")
              .build();
      String[] lines = list.toArray(new String[list.size()]);
      return JavaFileObjects.forSourceLines("foo.bar.Baz", lines);
    }
  }

  private static class OutputFileBuilder {
    Iterable<String> imports = ImmutableList.of();
    List<String> fieldAnnotations = new ArrayList<>();
    List<String> methodAnnotations = new ArrayList<>();

    OutputFileBuilder setImports(Iterable<String> imports) {
      this.imports = imports;
      return this;
    }

    OutputFileBuilder addFieldAnnotations(String... annotations) {
      this.fieldAnnotations.addAll(ImmutableList.copyOf(annotations));
      return this;
    }

    OutputFileBuilder addMethodAnnotations(String... innerTypes) {
      this.methodAnnotations.addAll(ImmutableList.copyOf(innerTypes));
      return this;
    }

    OutputFileBuilder addMethodAnnotations(Iterable<String> innerTypes) {
      this.methodAnnotations.addAll(ImmutableList.copyOf(innerTypes));
      return this;
    }

    JavaFileObject build() {
      String nullable = methodAnnotations.contains("@Nullable") ? "@Nullable " : "";
      ImmutableSortedSet<String> allImports =
          ImmutableSortedSet.<String>naturalOrder()
              .add(GeneratedImport.importGeneratedAnnotationType())
              .addAll(imports)
              .build();
      ImmutableList<String> list =
          ImmutableList.<String>builder()
              .add("package foo.bar;", "")
              .addAll(allImports)
              .add(
                  "",
                  "@Generated(\"" + AutoValueProcessor.class.getName() + "\")",
                  "final class AutoValue_Baz extends Baz {")
              .addAll(fieldAnnotations)
              .add(
                  "  private final int buh;",
                  "  AutoValue_Baz(" + nullable + "int buh) {",
                  "    this.buh = buh;",
                  "  }",
                  "")
              .addAll(methodAnnotations)
              .add(
                  "  @Override public int buh() {",
                  "    return buh;",
                  "  }",
                  "",
                  "  @Override public String toString() {",
                  "    return \"Baz{\"",
                  "        + \"buh=\" + buh",
                  "        + \"}\";",
                  "  }",
                  "",
                  "  @Override public boolean equals(Object o) {",
                  "    if (o == this) {",
                  "      return true;",
                  "    }",
                  "    if (o instanceof Baz) {",
                  "      Baz that = (Baz) o;",
                  "      return this.buh == that.buh();",
                  "    }",
                  "    return false;",
                  "  }",
                  "",
                  "  @Override public int hashCode() {",
                  "    int h$ = 1;",
                  "    h$ *= 1000003;",
                  "    h$ ^= buh;",
                  "    return h$;",
                  "  }",
                  "}")
              .build();

      String[] lines = list.toArray(new String[list.size()]);
      return JavaFileObjects.forSourceLines("foo.bar.AutoValue_Baz", lines);
    }
  }

  private ImmutableSet<String> getImports(Class<?>... classes) {
    ImmutableSet.Builder<String> stringBuilder = ImmutableSortedSet.naturalOrder();
    for (Class<?> clazz : classes) {
      stringBuilder.add("import " + clazz.getName() + ";");
    }
    return stringBuilder.build();
  }

  private void assertGeneratedMatches(
      Iterable<String> imports,
      Iterable<String> annotations,
      Iterable<String> expectedMethodAnnotations) {

    JavaFileObject javaFileObject =
        new InputFileBuilder().setImports(imports).addAnnotations(annotations).build();

    JavaFileObject expectedOutput =
        new OutputFileBuilder()
            .setImports(imports)
            .addMethodAnnotations(expectedMethodAnnotations)
            .build();

    assertAbout(javaSource())
        .that(javaFileObject)
        .processedWith(new AutoValueProcessor())
        .compilesWithoutError()
        .and()
        .generatesSources(expectedOutput);
  }

  @Test
  public void testSimpleAnnotation() {
    assertGeneratedMatches(
        ImmutableList.of(), ImmutableList.of("@Deprecated"), ImmutableList.of("@Deprecated"));
  }

  @Test
  public void testSingleStringValueAnnotation() {
    assertGeneratedMatches(
        ImmutableList.of(),
        ImmutableList.of("@SuppressWarnings(\"a\")"),
        ImmutableList.of("@SuppressWarnings(\"a\")"));
  }

  @Test
  public void testMultiStringValueAnnotation() {
    assertGeneratedMatches(
        ImmutableList.of(),
        ImmutableList.of("@SuppressWarnings({\"a\", \"b\"})"),
        ImmutableList.of("@SuppressWarnings({\"a\", \"b\"})"));
  }

  @Test
  public void testNumberValueAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ANNOTATION + "(testShort = 1, testInt = 2, testLong = 3L)"),
        ImmutableList.of(TEST_ANNOTATION + "(testShort = 1, testInt = 2, testLong = 3L)"));
  }

  @Test
  public void testByteValueAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ANNOTATION + "(testByte = 0)"),
        ImmutableList.of(TEST_ANNOTATION + "(testByte = 0)"));
  }

  @Test
  public void testDecimalValueAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ANNOTATION + "(testDouble = 1.2d, testFloat = 3.4f)"),
        ImmutableList.of(TEST_ANNOTATION + "(testDouble = 1.2d, testFloat = 3.4f)"));
  }

  @Test
  public void testOtherValuesAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ANNOTATION + "(testBoolean = true, testString = \"hallo\", testChar = 'a')"),
        ImmutableList.of(
            TEST_ANNOTATION + "(testBoolean = true, testString = \"hallo\", testChar = 'a')"));
  }

  @Test
  public void testClassAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ANNOTATION + "(testClass = String.class)"),
        ImmutableList.of(TEST_ANNOTATION + "(testClass = String.class)"));
  }

  @Test
  public void testEnumAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ANNOTATION
                + "(testEnum = "
                + PropertyAnnotationsTest.class.getName()
                + ".TestEnum.A)"),
        ImmutableList.of(TEST_ANNOTATION + "(testEnum = PropertyAnnotationsTest.TestEnum.A)"));
  }

  @Test
  public void testEmptyAnnotationAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ANNOTATION + "(testAnnotation = @PropertyAnnotationsTest.OtherAnnotation)"),
        ImmutableList.of(
            TEST_ANNOTATION + "(testAnnotation = @PropertyAnnotationsTest.OtherAnnotation)"));
  }

  @Test
  public void testValuedAnnotationAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ANNOTATION
                + "(testAnnotation = @PropertyAnnotationsTest.OtherAnnotation(foo=999))"),
        ImmutableList.of(
            TEST_ANNOTATION
                + "(testAnnotation = @PropertyAnnotationsTest.OtherAnnotation(foo=999))"));
  }

  @Test
  public void testNumberArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testShorts = {2, 3}, testInts = {4, 5}, testLongs = {6L, 7L})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testShorts = {2, 3}, testInts = {4, 5}, testLongs = {6L, 7L})"));
  }

  @Test
  public void testByteArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ARRAY_ANNOTATION + "(testBytes = {0, 1})"),
        ImmutableList.of(TEST_ARRAY_ANNOTATION + "(testBytes = {0, 1})"));
  }

  @Test
  public void testDecimalArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION + "(testDoubles = {1.2d, 3.4d}, testFloats = {5.6f, 7.8f})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION + "(testDoubles = {1.2d, 3.4d}, testFloats = {5.6f, 7.8f})"));
  }

  @Test
  public void testOtherArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testBooleans = {false, false},"
                + " testStrings = {\"aaa\", \"bbb\"}, testChars={'x', 'y'})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testBooleans = {false, false},"
                + " testStrings = {\"aaa\", \"bbb\"}, testChars={'x', 'y'})"));
  }

  @Test
  public void testClassArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(TEST_ARRAY_ANNOTATION + "(testClasses = {String.class, Long.class})"),
        ImmutableList.of(TEST_ARRAY_ANNOTATION + "(testClasses = {String.class, Long.class})"));
  }

  @Test
  public void testImportedClassArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class, Nullable.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testClasses = {javax.annotation.Nullable.class, Long.class})"),
        ImmutableList.of(TEST_ARRAY_ANNOTATION + "(testClasses = {Nullable.class, Long.class})"));
  }

  @Test
  public void testEnumArrayAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION + "(testEnums = {PropertyAnnotationsTest.TestEnum.A,"
                + " PropertyAnnotationsTest.TestEnum.B})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION + "(testEnums = {PropertyAnnotationsTest.TestEnum.A,"
                + " PropertyAnnotationsTest.TestEnum.B})"));
  }

  @Test
  public void testArrayOfEmptyAnnotationAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = {@PropertyAnnotationsTest.OtherAnnotation})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = @PropertyAnnotationsTest.OtherAnnotation)"));
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = {@PropertyAnnotationsTest.OtherAnnotation,"
                + " @PropertyAnnotationsTest.OtherAnnotation})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = {@PropertyAnnotationsTest.OtherAnnotation,"
                + " @PropertyAnnotationsTest.OtherAnnotation})"));
  }

  @Test
  public void testArrayOfValuedAnnotationAnnotation() {
    assertGeneratedMatches(
        getImports(PropertyAnnotationsTest.class),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = {@PropertyAnnotationsTest.OtherAnnotation(foo = 999)})"),
        ImmutableList.of(
            TEST_ARRAY_ANNOTATION
                + "(testAnnotations = @PropertyAnnotationsTest.OtherAnnotation(foo = 999))"));
  }

  /**
   * Tests that when CopyAnnotations is present on a method, all non-inherited annotations (except
   * those appearing in CopyAnnotations.exclude) are copied to the method implementation in the
   * generated class.
   */
  @Test
  public void testCopyingMethodAnnotations() {
    ImmutableSet<String> imports = getImports(PropertyAnnotationsTest.class);
    JavaFileObject inputFile =
        new InputFileBuilder()
            .setImports(imports)
            .addAnnotations(
                "@AutoValue.CopyAnnotations(exclude="
                    + "{PropertyAnnotationsTest.TestAnnotation.class})",
                "@Deprecated",
                "@PropertyAnnotationsTest.TestAnnotation",
                "@PropertyAnnotationsTest.InheritedAnnotation")
            .build();

    JavaFileObject outputFile =
        new OutputFileBuilder()
            .setImports(imports)
            .addMethodAnnotations("@Deprecated", "@PropertyAnnotationsTest.InheritedAnnotation")
            .addFieldAnnotations("@Deprecated", "@PropertyAnnotationsTest.InheritedAnnotation")
            .build();

    assertAbout(javaSource())
        .that(inputFile)
        .processedWith(new AutoValueProcessor())
        .compilesWithoutError()
        .and()
        .generatesSources(outputFile);
  }

  /**
   * Tests that when CopyAnnotationsToGeneratedField is present on a method, all non-inherited
   * annotations (except those appearing in CopyAnnotationsToGeneratedField.exclude) are copied to
   * the method implementation in the generated class.
   */
  @Test
  public void testCopyingMethodAnnotationsToGeneratedFields() {
    JavaFileObject inputFile =
        new InputFileBuilder()
            .setImports(getImports(PropertyAnnotationsTest.class, Target.class, ElementType.class))
            .addAnnotations(
                "@AutoValue.CopyAnnotations(exclude={PropertyAnnotationsTest."
                    + "TestAnnotation.class})",
                "@Deprecated",
                "@PropertyAnnotationsTest.TestAnnotation",
                "@PropertyAnnotationsTest.InheritedAnnotation",
                "@MethodsOnly")
            .addInnerTypes("@Target(ElementType.METHOD) @interface MethodsOnly {}")
            .build();

    JavaFileObject outputFile =
        new OutputFileBuilder()
            .setImports(getImports(PropertyAnnotationsTest.class))
            .addFieldAnnotations("@Deprecated", "@PropertyAnnotationsTest.InheritedAnnotation")
            .addMethodAnnotations(
                "@Deprecated",
                "@PropertyAnnotationsTest.InheritedAnnotation",
                "@Baz.MethodsOnly")
            .build();

    assertAbout(javaSource())
        .that(inputFile)
        .processedWith(new AutoValueProcessor())
        .compilesWithoutError()
        .and()
        .generatesSources(outputFile);
  }
}
