/*
 * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.jboss.errai.codegen.test;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Set;

import org.jboss.errai.codegen.builder.ClassStructureBuilder;
import org.jboss.errai.codegen.builder.impl.ClassBuilder;
import org.junit.Test;

/**
 * Tests the generation of annotations.
 *
 * @author Johannes Barop <jb@barop.de>
 */
public class AnnotationTest extends AbstractCodegenTest {

  @Test
  public void testSingleBoolean() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.BooleanAnnotation;\n" +
            "@BooleanAnnotation(arrayValue = false, value = false)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructBooleanAnnotation(false));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiBoolean() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.BooleanAnnotation;\n" +
            "@BooleanAnnotation(arrayValue = {true, false, true}, value = true)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructBooleanAnnotation(true, false, true));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleByte() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ByteAnnotation;\n" +
            "@ByteAnnotation(arrayValue = 1, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructByteAnnotation(new byte[]{1}));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiByte() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ByteAnnotation;\n" +
            "@ByteAnnotation(arrayValue = {1, 2, 3}, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructByteAnnotation(new byte[]{1, 2, 3}));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleChar() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.CharAnnotation;\n" +
            "@CharAnnotation(arrayValue = 'a', value = 'a')\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructCharAnnotation('a'));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiChar() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.CharAnnotation;\n" +
            "@CharAnnotation(arrayValue = {'a', 'b', 'c'}, value = 'a')\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructCharAnnotation('a', 'b', 'c'));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleShort() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ShortAnnotation;\n" +
            "@ShortAnnotation(arrayValue = 1, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructShortAnnotation(new short[]{1}));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiShort() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ShortAnnotation;\n" +
            "@ShortAnnotation(arrayValue = {1, 2, 3}, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructShortAnnotation(new short[]{1, 2, 3}));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleInt() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.IntAnnotation;\n" +
            "@IntAnnotation(arrayValue = 1, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructIntAnnotation(1));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiInt() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.IntAnnotation;\n" +
            "@IntAnnotation(arrayValue = {1, 2, 3}, value = 1)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructIntAnnotation(1, 2, 3));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleLong() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.LongAnnotation;\n" +
            "@LongAnnotation(arrayValue = 1L, value = 1L)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructLongAnnotation(1l));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiLong() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.LongAnnotation;\n" +
            "@LongAnnotation(arrayValue = {1L, 2L, 3L}, value = 1L)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructLongAnnotation(1l, 2l, 3l));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleFloat() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.FloatAnnotation;\n" +
            "@FloatAnnotation(arrayValue = 1.1f, value = 1.1f)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructFloatAnnotation(1.1f));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiFloat() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.FloatAnnotation;\n" +
            "@FloatAnnotation(arrayValue = {1.1f, 2.22f, 3.333f}, value = 1.1f)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructFloatAnnotation(1.1f, 2.22f, 3.333f));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleDouble() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.DoubleAnnotation;\n" +
            "@DoubleAnnotation(arrayValue = 1.1d, value = 1.1d)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructDoubleAnnotation(1.1));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiDouble() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.DoubleAnnotation;\n" +
            "@DoubleAnnotation(arrayValue = {1.1d, 2.22d, 3.333d}, value = 1.1d)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructDoubleAnnotation(1.1, 2.22, 3.333));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleString() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.StringAnnotation;\n" +
            "@StringAnnotation(arrayValue = \"Hello World\", value = \"Hello World\")\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructStringAnnotation("Hello World"));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiString() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.StringAnnotation;\n" +
            "@StringAnnotation(arrayValue = {\"Hang\", \"to\", \"your\", \"helmet\"}, value = \"Hang\")\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructStringAnnotation("Hang", "to", "your", "helmet"));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleClass() {
    String expected = "" +
            "import java.util.Set;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ClassAnnotation;\n" +
            "@ClassAnnotation(arrayValue = Set.class, value = Set.class)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructClassAnnotation(Set.class));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiClass() {
    String expected = "" +
            "import java.util.List;\n" +
            "import java.util.Set;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.ClassAnnotation;\n" +
            "@ClassAnnotation(arrayValue = {Set.class, List.class}, value = Set.class)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructClassAnnotation(Set.class, List.class));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleEnum() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.EnumAnnotation;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.SimpleEnum;\n" +
            "@EnumAnnotation(arrayValue = SimpleEnum.YES, value = SimpleEnum.YES)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructEnumAnnotation(SimpleEnum.YES));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiEnum() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.EnumAnnotation;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.SimpleEnum;\n" +
            "@EnumAnnotation(arrayValue = {SimpleEnum.YES, SimpleEnum.NO, SimpleEnum.YES}, value = SimpleEnum.YES)\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructEnumAnnotation(SimpleEnum.YES, SimpleEnum.NO, SimpleEnum.YES));
    assertEquals(expected, cls);
  }

  @Test
  public void testSingleAnnotation() {
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.AnnotationAnnotation;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.BooleanAnnotation;\n" +
            "@AnnotationAnnotation(arrayValue = \n" +
            "               @BooleanAnnotation(arrayValue = {true, false}, value = true),\n" +
            "       value = @BooleanAnnotation(arrayValue = {true, false}, value = true))\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructAnnotationAnnotation(constructBooleanAnnotation(true, false)));
    assertEquals(expected, cls);
  }

  @Test
  public void testMultiAnnotation() {
    // XXX:
    // FIXME: The PrettyPrinter output in nested annotations is different
    String expected = "" +
            "import org.jboss.errai.codegen.test.AnnotationTest.AnnotationAnnotation;\n" +
            "import org.jboss.errai.codegen.test.AnnotationTest.BooleanAnnotation;\n" +
            "@AnnotationAnnotation(arrayValue = {\n" +
            "               @BooleanAnnotation(arrayValue = { true, false }, value = true),\n" + /* XXX */
            "               @BooleanAnnotation(arrayValue = { true, true}, value = true)},\n" + /* XXX */
            "       value = @BooleanAnnotation(arrayValue = {true, false}, value = true))\n" +
            "public class FooBar {}";
    String cls = constructTypeWithAnnotation(constructAnnotationAnnotation(constructBooleanAnnotation(true, false), constructBooleanAnnotation(true, true)));
    assertEquals(expected, cls);
  }

  private static String constructTypeWithAnnotation(Annotation... annotations) {
    ClassStructureBuilder<?> builder = ClassBuilder.define("FooBar").publicScope().body();
    for (Annotation annotation : annotations) {
      builder.getClassDefinition().addAnnotation(annotation);
    }
    return builder.toJavaString();
  }

  private enum SimpleEnum {
    YES, NO
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface BooleanAnnotation {
    boolean value();

    boolean[] arrayValue();
  }

  private static BooleanAnnotation constructBooleanAnnotation(final boolean... values) {
    return new BooleanAnnotation() {
      @Override
      public boolean value() {
        return values[0];
      }

      @Override
      public boolean[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return BooleanAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface ByteAnnotation {
    byte value();

    byte[] arrayValue();
  }

  private static ByteAnnotation constructByteAnnotation(final byte... values) {
    return new ByteAnnotation() {
      @Override
      public byte value() {
        return values[0];
      }

      @Override
      public byte[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return ByteAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface CharAnnotation {
    char value();

    char[] arrayValue();
  }

  private static CharAnnotation constructCharAnnotation(final char... values) {
    return new CharAnnotation() {
      @Override
      public char value() {
        return values[0];
      }

      @Override
      public char[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return CharAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface ShortAnnotation {
    short value();

    short[] arrayValue();
  }

  private static ShortAnnotation constructShortAnnotation(final short... values) {
    return new ShortAnnotation() {
      @Override
      public short value() {
        return values[0];
      }

      @Override
      public short[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return ShortAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface IntAnnotation {
    int value();

    int[] arrayValue();
  }

  private static IntAnnotation constructIntAnnotation(final int... values) {
    return new IntAnnotation() {
      @Override
      public int value() {
        return values[0];
      }

      @Override
      public int[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return IntAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface LongAnnotation {
    long value();

    long[] arrayValue();
  }

  private static LongAnnotation constructLongAnnotation(final long... values) {
    return new LongAnnotation() {
      @Override
      public long value() {
        return values[0];
      }

      @Override
      public long[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return LongAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface FloatAnnotation {
    float value();

    float[] arrayValue();
  }

  private static FloatAnnotation constructFloatAnnotation(final float... values) {
    return new FloatAnnotation() {
      @Override
      public float value() {
        return values[0];
      }

      @Override
      public float[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return FloatAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface DoubleAnnotation {
    double value();

    double[] arrayValue();
  }

  private static DoubleAnnotation constructDoubleAnnotation(final double... values) {
    return new DoubleAnnotation() {
      @Override
      public double value() {
        return values[0];
      }

      @Override
      public double[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return DoubleAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface StringAnnotation {
    String value();

    String[] arrayValue();
  }

  private static StringAnnotation constructStringAnnotation(final String... values) {
    return new StringAnnotation() {
      @Override
      public String value() {
        return values[0];
      }

      @Override
      public String[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return StringAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface ClassAnnotation {
    Class<?> value();

    Class<?>[] arrayValue();
  }

  private static ClassAnnotation constructClassAnnotation(final Class<?>... values) {
    return new ClassAnnotation() {
      @Override
      public Class<?> value() {
        return values[0];
      }

      @Override
      public Class<?>[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return ClassAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface EnumAnnotation {
    SimpleEnum value();

    SimpleEnum[] arrayValue();
  }

  private static EnumAnnotation constructEnumAnnotation(final SimpleEnum... values) {
    return new EnumAnnotation() {
      @Override
      public SimpleEnum value() {
        return values[0];
      }

      @Override
      public SimpleEnum[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return EnumAnnotation.class;
      }
    };
  }

  @Target(TYPE)
  @Retention(RUNTIME)
  private @interface AnnotationAnnotation {
    BooleanAnnotation value();

    BooleanAnnotation[] arrayValue();
  }

  private static AnnotationAnnotation constructAnnotationAnnotation(final BooleanAnnotation... values) {
    return new AnnotationAnnotation() {
      @Override
      public BooleanAnnotation value() {
        return values[0];
      }

      @Override
      public BooleanAnnotation[] arrayValue() {
        return values;
      }

      @Override
      public Class<? extends Annotation> annotationType() {
        return AnnotationAnnotation.class;
      }
    };
  }

}
