/*
 * Copyright (C) 2017 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 com.android.dx;

import static com.android.dx.TypeId.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.os.Build;

import androidx.test.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static java.lang.reflect.Modifier.PUBLIC;

import java.io.File;
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class AnnotationIdTest {

    /**
     *  Method Annotation definition for test
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @interface MethodAnnotation {
        boolean elementBoolean() default false;
        byte elementByte() default Byte.MIN_VALUE;
        char elementChar() default 'a';
        double elementDouble() default Double.MIN_NORMAL;
        float elementFloat() default Float.MIN_NORMAL;
        int elementInt() default Integer.MIN_VALUE;
        long elementLong() default Long.MIN_VALUE;
        short elementShort() default Short.MIN_VALUE;
        String elementString() default "foo";
        ElementEnum elementEnum() default ElementEnum.INSTANCE_0;
        Class<?> elementClass() default Object.class;
    }

    enum ElementEnum {
        INSTANCE_0,
        INSTANCE_1,
    }

    private DexMaker dexMaker;
    private static TypeId<?> GENERATED = TypeId.get("LGenerated;");
    private static final Map<TypeId<?>, Class<?>> TYPE_TO_PRIMITIVE = new HashMap<>();
    static {
        TYPE_TO_PRIMITIVE.put(BOOLEAN, boolean.class);
        TYPE_TO_PRIMITIVE.put(BYTE, byte.class);
        TYPE_TO_PRIMITIVE.put(CHAR, char.class);
        TYPE_TO_PRIMITIVE.put(DOUBLE, double.class);
        TYPE_TO_PRIMITIVE.put(FLOAT, float.class);
        TYPE_TO_PRIMITIVE.put(INT, int.class);
        TYPE_TO_PRIMITIVE.put(LONG, long.class);
        TYPE_TO_PRIMITIVE.put(SHORT, short.class);
        TYPE_TO_PRIMITIVE.put(VOID, void.class);
    }

    @Before
    public void setUp() {
        init();
    }

    /**
     *  Test adding a method annotation with new value of Boolean element.
     */
    @Test
    public void addMethodAnnotationWithBooleanElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.BOOLEAN);
        AnnotationId.Element element = new AnnotationId.Element("elementBoolean", true);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        Boolean elementBoolean = ((MethodAnnotation)methodAnnotations[0]).elementBoolean();
        assertEquals(true, elementBoolean);
    }

    /**
     *  Test adding a method annotation with new value of Byte element.
     */
    @Test
    public void addMethodAnnotationWithByteElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.BYTE);
        AnnotationId.Element element = new AnnotationId.Element("elementByte", Byte.MAX_VALUE);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        byte elementByte = ((MethodAnnotation)methodAnnotations[0]).elementByte();
        assertEquals(Byte.MAX_VALUE, elementByte);
    }

    /**
     *  Test adding a method annotation with new value of Char element.
     */
    @Test
    public void addMethodAnnotationWithCharElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.CHAR);
        AnnotationId.Element element = new AnnotationId.Element("elementChar", 'X');
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        char elementChar = ((MethodAnnotation)methodAnnotations[0]).elementChar();
        assertEquals('X', elementChar);
    }

    /**
     *  Test adding a method annotation with new value of Double element.
     */
    @Test
    public void addMethodAnnotationWithDoubleElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.DOUBLE);
        AnnotationId.Element element = new AnnotationId.Element("elementDouble", Double.NaN);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        double elementDouble = ((MethodAnnotation)methodAnnotations[0]).elementDouble();
        assertEquals(Double.NaN, elementDouble, 0);
    }

    /**
     *  Test adding a method annotation with new value of Float element.
     */
    @Test
    public void addMethodAnnotationWithFloatElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.FLOAT);
        AnnotationId.Element element = new AnnotationId.Element("elementFloat", Float.NaN);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        float elementFloat = ((MethodAnnotation)methodAnnotations[0]).elementFloat();
        assertEquals(Float.NaN, elementFloat, 0);
    }

    /**
     *  Test adding a method annotation with new value of Int element.
     */
    @Test
    public void addMethodAnnotationWithIntElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.INT);
        AnnotationId.Element element = new AnnotationId.Element("elementInt", Integer.MAX_VALUE);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        int elementInt = ((MethodAnnotation)methodAnnotations[0]).elementInt();
        assertEquals(Integer.MAX_VALUE, elementInt);
    }

    /**
     *  Test adding a method annotation with new value of Long element.
     */
    @Test
    public void addMethodAnnotationWithLongElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.LONG);
        AnnotationId.Element element = new AnnotationId.Element("elementLong", Long.MAX_VALUE);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        long elementLong = ((MethodAnnotation)methodAnnotations[0]).elementLong();
        assertEquals(Long.MAX_VALUE, elementLong);
    }

    /**
     *  Test adding a method annotation with new value of Short element.
     */
    @Test
    public void addMethodAnnotationWithShortElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.SHORT);
        AnnotationId.Element element = new AnnotationId.Element("elementShort", Short.MAX_VALUE);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        short elementShort = ((MethodAnnotation)methodAnnotations[0]).elementShort();
        assertEquals(Short.MAX_VALUE, elementShort);
    }

    /**
     *  Test adding a method annotation with new value of String element.
     */
    @Test
    public void addMethodAnnotationWithStingElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.STRING);
        AnnotationId.Element element = new AnnotationId.Element("elementString", "hello");
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        String elementString = ((MethodAnnotation)methodAnnotations[0]).elementString();
        assertEquals("hello", elementString);
    }

    /**
     *  Test adding a method annotation with new value of Enum element.
     */
    @Test
    public void addMethodAnnotationWithEnumElement() throws Exception {
        assumeTrue(Build.VERSION.SDK_INT >= 21);

        MethodId<?, Void> methodId = generateVoidMethod(TypeId.get(Enum.class));
        AnnotationId.Element element = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        ElementEnum elementEnum = ((MethodAnnotation)methodAnnotations[0]).elementEnum();
        assertEquals(ElementEnum.INSTANCE_1, elementEnum);
    }

    /**
     *  Test adding a method annotation with new value of Class element.
     */
    @Test
    public void addMethodAnnotationWithClassElement() throws Exception {
        MethodId<?, Void> methodId = generateVoidMethod(TypeId.get(AnnotationId.class));
        AnnotationId.Element element = new AnnotationId.Element("elementClass", AnnotationId.class);
        addAnnotationToMethod(methodId, element);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        Class<?> elementClass = ((MethodAnnotation)methodAnnotations[0]).elementClass();
        assertEquals(AnnotationId.class, elementClass);
    }

    /**
     *  Test adding a method annotation with new multiple values of an element.
     */
    @Test
    public void addMethodAnnotationWithMultiElements() throws Exception {
        assumeTrue(Build.VERSION.SDK_INT >= 21);

        MethodId<?, Void> methodId = generateVoidMethod();
        AnnotationId.Element element1 = new AnnotationId.Element("elementClass", AnnotationId.class);
        AnnotationId.Element element2 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
        AnnotationId.Element[] elements = {element1, element2};
        addAnnotationToMethod(methodId, elements);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        ElementEnum elementEnum = ((MethodAnnotation)methodAnnotations[0]).elementEnum();
        assertEquals(ElementEnum.INSTANCE_1, elementEnum);
        Class<?> elementClass = ((MethodAnnotation)methodAnnotations[0]).elementClass();
        assertEquals(AnnotationId.class, elementClass);
    }

    /**
     *  Test adding a method annotation with duplicate values of an element. The previous value will
     *  be replaced by latter one.
     */
    @Test
    public void addMethodAnnotationWithDuplicateElements() throws Exception {
        assumeTrue(Build.VERSION.SDK_INT >= 21);

        MethodId<?, Void> methodId = generateVoidMethod();
        AnnotationId.Element element1 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_1);
        AnnotationId.Element element2 = new AnnotationId.Element("elementEnum", ElementEnum.INSTANCE_0);
        addAnnotationToMethod(methodId, element1, element2);

        Annotation[] methodAnnotations = getMethodAnnotations(methodId);
        assertEquals(methodAnnotations.length, 1);

        ElementEnum elementEnum = ((MethodAnnotation)methodAnnotations[0]).elementEnum();
        assertEquals(ElementEnum.INSTANCE_0, elementEnum);
    }


    /**
     *  Test adding a method annotation with new array value of an element. It's not supported yet.
     */
    @Test
    public void addMethodAnnotationWithArrayElementValue() {
        try {
            MethodId<?, Void> methodId = generateVoidMethod();
            int[] a = {1, 2};
            AnnotationId.Element element = new AnnotationId.Element("elementInt", a);
            addAnnotationToMethod(methodId, element);
            fail();
        } catch (UnsupportedOperationException e) {
            System.out.println(e);
        }
    }

    /**
     *  Test adding a method annotation with new TypeId value of an element. It's not supported yet.
     */
    @Test
    public void addMethodAnnotationWithTypeIdElementValue() {
        try {
            MethodId<?, Void> methodId = generateVoidMethod();
            AnnotationId.Element element = new AnnotationId.Element("elementInt", INT);
            addAnnotationToMethod(methodId, element);
            fail();
        } catch (UnsupportedOperationException e) {
            System.out.println(e);
        }
    }

    @After
    public void tearDown() {
    }

    /**
     *  Internal methods
     */
    private void init() {
        clearDataDirectory();

        dexMaker = new DexMaker();
        dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
    }

    private void clearDataDirectory() {
        for (File f : getDataDirectory().listFiles()) {
            if (f.getName().endsWith(".jar") || f.getName().endsWith(".dex")) {
                f.delete();
            }
        }
    }

    private static File getDataDirectory() {
        String dataDir = InstrumentationRegistry.getTargetContext().getApplicationInfo().dataDir;
        return new File(dataDir + "/cache" );
    }

    private MethodId<?, Void> generateVoidMethod(TypeId<?>... parameters) {
        MethodId<?, Void> methodId = GENERATED.getMethod(VOID, "call", parameters);
        Code code = dexMaker.declare(methodId, PUBLIC);
        code.returnVoid();
        return methodId;
    }

    private void addAnnotationToMethod(MethodId<?, Void> methodId, AnnotationId.Element... elements) {
        TypeId<MethodAnnotation> annotationTypeId = TypeId.get(MethodAnnotation.class);
        AnnotationId<?, MethodAnnotation> annotationId = AnnotationId.get(GENERATED, annotationTypeId, ElementType.METHOD);
        for (AnnotationId.Element element : elements) {
            annotationId.set(element);
        }
        annotationId.addToMethod(dexMaker, methodId);
    }

    private Annotation[] getMethodAnnotations(MethodId<?, Void> methodId) throws Exception {
        Class<?> generatedClass = generateAndLoad();
        Class<?>[] parameters = getMethodParameters(methodId);
        Method method = generatedClass.getMethod(methodId.getName(), parameters);
        return method.getAnnotations();
    }

    private Class<?>[] getMethodParameters(MethodId<?, Void> methodId) throws ClassNotFoundException {
        List<TypeId<?>> paras = methodId.getParameters();
        Class<?>[] p = null;
        if (paras.size() > 0) {
            p = new Class<?>[paras.size()];
            for (int i = 0; i < paras.size(); i++) {
                p[i] = TYPE_TO_PRIMITIVE.get(paras.get(i));
                if (p[i] == null) {
                    String name = paras.get(i).getName().replace('/', '.');
                    if (name.charAt(0) == 'L') {
                        name = name.substring(1, name.length()-1);
                    }
                    p[i] = Class.forName(name);
                }
            }
        }
        return p;
    }

    private Class<?> generateAndLoad() throws Exception {
        return dexMaker.generateAndLoad(getClass().getClassLoader(), getDataDirectory())
                .loadClass("Generated");
    }
}
