/*
 * 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.auto.value;

import static org.junit.Assert.assertEquals;

import com.google.auto.value.annotations.Empty;
import com.google.auto.value.annotations.GwtArrays;
import com.google.auto.value.annotations.StringValues;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.primitives.Ints;
import com.google.common.testing.EqualsTester;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * @author emcmanus@google.com (Éamonn McManus)
 */
@RunWith(JUnit4.class)
public class AutoAnnotationTest {
  @AutoAnnotation private static StringValues newStringValues(String[] value) {
    return new AutoAnnotation_AutoAnnotationTest_newStringValues(value);
  }

  @Empty
  @StringValues("oops")
  static class AnnotatedClass {}

  @Test
  public void testSimple() {
    StringValues expectedStringValues = AnnotatedClass.class.getAnnotation(StringValues.class);
    StringValues actualStringValues = newStringValues(new String[] {"oops"});
    StringValues otherStringValues = newStringValues(new String[] {});
    new EqualsTester()
        .addEqualityGroup(expectedStringValues, actualStringValues)
        .addEqualityGroup(otherStringValues)
        .testEquals();
  }

  @Test
  public void testArraysAreCloned() {
    String[] array = {"Jekyll"};
    StringValues stringValues = newStringValues(array);
    array[0] = "Hyde";
    assertEquals("Jekyll", stringValues.value()[0]);
    stringValues.value()[0] = "Hyde";
    assertEquals("Jekyll", stringValues.value()[0]);
  }

  @Test
  public void testGwtArraysAreCloned() {
    String[] strings = {"Jekyll"};
    int[] ints = {2, 3, 5};
    GwtArrays arrays = newGwtArrays(strings, ints);
    assertEquals(ImmutableList.of("Jekyll"), ImmutableList.copyOf(arrays.strings()));
    assertEquals(ImmutableList.of(2, 3, 5), Ints.asList(arrays.ints()));
    strings[0] = "Hyde";
    ints[0] = -1;
    assertEquals(ImmutableList.of("Jekyll"), ImmutableList.copyOf(arrays.strings()));
    assertEquals(ImmutableList.of(2, 3, 5), Ints.asList(arrays.ints()));
  }

  @AutoAnnotation private static GwtArrays newGwtArrays(String[] strings, int[] ints) {
    return new AutoAnnotation_AutoAnnotationTest_newGwtArrays(strings, ints);
  }

  @AutoAnnotation private static StringValues newStringValuesVarArgs(String... value) {
    return new AutoAnnotation_AutoAnnotationTest_newStringValuesVarArgs(value);
  }

  @Test
  public void testSimpleVarArgs() {
    StringValues expectedStringValues = AnnotatedClass.class.getAnnotation(StringValues.class);
    StringValues actualStringValues = newStringValuesVarArgs("oops");
    StringValues otherStringValues = newStringValuesVarArgs(new String[] {});
    new EqualsTester()
        .addEqualityGroup(expectedStringValues, actualStringValues)
        .addEqualityGroup(otherStringValues)
        .testEquals();
  }

  @AutoAnnotation private static Empty newEmpty() {
    return new AutoAnnotation_AutoAnnotationTest_newEmpty();
  }

  @Test
  public void testEmpty() {
    Empty expectedEmpty = AnnotatedClass.class.getAnnotation(Empty.class);
    Empty actualEmpty = newEmpty();
    new EqualsTester()
        .addEqualityGroup(expectedEmpty, actualEmpty)
        .testEquals();
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface Everything {
    byte aByte();
    short aShort();
    int anInt();
    long aLong();
    float aFloat();
    double aDouble();
    char aChar();
    boolean aBoolean();
    String aString();
    RetentionPolicy anEnum();
    StringValues anAnnotation();
    byte[] bytes();
    short[] shorts();
    int[] ints();
    long[] longs();
    float[] floats();
    double[] doubles();
    char[] chars();
    boolean[] booleans();
    String[] strings();
    RetentionPolicy[] enums();
    StringValues[] annotations();
  }

  @AutoAnnotation static Everything newEverything(
      byte aByte,
      short aShort,
      int anInt,
      long aLong,
      float aFloat,
      double aDouble,
      char aChar,
      boolean aBoolean,
      String aString,
      RetentionPolicy anEnum,
      StringValues anAnnotation,
      byte[] bytes,
      short[] shorts,
      int[] ints,
      long[] longs,
      float[] floats,
      double[] doubles,
      char[] chars,
      boolean[] booleans,
      String[] strings,
      RetentionPolicy[] enums,
      StringValues[] annotations) {
    return new AutoAnnotation_AutoAnnotationTest_newEverything(
        aByte, aShort, anInt, aLong, aFloat, aDouble, aChar, aBoolean, aString, anEnum,
        anAnnotation, bytes, shorts, ints, longs, floats, doubles, chars, booleans, strings, enums,
        annotations);
  }

  @AutoAnnotation static Everything newEverythingCollections(
      byte aByte,
      short aShort,
      int anInt,
      long aLong,
      float aFloat,
      double aDouble,
      char aChar,
      boolean aBoolean,
      String aString,
      RetentionPolicy anEnum,
      StringValues anAnnotation,
      Collection<Byte> bytes,
      List<Short> shorts,
      ArrayList<Integer> ints,
      Set<Long> longs,
      SortedSet<Float> floats,
      TreeSet<Double> doubles,
      LinkedHashSet<Character> chars,
      ImmutableCollection<Boolean> booleans,
      ImmutableList<String> strings,
      ImmutableSet<RetentionPolicy> enums,
      Set<StringValues> annotations) {
    return new AutoAnnotation_AutoAnnotationTest_newEverythingCollections(
        aByte, aShort, anInt, aLong, aFloat, aDouble, aChar, aBoolean, aString, anEnum,
        anAnnotation, bytes, shorts, ints, longs, floats, doubles, chars, booleans, strings, enums,
        annotations);
  }

  @Everything(
      aByte = 1,
      aShort = 2,
      anInt = 3,
      aLong = -4,
      aFloat = Float.NaN,
      aDouble = Double.NaN,
      aChar = '#',
      aBoolean = true,
      aString = "maybe\nmaybe not\n",
      anEnum = RetentionPolicy.RUNTIME,
      anAnnotation = @StringValues("whatever"),
      bytes = {5, 6},
      shorts = {},
      ints = {7},
      longs = {8, 9},
      floats = {10, 11},
      doubles = {Double.NEGATIVE_INFINITY, -12.0, Double.POSITIVE_INFINITY},
      chars = {'?', '!', '\n'},
      booleans = {false, true, false},
      strings = {"ver", "vers", "vert", "verre", "vair"},
      enums = {RetentionPolicy.CLASS, RetentionPolicy.RUNTIME},
      annotations = {
        @StringValues({}), @StringValues({"foo", "bar"})
      }
  )
  private static class AnnotatedWithEverything {}

  // Get an instance of @Everything via reflection on the class AnnotatedWithEverything,
  // fabricate an instance using newEverything that is supposed to be equal to it, and
  // fabricate another instance using newEverything that is supposed to be different.
  private static final Everything EVERYTHING_FROM_REFLECTION =
      AnnotatedWithEverything.class.getAnnotation(Everything.class);
  private static final Everything EVERYTHING_FROM_AUTO = newEverything(
      (byte) 1, (short) 2, 3, -4, Float.NaN, Double.NaN, '#', true,
      "maybe\nmaybe not\n",
      RetentionPolicy.RUNTIME,
      newStringValues(new String[] {"whatever"}),
      new byte[] {5, 6},
      new short[] {},
      new int[] {7},
      new long[] {8, 9},
      new float[] {10, 11},
      new double[] {Double.NEGATIVE_INFINITY, -12.0, Double.POSITIVE_INFINITY},
      new char[] {'?', '!', '\n'},
      new boolean[] {false, true, false},
      new String[] {"ver", "vers", "vert", "verre", "vair"},
      new RetentionPolicy[] {RetentionPolicy.CLASS, RetentionPolicy.RUNTIME},
      new StringValues[] {
        newStringValues(new String[] {}),
        newStringValues(new String[] {"foo", "bar"}),
      });
  private static final Everything EVERYTHING_FROM_AUTO_COLLECTIONS = newEverythingCollections(
      (byte) 1, (short) 2, 3, -4, Float.NaN, Double.NaN, '#', true,
      "maybe\nmaybe not\n",
      RetentionPolicy.RUNTIME,
      newStringValues(new String[] {"whatever"}),
      Arrays.asList((byte) 5, (byte) 6),
      Collections.<Short>emptyList(),
      new ArrayList<Integer>(Collections.singleton(7)),
      ImmutableSet.of(8L, 9L),
      ImmutableSortedSet.of(10f, 11f),
      new TreeSet<Double>(
          ImmutableList.of(Double.NEGATIVE_INFINITY, -12.0, Double.POSITIVE_INFINITY)),
      new LinkedHashSet<Character>(ImmutableList.of('?', '!', '\n')),
      ImmutableList.of(false, true, false),
      ImmutableList.of("ver", "vers", "vert", "verre", "vair"),
      ImmutableSet.of(RetentionPolicy.CLASS, RetentionPolicy.RUNTIME),
      ImmutableSet.of(
          newStringValues(new String[] {}),
          newStringValues(new String[] {"foo", "bar"})));
  private static final Everything EVERYTHING_ELSE_FROM_AUTO = newEverything(
      (byte) 0, (short) 0, 0, 0, 0, 0, '0', false, "", RetentionPolicy.SOURCE,
      newStringValues(new String[] {""}),
      new byte[0], new short[0], new int[0], new long[0], new float[0], new double[0],
      new char[0], new boolean[0], new String[0], new RetentionPolicy[0], new StringValues[0]);
  private static final Everything EVERYTHING_ELSE_FROM_AUTO_COLLECTIONS = newEverythingCollections(
      (byte) 0, (short) 0, 0, 0, 0, 0, '0', false, "", RetentionPolicy.SOURCE,
      newStringValues(new String[] {""}),
      ImmutableList.<Byte>of(),
      Collections.<Short>emptyList(),
      new ArrayList<Integer>(),
      Collections.<Long>emptySet(),
      ImmutableSortedSet.<Float>of(),
      new TreeSet<Double>(),
      new LinkedHashSet<Character>(),
      ImmutableSet.<Boolean>of(),
      ImmutableList.<String>of(),
      ImmutableSet.<RetentionPolicy>of(),
      Collections.<StringValues>emptySet());

  @Test
  public void testEqualsAndHashCode() {
    new EqualsTester()
        .addEqualityGroup(
            EVERYTHING_FROM_REFLECTION, EVERYTHING_FROM_AUTO, EVERYTHING_FROM_AUTO_COLLECTIONS)
        .addEqualityGroup(EVERYTHING_ELSE_FROM_AUTO, EVERYTHING_ELSE_FROM_AUTO_COLLECTIONS)
        .testEquals();
  }

  public static class IntList extends ArrayList<Integer> {
    IntList(Collection<Integer> c) {
      super(c);
    }
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface IntArray {
    int[] ints();
  }

  @IntArray(ints = {1, 2, 3})
  private static class AnnotatedWithIntArray {}

  @AutoAnnotation static IntArray newIntArray(IntList ints) {
    return new AutoAnnotation_AutoAnnotationTest_newIntArray(ints);
  }

  /**
   * Test that we can represent a primitive array member with a parameter whose type is a
   * collection of the corresponding wrapper type, even if the wrapper type is not explicitly a type
   * parameter. Specifically, if the member is an {@code int[]} then obviously we can represent it
   * as a {@code List<Integer>}, but here we test that we can also represent it as an {@code
   * IntList}, which is only a {@code List<Integer>} by virtue of inheritance. This is a separate
   * test rather than just putting an {@code IntList} parameter into {@link
   * #newEverythingCollections} because we want to check that we are still able to detect the
   * primitive wrapper type even though it's hidden in this way. We need to generate a helper method
   * for every primitive wrapper.
   */
  @Test
  public void testDerivedPrimitiveCollection() {
    IntList intList = new IntList(ImmutableList.of(1, 2, 3));
    IntArray actual = newIntArray(intList);
    IntArray expected = AnnotatedWithIntArray.class.getAnnotation(IntArray.class);
    assertEquals(expected, actual);
  }

  @Test
  public void testToString() {
    String expected = "@com.google.auto.value.AutoAnnotationTest.Everything("
        + "aByte=1, aShort=2, anInt=3, aLong=-4, aFloat=NaN, aDouble=NaN, aChar='#', "
        + "aBoolean=true, aString=\"maybe\\nmaybe not\\n\", anEnum=RUNTIME, "
        + "anAnnotation=@com.google.auto.value.annotations.StringValues([\"whatever\"]), "
        + "bytes=[5, 6], shorts=[], ints=[7], longs=[8, 9], floats=[10.0, 11.0], "
        + "doubles=[-Infinity, -12.0, Infinity], "
        + "chars=['?', '!', '\\n'], "
        + "booleans=[false, true, false], "
        + "strings=[\"ver\", \"vers\", \"vert\", \"verre\", \"vair\"], "
        + "enums=[CLASS, RUNTIME], "
        + "annotations=["
        + "@com.google.auto.value.annotations.StringValues([]), "
        + "@com.google.auto.value.annotations.StringValues([\"foo\", \"bar\"])"
        + "]"
        + ")";
    assertEquals(expected, EVERYTHING_FROM_AUTO.toString());
    assertEquals(expected, EVERYTHING_FROM_AUTO_COLLECTIONS.toString());
  }

  @Test
  public void testStringQuoting() {
    StringValues instance = newStringValues(
        new String[] {
          "",
          "\r\n",
          "hello, world",
          "Éamonn",
          "\007\uffef",
        });
    String expected = "@com.google.auto.value.annotations.StringValues("
        + "[\"\", \"\\r\\n\", \"hello, world\", \"Éamonn\", \"\\007\\uffef\"])";
    assertEquals(expected, instance.toString());
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface AnnotationsAnnotation {
    Class<? extends Annotation>[] value();
  }

  @AnnotationsAnnotation(AnnotationsAnnotation.class)
  static class AnnotatedWithAnnotationsAnnotation {}

  @AutoAnnotation static AnnotationsAnnotation newAnnotationsAnnotation(
      List<Class<? extends Annotation>> value) {
    return new AutoAnnotation_AutoAnnotationTest_newAnnotationsAnnotation(value);
  }

  @Test
  public void testGenericArray() {
    AnnotationsAnnotation generated =
        newAnnotationsAnnotation(
            ImmutableList.<Class<? extends Annotation>>of(AnnotationsAnnotation.class));
    AnnotationsAnnotation fromReflect =
        AnnotatedWithAnnotationsAnnotation.class.getAnnotation(AnnotationsAnnotation.class);
    assertEquals(fromReflect, generated);
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface ClassesAnnotation {
    Class<?>[] value();
  }

  @ClassesAnnotation(AnnotationsAnnotation.class)
  static class AnnotatedWithClassesAnnotation {}

  @AutoAnnotation static ClassesAnnotation newClassesAnnotation(
      List<Class<?>> value) {
    return new AutoAnnotation_AutoAnnotationTest_newClassesAnnotation(value);
  }

  @Test
  public void testWildcardArray() {
    ClassesAnnotation generated =
        newClassesAnnotation(
            Arrays.<Class<?>>asList(AnnotationsAnnotation.class));
    ClassesAnnotation fromReflect =
        AnnotatedWithClassesAnnotation.class.getAnnotation(ClassesAnnotation.class);
    assertEquals(fromReflect, generated);
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface IntegersAnnotation {
    int one() default Integer.MAX_VALUE;
    int two() default Integer.MAX_VALUE;
    int three();
  }

  @IntegersAnnotation(three = 23)
  static class AnnotatedWithIntegersAnnotation {}

  @AutoAnnotation static IntegersAnnotation newIntegersAnnotation(int three) {
    return new AutoAnnotation_AutoAnnotationTest_newIntegersAnnotation(three);
  }

  @Test
  public void testConstantOverflowInHashCode() {
    IntegersAnnotation generated = newIntegersAnnotation(23);
    IntegersAnnotation fromReflect =
        AnnotatedWithIntegersAnnotation.class.getAnnotation(IntegersAnnotation.class);
    new EqualsTester().addEqualityGroup(generated, fromReflect).testEquals();
  }

  @Retention(RetentionPolicy.RUNTIME)
  @interface EverythingWithDefaults {
    byte aByte() default 5;
    short aShort() default 17;
    int anInt() default 23;
    long aLong() default 1729;
    float aFloat() default 5;
    double aDouble() default 17;
    char aChar() default 'x';
    boolean aBoolean() default true;
    String aString() default "whatever";
    RetentionPolicy anEnum() default RetentionPolicy.CLASS;
    // We don't yet support defaulting annotation values.
    // StringValues anAnnotation() default @StringValues({"foo", "bar"});
    byte[] bytes() default {1, 2};
    short[] shorts() default {3, 4};
    int[] ints() default {5, 6};
    long[] longs() default {7, 8};
    float[] floats() default {9, 10};
    double[] doubles() default {11, 12};
    char[] chars() default {'D', 'E'};
    boolean[] booleans() default {true, false};
    String[] strings() default {"vrai", "faux"};
    RetentionPolicy[] enums() default {RetentionPolicy.SOURCE, RetentionPolicy.CLASS};
    // We don't yet support defaulting annotation values.
    // StringValues[] annotations() default {
    //   @StringValues({"foo", "bar"}), @StringValues({"baz", "buh"})
    // };
  }

  @EverythingWithDefaults
  static class AnnotatedWithEverythingWithDefaults {}

  @AutoAnnotation static EverythingWithDefaults newEverythingWithDefaults() {
    return new AutoAnnotation_AutoAnnotationTest_newEverythingWithDefaults();
  }

  @Test
  public void testDefaultedValues() {
    EverythingWithDefaults generated = newEverythingWithDefaults();
    EverythingWithDefaults fromReflect =
        AnnotatedWithEverythingWithDefaults.class.getAnnotation(EverythingWithDefaults.class);
    new EqualsTester().addEqualityGroup(generated, fromReflect).testEquals();
  }
}
