/*
 * Copyright 2010-2015 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.load.kotlin.PackageClassUtils;
import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils;
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);
    }

    private ClassLoader loadFileGetClassLoader(@NotNull String text) {
        loadText(text);
        return generateAndCreateClassLoader();
    }

    private Class<?> getPackageClass(@NotNull ClassLoader loader) throws ClassNotFoundException {
        return loader.loadClass(PackageClassUtils.getPackageClassName(myFiles.getPsiFile().getPackageFqName()));
    }

    private Class<?> getPackageSrcClass(@NotNull ClassLoader loader) throws ClassNotFoundException {
        return loader.loadClass(PackagePartClassUtils.getPackagePartInternalName(myFiles.getPsiFile()));
    }

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

    public void testPropField() throws Exception {
        ClassLoader loader = loadFileGetClassLoader("@[Deprecated] var x = 0");
        Class<?> packageClass = getPackageClass(loader);
        assertNull(packageClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNull(packageClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        Class<?> srcClass = getPackageSrcClass(loader);
        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 {
        ClassLoader loader = loadFileGetClassLoader("var x = 0\n" +
                 "@[Deprecated] get");
        Class<?> packageClass = getPackageClass(loader);
        assertNotNull(packageClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNull(packageClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        Class<?> srcClass = getPackageSrcClass(loader);
        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 {
        ClassLoader loader = loadFileGetClassLoader("var x = 0\n" +
                 "@[Deprecated] set");
        Class<?> packageClass = getPackageClass(loader);
        assertNull(packageClass.getDeclaredMethod("getX").getAnnotation(Deprecated.class));
        assertNotNull(packageClass.getDeclaredMethod("setX", int.class).getAnnotation(Deprecated.class));
        Class<?> scrClass = getPackageSrcClass(loader);
        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 {
        ClassLoader loader = loadFileGetClassLoader("fun x(@[Deprecated] i: Int) {}");
        Class<?> packageClass = getPackageClass(loader);
        Method packageClassMethod = packageClass.getMethod("x", int.class);
        assertNotNull(packageClassMethod);
        assertNotNull(getDeprecatedAnnotationFromList(packageClassMethod.getParameterAnnotations()[0]));
        Class<?> srcClass = getPackageSrcClass(loader);
        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(@[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(@[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 (@[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(@[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(@[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 (@[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 {
        ClassLoader loader = loadFileGetClassLoader("import java.lang.annotation.*\n" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: String)\n" +
                 "fun x(A(\"239\") i: Int) {}");
        Class<?> packageClass = getPackageSrcClass(loader);
        Method packageClassMethod = packageClass.getMethod("x", int.class);
        assertNotNull(packageClassMethod);
        assertNotNull(getAnnotationByName(packageClassMethod.getParameterAnnotations()[0], "A"));

        Class<?> srcClass = getPackageSrcClass(loader);
        Method srcClassMethod = srcClass.getMethod("x", int.class);
        assertNotNull(srcClassMethod);
        assertNotNull(getAnnotationByName(srcClassMethod.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 @[Deprecated] constructor() {}");
        Class<?> aClass = generateClass("A");
        Constructor x = aClass.getDeclaredConstructor();
        Deprecated annotation = (Deprecated) x.getAnnotation(Deprecated.class);
        assertNotNull(annotation);
    }

    public void testMethod() throws Exception {
        ClassLoader loader = loadFileGetClassLoader("@[Deprecated] fun x () {}");

        Class<?> packageClass = getPackageClass(loader);
        Method packageClassMethod = packageClass.getDeclaredMethod("x");
        assertNotNull(packageClassMethod.getAnnotation(Deprecated.class));

        Class<?> srcClass = getPackageSrcClass(loader);
        Method srcClassMethod = srcClass.getDeclaredMethod("x");
        assertNotNull(srcClassMethod.getAnnotation(Deprecated.class));

    }

    public void testClass() throws NoSuchFieldException, NoSuchMethodException {
        loadText("@[Deprecated] class A () {}");
        Class aClass = generateClass("A");
        Deprecated annotation = (Deprecated) 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" +
                 "" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: String)\n" +
                 "" +
                 "A(\"239\") class B()");
        Class aClass = generateClass("A");

        Retention annotation = (Retention)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" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: C)\n" +
                 "" +
                 "A(C(\"239\")) class B()");
        Class aClass = generateClass("A");

        Retention annotation = (Retention)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" +
                 "" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: Array<String>)\n" +
                 "" +
                 "A(array(\"239\",\"932\")) class B()");
        Class aClass = generateClass("A");

        Retention annotation = (Retention)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" +
                 "" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: IntArray)\n" +
                 "" +
                 "A(intArray(239,932)) class B()");
        Class aClass = generateClass("A");

        Retention annotation = (Retention)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" +
                 "" +
                 "Target(ElementType.TYPE, ElementType.METHOD) annotation class A");
        Class aClass = generateClass("A");

        Target annotation = (Target)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" +
                 "" +
                 "Retention(RetentionPolicy.RUNTIME) annotation class A(val a: Array<Retention>)\n" +
                 "" +
                 "A(array(Retention(RetentionPolicy.RUNTIME),Retention(RetentionPolicy.SOURCE))) class B()");
        Class aClass = 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);
    }
}
