/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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 libcore.java.lang.reflect;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import junit.framework.TestCase;

public final class AnnotationsTest extends TestCase {

    public void testClassDirectAnnotations() {
        assertAnnotatedElement(Type.class,
                AnnotationA.class, AnnotationB.class, RepeatableAnnotation.class);
        assertAnnotatedElementDeclared(Type.class,
                AnnotationA.class, AnnotationB.class, RepeatableAnnotation.class);
    }

    public void testClassInheritedAnnotations() {
        assertAnnotatedElement(ExtendsType.class, AnnotationB.class);
        assertAnnotatedElementDeclared(ExtendsType.class);
    }

    public void testConstructorAnnotations() throws Exception {
        Constructor<Type> constructor = Type.class.getConstructor();
        assertAnnotatedElement(constructor, AnnotationA.class, AnnotationC.class);
    }

    public void testFieldAnnotations() throws Exception {
        Field field = Type.class.getField("field");
        assertAnnotatedElement(field, AnnotationA.class, AnnotationD.class);
    }

    public void testMethodAnnotations() throws Exception {
        Method method = Type.class.getMethod("method", String.class, String.class);
        assertAnnotatedElement(method, AnnotationB.class, AnnotationC.class);
    }

    public void testParameterAnnotations() throws Exception {
        Method method = Type.class.getMethod("method", String.class, String.class);
        Annotation[][] noParameterAnnotations = method.getParameterAnnotations();
        assertEquals(2, noParameterAnnotations.length);
        assertEquals(set(), annotationsToTypes(noParameterAnnotations[0]));
        assertEquals(set(), annotationsToTypes(noParameterAnnotations[1]));

        Method parameters = Type.class.getMethod("parameters", String.class, String.class);
        Annotation[][] parameterAnnotations = parameters.getParameterAnnotations();
        assertEquals(2, parameterAnnotations.length);
        assertEquals(set(AnnotationB.class, AnnotationD.class),
                annotationsToTypes(parameterAnnotations[0]));
        assertEquals(set(AnnotationC.class, AnnotationD.class),
                annotationsToTypes(parameterAnnotations[1]));
    }

    public void testAnnotationDefaults() throws Exception {
        assertEquals((byte) 5, defaultValue("a"));
        assertEquals((short) 6, defaultValue("b"));
        assertEquals(7, defaultValue("c"));
        assertEquals(8L, defaultValue("d"));
        assertEquals(9.0f, defaultValue("e"));
        assertEquals(10.0, defaultValue("f"));
        assertEquals('k', defaultValue("g"));
        assertEquals(true, defaultValue("h"));
        assertEquals(Breakfast.WAFFLES, defaultValue("i"));
        assertEquals("@" + AnnotationA.class.getName() + "()", defaultValue("j").toString());
        assertEquals("maple", defaultValue("k"));
        assertEquals(AnnotationB.class, defaultValue("l"));
        assertEquals("[1, 2, 3]", Arrays.toString((int[]) defaultValue("m")));
        assertEquals("[WAFFLES, PANCAKES]", Arrays.toString((Breakfast[]) defaultValue("n")));
        assertEquals(null, defaultValue("o"));
        assertEquals(null, defaultValue("p"));
    }

    private Object defaultValue(String name) throws NoSuchMethodException {
        return HasDefaultsAnnotation.class.getMethod(name).getDefaultValue();
    }

    public void testGetEnclosingClass() {
        assertNull(AnnotationsTest.class.getEnclosingClass());
        assertEquals(AnnotationsTest.class, Foo.class.getEnclosingClass());
        assertEquals(AnnotationsTest.class, HasMemberClassesInterface.class.getEnclosingClass());
        assertEquals(HasMemberClassesInterface.class,
                HasMemberClassesInterface.D.class.getEnclosingClass());
        assertEquals(AnnotationsTest.class, Foo.class.getEnclosingClass());
    }

    public void testGetDeclaringClass() {
        assertNull(AnnotationsTest.class.getDeclaringClass());
        assertEquals(AnnotationsTest.class, Foo.class.getDeclaringClass());
        assertEquals(AnnotationsTest.class, HasMemberClassesInterface.class.getDeclaringClass());
        assertEquals(HasMemberClassesInterface.class,
                HasMemberClassesInterface.D.class.getDeclaringClass());
    }

    public void testGetEnclosingClassIsTransitiveForClassesDefinedInAMethod() {
        class C {}
        assertEquals(AnnotationsTest.class, C.class.getEnclosingClass());
    }

    public void testGetDeclaringClassIsNotTransitiveForClassesDefinedInAMethod() {
        class C {}
        assertEquals(null, C.class.getDeclaringClass());
    }

    public void testGetEnclosingMethodIsNotTransitive() {
        class C {
            class D {}
        }
        assertEquals(null, C.D.class.getEnclosingMethod());
    }

    public void testStaticFieldAnonymousClass() {
        // The class declared in the <clinit> is enclosed by the <clinit>'s class.
        // http://b/11245138
        assertEquals(AnnotationsTest.class, staticAnonymous.getClass().getEnclosingClass());
        // However, because it is anonymous, it has no declaring class.
        // https://code.google.com/p/android/issues/detail?id=61003
        assertNull(staticAnonymous.getClass().getDeclaringClass());
        // Because the class is declared in <clinit> which is not exposed through reflection,
        // it has no enclosing method or constructor.
        assertNull(staticAnonymous.getClass().getEnclosingMethod());
        assertNull(staticAnonymous.getClass().getEnclosingConstructor());
    }

    public void testGetEnclosingMethodOfTopLevelClass() {
        assertNull(AnnotationsTest.class.getEnclosingMethod());
    }

    public void testGetEnclosingConstructorOfTopLevelClass() {
        assertNull(AnnotationsTest.class.getEnclosingConstructor());
    }

    public void testClassEnclosedByConstructor() throws Exception {
        Foo foo = new Foo("string");
        assertEquals(Foo.class, foo.c.getEnclosingClass());
        assertEquals(Foo.class.getDeclaredConstructor(String.class),
                foo.c.getEnclosingConstructor());
        assertNull(foo.c.getEnclosingMethod());
        assertNull(foo.c.getDeclaringClass());
    }

    public void testClassEnclosedByMethod() throws Exception {
        Foo foo = new Foo();
        foo.foo("string");
        assertEquals(Foo.class, foo.c.getEnclosingClass());
        assertNull(foo.c.getEnclosingConstructor());
        assertEquals(Foo.class.getDeclaredMethod("foo", String.class),
                foo.c.getEnclosingMethod());
        assertNull(foo.c.getDeclaringClass());
    }

    public void testGetClasses() throws Exception {
        // getClasses() doesn't include classes inherited from interfaces!
        assertSetEquals(HasMemberClasses.class.getClasses(),
                HasMemberClassesSuperclass.B.class, HasMemberClasses.H.class);
    }

    public void testGetDeclaredClasses() throws Exception {
        assertSetEquals(HasMemberClasses.class.getDeclaredClasses(),
                HasMemberClasses.G.class, HasMemberClasses.H.class, HasMemberClasses.I.class,
                HasMemberClasses.J.class, HasMemberClasses.K.class, HasMemberClasses.L.class);
    }

    public void testConstructorGetExceptions() throws Exception {
        assertSetEquals(HasThrows.class.getConstructor().getExceptionTypes(),
                IOException.class, InvocationTargetException.class, IllegalStateException.class);
        assertSetEquals(HasThrows.class.getConstructor(Void.class).getExceptionTypes());
    }

    public void testClassMethodGetExceptions() throws Exception {
        assertSetEquals(HasThrows.class.getMethod("foo").getExceptionTypes(),
                IOException.class, InvocationTargetException.class, IllegalStateException.class);
        assertSetEquals(HasThrows.class.getMethod("foo", Void.class).getExceptionTypes());
    }

    public void testProxyMethodGetExceptions() throws Exception {
        InvocationHandler emptyInvocationHandler = new InvocationHandler() {
            @Override public Object invoke(Object proxy, Method method, Object[] args) {
                return null;
            }
        };

        Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class[] { ThrowsInterface.class }, emptyInvocationHandler);
        assertSetEquals(proxy.getClass().getMethod("foo").getExceptionTypes(),
                IOException.class, InvocationTargetException.class, IllegalStateException.class);
        assertSetEquals(proxy.getClass().getMethod("foo", Void.class).getExceptionTypes());
    }

    public void testClassModifiers() {
        int modifiers = AnnotationsTest.class.getModifiers();
        assertTrue(Modifier.isPublic(modifiers));
        assertFalse(Modifier.isProtected(modifiers));
        assertFalse(Modifier.isPrivate(modifiers));
        assertFalse(Modifier.isAbstract(modifiers));
        assertFalse(Modifier.isStatic(modifiers));
        assertTrue(Modifier.isFinal(modifiers));
        assertFalse(Modifier.isStrict(modifiers));
    }

    public void testInnerClassModifiers() {
        int modifiers = Foo.class.getModifiers();
        assertFalse(Modifier.isPublic(modifiers));
        assertFalse(Modifier.isProtected(modifiers));
        assertTrue(Modifier.isPrivate(modifiers));
        assertFalse(Modifier.isAbstract(modifiers));
        assertTrue(Modifier.isStatic(modifiers));
        assertFalse(Modifier.isFinal(modifiers));
        assertFalse(Modifier.isStrict(modifiers));
    }

    public void testAnonymousClassModifiers() {
        int modifiers = staticAnonymous.getClass().getModifiers();
        assertFalse(Modifier.isPublic(modifiers));
        assertFalse(Modifier.isProtected(modifiers));
        assertFalse(Modifier.isPrivate(modifiers));
        assertFalse(Modifier.isAbstract(modifiers));
        assertTrue(Modifier.isStatic(modifiers));
        assertFalse(Modifier.isFinal(modifiers));
        assertFalse(Modifier.isStrict(modifiers));
    }

    public void testInnerClassName() {
        assertEquals("AnnotationsTest", AnnotationsTest.class.getSimpleName());
        assertEquals("Foo", Foo.class.getSimpleName());
        assertEquals("", staticAnonymous.getClass().getSimpleName());
    }

    public void testIsAnonymousClass() {
        assertFalse(AnnotationsTest.class.isAnonymousClass());
        assertFalse(Foo.class.isAnonymousClass());
        assertTrue(staticAnonymous.getClass().isAnonymousClass());
    }

    public void testRepeatableAnnotation() {
        RepeatableAnnotation[] annotations = TypeWithMultipleRepeatableAnnotations.class
                .getDeclaredAnnotationsByType(RepeatableAnnotation.class);
        assertNotNull(annotations);
        assertEquals(2, annotations.length);

        // The non-"WithType" methods will see the wrapper annotation
        assertAnnotatedElement(TypeWithMultipleRepeatableAnnotations.class,
                RepeatableAnnotations.class);
        assertAnnotatedElementDeclared(TypeWithMultipleRepeatableAnnotations.class,
                RepeatableAnnotations.class);
    }

    public void testRepeatableAnnotationExplicit() {
        RepeatableAnnotation[] annotations = TypeWithExplicitRepeatableAnnotations.class
                .getDeclaredAnnotationsByType(RepeatableAnnotation.class);
        assertNotNull(annotations);
        assertEquals(2, annotations.length);

        // The non-"WithType" methods will see the wrapper annotation
        assertAnnotatedElement(TypeWithExplicitRepeatableAnnotations.class,
                RepeatableAnnotations.class);
        assertAnnotatedElementDeclared(TypeWithExplicitRepeatableAnnotations.class,
                RepeatableAnnotations.class);
    }

    public void testRepeatableAnnotationOnPackage() {
        Package aPackage = AnnotationsTest.class.getPackage();
        RepeatableAnnotation[] annotations = aPackage
                .getDeclaredAnnotationsByType(RepeatableAnnotation.class);
        assertNotNull(annotations);
        assertEquals(2, annotations.length);

        // The non-"WithType" methods will see the wrapper annotation
        assertPresent(true, aPackage, RepeatableAnnotations.class);
        assertDeclared(true, aPackage, RepeatableAnnotations.class);
    }

    public void testRetentionPolicy() {
        assertNull(RetentionAnnotations.class.getAnnotation(ClassRetentionAnnotation.class));
        assertNotNull(RetentionAnnotations.class.getAnnotation(RuntimeRetentionAnnotation.class));
        assertNull(RetentionAnnotations.class.getAnnotation(SourceRetentionAnnotation.class));
    }

    private static final Object staticAnonymous = new Object() {};

    private static class Foo {
        Class<?> c;
        private Foo() {
        }
        private Foo(String s) {
            c = new Object() {}.getClass();
        }
        private Foo(int i) {
            c = new Object() {}.getClass();
        }
        private void foo(String s) {
            c = new Object() {}.getClass();
        }
        private void foo(int i) {
            c = new Object() {}.getClass();
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationA {}

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationB {}

    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationC {}

    @Retention(RetentionPolicy.RUNTIME)
    public @interface AnnotationD {}

    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(RepeatableAnnotations.class)
    public @interface RepeatableAnnotation {}

    @Retention(RetentionPolicy.RUNTIME)
    public @interface RepeatableAnnotations {
        RepeatableAnnotation[] value();
    }

    @Retention(RetentionPolicy.CLASS)
    public @interface ClassRetentionAnnotation {}

    @Retention(RetentionPolicy.RUNTIME)
    public @interface RuntimeRetentionAnnotation {}

    @Retention(RetentionPolicy.SOURCE)
    public @interface SourceRetentionAnnotation {}

    @AnnotationA @AnnotationB @RepeatableAnnotation
    public static class Type {
        @AnnotationA @AnnotationC public Type() {}
        @AnnotationA @AnnotationD public String field;
        @AnnotationB @AnnotationC public void method(String parameter1, String parameter2) {}
        @AnnotationB @AnnotationC public void parameters(@AnnotationB @AnnotationD String parameter1,
                @AnnotationC @AnnotationD String parameter2) {}
    }

    public static class ExtendsType extends Type {}

    @RepeatableAnnotation
    @RepeatableAnnotation
    public static class TypeWithMultipleRepeatableAnnotations {}

    @RepeatableAnnotations({ @RepeatableAnnotation, @RepeatableAnnotation})
    public static class TypeWithExplicitRepeatableAnnotations {}

    @ClassRetentionAnnotation @RuntimeRetentionAnnotation @SourceRetentionAnnotation
    public static class RetentionAnnotations {}

    static enum Breakfast { WAFFLES, PANCAKES }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface HasDefaultsAnnotation {
        byte a() default 5;
        short b() default 6;
        int c() default 7;
        long d() default 8;
        float e() default 9.0f;
        double f() default 10.0;
        char g() default 'k';
        boolean h() default true;
        Breakfast i() default Breakfast.WAFFLES;
        AnnotationA j() default @AnnotationA();
        String k() default "maple";
        Class l() default AnnotationB.class;
        int[] m() default { 1, 2, 3 };
        Breakfast[] n() default { Breakfast.WAFFLES, Breakfast.PANCAKES };
        Breakfast o();
        int p();
    }

    static class HasMemberClassesSuperclass {
        class A {}
        public class B {}
        static class C {}
    }

    public interface HasMemberClassesInterface {
        class D {}
        public class E {}
        static class F {}
    }

    public static class HasMemberClasses extends HasMemberClassesSuperclass
            implements HasMemberClassesInterface {
        class G {}
        public class H {}
        static class I {}
        enum J {}
        interface K {}
        @interface L {}
    }

    public static class HasThrows {
        public HasThrows() throws IOException, InvocationTargetException, IllegalStateException {}
        public HasThrows(Void v) {}
        public void foo() throws IOException, InvocationTargetException, IllegalStateException {}
        public void foo(Void v) {}
    }

    public static interface ThrowsInterface {
        void foo() throws IOException, InvocationTargetException, IllegalStateException;
        void foo(Void v);
    }

    private void assertAnnotatedElement(
            AnnotatedElement element, Class<? extends Annotation>... expectedAnnotations) {
        Set<Class<? extends Annotation>> actualTypes = annotationsToTypes(element.getAnnotations());
        Set<Class<? extends Annotation>> expectedTypes = set(expectedAnnotations);
        assertEquals(expectedTypes, actualTypes);

        // getAnnotations() should be consistent with isAnnotationPresent() and getAnnotation()
        assertPresent(expectedTypes.contains(AnnotationA.class), element, AnnotationA.class);
        assertPresent(expectedTypes.contains(AnnotationB.class), element, AnnotationB.class);
        assertPresent(expectedTypes.contains(AnnotationC.class), element, AnnotationC.class);
        assertPresent(expectedTypes.contains(RepeatableAnnotation.class),
                element, RepeatableAnnotation.class);

        try {
            element.isAnnotationPresent(null);
            fail();
        } catch (NullPointerException expected) {
        }

        try {
            element.getAnnotation(null);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    private void assertAnnotatedElementDeclared(
            AnnotatedElement element,
            Class<? extends Annotation>... expectedDeclaredAnnotations) {
        Set<Class<? extends Annotation>> actualTypes = annotationsToTypes(element.getDeclaredAnnotations());
        Set<Class<? extends Annotation>> expectedTypes = set(expectedDeclaredAnnotations);
        assertEquals(expectedTypes, actualTypes);

        assertDeclared(expectedTypes.contains(AnnotationA.class), element, AnnotationA.class);
        assertDeclared(expectedTypes.contains(AnnotationB.class), element, AnnotationB.class);
        assertDeclared(expectedTypes.contains(AnnotationC.class), element, AnnotationC.class);
        assertDeclared(expectedTypes.contains(RepeatableAnnotation.class),
                element, RepeatableAnnotation.class);

        try {
            element.getDeclaredAnnotation(null);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    private Set<Class<? extends Annotation>> annotationsToTypes(Annotation[] annotations) {
        Set<Class<? extends Annotation>> result = new HashSet<Class<? extends Annotation>>();
        for (Annotation annotation : annotations) {
            result.add(annotation.annotationType());
        }
        return result;
    }

    private void assertPresent(boolean present, AnnotatedElement element,
            Class<? extends Annotation> annotation) {
        if (present) {
            assertNotNull(element.getAnnotation(annotation));
            assertTrue(element.isAnnotationPresent(annotation));
        } else {
            assertNull(element.getAnnotation(annotation));
            assertFalse(element.isAnnotationPresent(annotation));
        }
    }

    private void assertDeclared(boolean present, AnnotatedElement element,
            Class<? extends Annotation> annotation) {
        if (present) {
            assertNotNull(element.getDeclaredAnnotation(annotation));
        } else {
            assertNull(element.getDeclaredAnnotation(annotation));
        }
    }

    private <T> Set<T> set(T... instances) {
        return new HashSet<T>(Arrays.asList(instances));
    }

    private void assertSetEquals(Object[] actual, Object... expected) {
        Set<Object> actualSet = new HashSet<Object>(Arrays.asList(actual));
        Set<Object> expectedSet = new HashSet<Object>(Arrays.asList(expected));
        assertEquals(expectedSet, actualSet);
    }
}
