/*
 * Copyright 2010-2016 JetBrains s.r.o.
 *
 * 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.jetbrains.kotlin.codegen;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.test.ConfigurationKind;

import java.lang.annotation.*;
import java.lang.reflect.*;

public class AnnotationGenTest extends CodegenTestCase {
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        createEnvironmentWithMockJdkAndIdeaAnnotations(ConfigurationKind.ALL);
    }

    public void testVolatileProperty() throws Exception {
        loadText("abstract class Foo { @Volatile public var x: String = \"\"; }");
        Class<?> aClass = generateClass("Foo");
        Field x = aClass.getDeclaredField("x");
        assertTrue((x.getModifiers() & Modifier.VOLATILE) != 0);
    }

    public void testPropField() throws Exception {
        loadText("@[java.lang.Deprecated] var x = 0");
        Class<?> srcClass = generateFacadeClass();
        assertNull(srcClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNull(srcClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        assertNotNull(srcClass.getDeclaredField("x").getAnnotation(Deprecated.class));
    }

    public void testPropGetter() throws Exception {
        loadText("var x = 0\n" +
                 "@[java.lang.Deprecated] get");
        Class<?> srcClass = generateFacadeClass();
        assertNotNull(srcClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNull(srcClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        assertNull(srcClass.getDeclaredField("x").getAnnotation(Deprecated.class));
    }

    public void testPropSetter() throws Exception {
        loadText("var x = 0\n" +
                 "@[java.lang.Deprecated] set");
        Class<?> scrClass = generateFacadeClass();
        assertNull(scrClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNotNull(scrClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        assertNull(scrClass.getDeclaredField("x").getAnnotation(Deprecated.class));
    }

    public void testAnnotationForParamInTopLevelFunction() throws Exception {
        loadText("fun x(@[java.lang.Deprecated] i: Int) {}");
        Class<?> srcClass = generateFacadeClass();
        Method srcClassMethod = srcClass.getMethod("x", int.class);
        assertNotNull(srcClassMethod);
        assertNotNull(getDeprecatedAnnotationFromList(srcClassMethod.getParameterAnnotations()[0]));
    }

    public void testAnnotationForParamInInstanceFunction() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class A() { fun x(@[java.lang.Deprecated] i: Int) {}}");
        Class<?> aClass = generateClass("A");
        Method x = aClass.getMethod("x", int.class);
        assertNotNull(x);
        // Get annotations for first parameter
        Annotation[] annotations = x.getParameterAnnotations()[0];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
    }

    public void testAnnotationForParamInInstanceExtensionFunction() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class A() { fun String.x(@[java.lang.Deprecated] i: Int) {}}");
        Class<?> aClass = generateClass("A");
        Method x = aClass.getMethod("x", String.class, int.class);
        assertNotNull(x);
        // Get annotations for first real parameter
        Annotation[] annotations = x.getParameterAnnotations()[1];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
    }

    public void testParamInConstructor() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class A (@[java.lang.Deprecated] x: Int) {}");
        Class<?> aClass = generateClass("A");
        Constructor constructor = aClass.getDeclaredConstructor(int.class);
        assertNotNull(constructor);
        // Get annotations for first parameter
        Annotation[] annotations = constructor.getParameterAnnotations()[0];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
    }

    public void testParamInEnumConstructor() throws NoSuchFieldException, NoSuchMethodException {
        loadText("enum class E(@[java.lang.Deprecated] p: String)");
        Class<?> klass = generateClass("E");
        Constructor constructor = klass.getDeclaredConstructor(String.class, int.class, String.class);
        assertNotNull(constructor);
        // Get annotations for first parameter
        Annotation[] annotations = constructor.getParameterAnnotations()[0];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
    }

    public void testParamInInnerConstructor() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class Outer { inner class Inner(@[java.lang.Deprecated] x: Int) }");
        Class<?> outer = generateClass("Outer");
        Class<?> inner = outer.getDeclaredClasses()[0];
        Constructor constructor = inner.getDeclaredConstructor(outer, int.class);
        assertNotNull(constructor);
        // Get annotations for first parameter
        Annotation[] annotations = constructor.getParameterAnnotations()[0];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
    }

    public void testPropFieldInConstructor() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class A (@field:java.lang.Deprecated @param:java.lang.Deprecated var x: Int) {}");
        Class<?> aClass = generateClass("A");
        Constructor constructor = aClass.getDeclaredConstructor(int.class);
        assertNotNull(constructor);
        // Get annotations for first parameter
        Annotation[] annotations = constructor.getParameterAnnotations()[0];
        assertNotNull(getDeprecatedAnnotationFromList(annotations));
        assertNull(aClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNull(aClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        assertNotNull(aClass.getDeclaredField("x").getAnnotation(Deprecated.class));
    }
    
    public void testAnnotationWithParamForParamInFunction() throws Exception {
        loadText("import java.lang.annotation.*\n" +
                 "@java.lang.annotation.Retention(RetentionPolicy.RUNTIME) annotation class A(val a: String)\n" +
                 "fun x(@A(\"239\") i: Int) {}");
        Class<?> packageClass = generateFacadeClass();
        Method packageClassMethod = packageClass.getMethod("x", int.class);
        assertNotNull(packageClassMethod);
        assertNotNull(getAnnotationByName(packageClassMethod.getParameterAnnotations()[0], "A"));
    }

    @Nullable
    private static Annotation getAnnotationByName(@NotNull Annotation[] annotations, @NotNull String name) {
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().getCanonicalName().equals(name)) {
                return annotation;
            }
        }
        return null;
    }

    private static Deprecated getDeprecatedAnnotationFromList(Annotation[] annotations) {
        for (Annotation annotation : annotations) {
            if (annotation instanceof Deprecated) {
                return (Deprecated) annotation;
            }
        }
        return null;
    } 

    public void testConstructor() throws NoSuchFieldException, NoSuchMethodException {
        loadText("class A @[java.lang.Deprecated] constructor() {}");
        Class<?> aClass = generateClass("A");
        Constructor<?> x = aClass.getDeclaredConstructor();
        Deprecated annotation = x.getAnnotation(Deprecated.class);
        assertNotNull(annotation);
    }

    public void testMethod() throws Exception {
        loadText("@[java.lang.Deprecated] fun x () {}");
        Class<?> srcClass = generateFacadeClass();
        Method srcClassMethod = srcClass.getDeclaredMethod("x");
        assertNotNull(srcClassMethod.getAnnotation(Deprecated.class));
    }

    public void testClass() throws NoSuchFieldException, NoSuchMethodException {
        loadText("@[java.lang.Deprecated] class A () {}");
        Class<?> aClass = generateClass("A");
        Deprecated annotation = aClass.getAnnotation(Deprecated.class);
        assertNotNull(annotation);
    }

    public void testSimplestAnnotationClass() {
        loadText("annotation class A");
        Class<?> aClass = generateClass("A");
        Class[] interfaces = aClass.getInterfaces();
        assertEquals(0, aClass.getDeclaredMethods().length);
        assertTrue(aClass.isAnnotation());
        assertEquals(1, interfaces.length);
        assertEquals("java.lang.annotation.Annotation", interfaces[0].getName());
    }

    public void testAnnotationClassWithStringProperty()
        throws
        NoSuchFieldException,
        NoSuchMethodException,
        ClassNotFoundException,
        IllegalAccessException,
        InstantiationException,
        InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "" +
                 "@java.lang.annotation.Retention(RetentionPolicy.RUNTIME) annotation class A(val a: String)\n" +
                 "" +
                 "@A(\"239\") class B()");
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> aClass = (Class) generateClass("A");

        Retention annotation = aClass.getAnnotation(Retention.class);
        RetentionPolicy value = annotation.value();
        assertEquals(RetentionPolicy.RUNTIME, value);

        Method[] methods = aClass.getDeclaredMethods();
        assertEquals(1, methods.length);
        assertEquals("a", methods[0].getName());
        assertEquals(String.class, methods[0].getReturnType());
        assertEquals(0, methods[0].getParameterTypes().length);
        assertTrue(aClass.isAnnotation());

        Class<?> bClass = aClass.getClassLoader().loadClass("B");
        Annotation bClassAnnotation = bClass.getAnnotation(aClass);
        assertNotNull(bClassAnnotation);

        assertEquals("239", methods[0].invoke(bClassAnnotation));
    }

    public void testAnnotationClassWithAnnotationProperty()
        throws
        NoSuchFieldException,
        NoSuchMethodException,
        ClassNotFoundException,
        IllegalAccessException,
        InstantiationException,
        InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "" +
                 "annotation class C(val c: String)\n" +
                 "@java.lang.annotation.Retention(RetentionPolicy.RUNTIME) annotation class A(val a: C)\n" +
                 "" +
                 "@A(C(\"239\")) class B()");
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> aClass = (Class) generateClass("A");

        Retention annotation = aClass.getAnnotation(Retention.class);
        RetentionPolicy value = annotation.value();
        assertEquals(RetentionPolicy.RUNTIME, value);

        Method[] methods = aClass.getDeclaredMethods();
        assertEquals(1, methods.length);
        assertEquals("a", methods[0].getName());
        assertEquals("C", methods[0].getReturnType().getName());
        assertEquals(0, methods[0].getParameterTypes().length);
        assertTrue(aClass.isAnnotation());

        Class<?> bClass = aClass.getClassLoader().loadClass("B");
        Annotation bClassAnnotation = bClass.getAnnotation(aClass);
        assertNotNull(bClassAnnotation);

        Object invoke = methods[0].invoke(bClassAnnotation);
        // there is some Proxy here
        Class<?> cClass = invoke.getClass().getInterfaces()[0];
        assertEquals("C", cClass.getName());
        assertEquals("239", cClass.getDeclaredMethod("c").invoke(invoke));
    }

    public void testAnnotationClassWithStringArrayProperty()
            throws
            NoSuchFieldException,
            NoSuchMethodException,
            ClassNotFoundException,
            IllegalAccessException,
            InstantiationException,
            InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "" +
                 "@java.lang.annotation.Retention(RetentionPolicy.RUNTIME) annotation class A(val a: Array<String>)\n" +
                 "" +
                 "@A(arrayOf(\"239\",\"932\")) class B()");
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> aClass = (Class) generateClass("A");

        Retention annotation = aClass.getAnnotation(Retention.class);
        RetentionPolicy value = annotation.value();
        assertEquals(RetentionPolicy.RUNTIME, value);

        Method[] methods = aClass.getDeclaredMethods();
        assertEquals(1, methods.length);
        assertEquals("a", methods[0].getName());
        assertEquals(String[].class, methods[0].getReturnType());
        assertEquals(0, methods[0].getParameterTypes().length);
        assertTrue(aClass.isAnnotation());

        Class<?> bClass = aClass.getClassLoader().loadClass("B");
        Annotation bClassAnnotation = bClass.getAnnotation(aClass);
        assertNotNull(bClassAnnotation);

        Object invoke = methods[0].invoke(bClassAnnotation);
        assertEquals("239", ((String[])invoke)[0]);
        assertEquals("932", ((String[])invoke)[1]);
    }

    public void testAnnotationClassWithIntArrayProperty()
            throws
            NoSuchFieldException,
            NoSuchMethodException,
            ClassNotFoundException,
            IllegalAccessException,
            InstantiationException,
            InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "" +
                 "@java.lang.annotation.Retention(RetentionPolicy.RUNTIME) annotation class A(val a: IntArray)\n" +
                 "" +
                 "@A(intArrayOf(239,932)) class B()");
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> aClass = (Class) generateClass("A");

        Retention annotation = aClass.getAnnotation(Retention.class);
        RetentionPolicy value = annotation.value();
        assertEquals(RetentionPolicy.RUNTIME, value);

        Method[] methods = aClass.getDeclaredMethods();
        assertEquals(1, methods.length);
        assertEquals("a", methods[0].getName());
        assertEquals(int[].class, methods[0].getReturnType());
        assertEquals(0, methods[0].getParameterTypes().length);
        assertTrue(aClass.isAnnotation());

        Class<?> bClass = aClass.getClassLoader().loadClass("B");
        Annotation bClassAnnotation = bClass.getAnnotation(aClass);
        assertNotNull(bClassAnnotation);

        Object invoke = methods[0].invoke(bClassAnnotation);
        assertEquals(239, ((int[])invoke)[0]);
        assertEquals(932, ((int[])invoke)[1]);
    }

    public void testAnnotationClassWithEnumArrayProperty()
            throws
            NoSuchFieldException,
            NoSuchMethodException,
            ClassNotFoundException,
            IllegalAccessException,
            InstantiationException,
            InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "" +
                 "@java.lang.annotation.Target(ElementType.TYPE, ElementType.METHOD) annotation class A");
        Class<?> aClass = generateClass("A");

        Target annotation = aClass.getAnnotation(Target.class);
        ElementType[] value = annotation.value();
        assertEquals(2, value.length);

        assertEquals(ElementType.TYPE, value[0]);
        assertEquals(ElementType.METHOD, value[1]);
    }

    public void testAnnotationClassWithAnnotationArrayProperty()
            throws
            NoSuchFieldException,
            NoSuchMethodException,
            ClassNotFoundException,
            IllegalAccessException,
            InstantiationException,
            InvocationTargetException {
        loadText("import java.lang.annotation.*\n" +
                 "import java.lang.annotation.Retention\n" +
                 "" +
                 "@Retention(RetentionPolicy.RUNTIME) annotation class A(val a: Array<Retention>)\n" +
                 "" +
                 "@A(arrayOf(Retention(RetentionPolicy.RUNTIME),Retention(RetentionPolicy.SOURCE))) class B()");
        @SuppressWarnings("unchecked")
        Class<? extends Annotation> aClass = (Class) generateClass("A");

        Method[] methods = aClass.getDeclaredMethods();
        assertEquals(1, methods.length);
        assertEquals("a", methods[0].getName());

        Class<?> bClass = aClass.getClassLoader().loadClass("B");
        Annotation bClassAnnotation = bClass.getAnnotation(aClass);
        assertNotNull(bClassAnnotation);

        Object invoke = methods[0].invoke(bClassAnnotation);
        Retention[] invoke1 = (Retention[])invoke;
        assertEquals(2, invoke1.length);
        assertEquals(invoke1[0].value(), RetentionPolicy.RUNTIME);
        assertEquals(invoke1[1].value(), RetentionPolicy.SOURCE);
    }
}
