/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.dubbo.common.utils;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasKey;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

class ReflectUtilsTest {
    @Test
    void testIsPrimitives() {
        assertTrue(ReflectUtils.isPrimitives(boolean[].class));
        assertTrue(ReflectUtils.isPrimitives(byte.class));
        assertFalse(ReflectUtils.isPrimitive(Map[].class));
    }

    @Test
    void testIsPrimitive() {
        assertTrue(ReflectUtils.isPrimitive(boolean.class));
        assertTrue(ReflectUtils.isPrimitive(String.class));
        assertTrue(ReflectUtils.isPrimitive(Boolean.class));
        assertTrue(ReflectUtils.isPrimitive(Character.class));
        assertTrue(ReflectUtils.isPrimitive(Number.class));
        assertTrue(ReflectUtils.isPrimitive(Date.class));
        assertFalse(ReflectUtils.isPrimitive(Map.class));
    }

    @Test
    void testGetBoxedClass() {
        assertThat(ReflectUtils.getBoxedClass(int.class), sameInstance(Integer.class));
        assertThat(ReflectUtils.getBoxedClass(boolean.class), sameInstance(Boolean.class));
        assertThat(ReflectUtils.getBoxedClass(long.class), sameInstance(Long.class));
        assertThat(ReflectUtils.getBoxedClass(float.class), sameInstance(Float.class));
        assertThat(ReflectUtils.getBoxedClass(double.class), sameInstance(Double.class));
        assertThat(ReflectUtils.getBoxedClass(char.class), sameInstance(Character.class));
        assertThat(ReflectUtils.getBoxedClass(byte.class), sameInstance(Byte.class));
        assertThat(ReflectUtils.getBoxedClass(short.class), sameInstance(Short.class));
        assertThat(ReflectUtils.getBoxedClass(String.class), sameInstance(String.class));
    }

    @Test
    void testIsCompatible() {
        assertTrue(ReflectUtils.isCompatible(short.class, (short) 1));
        assertTrue(ReflectUtils.isCompatible(int.class, 1));
        assertTrue(ReflectUtils.isCompatible(double.class, 1.2));
        assertTrue(ReflectUtils.isCompatible(Object.class, 1.2));
        assertTrue(ReflectUtils.isCompatible(List.class, new ArrayList<String>()));
    }

    @Test
    void testIsCompatibleWithArray() {
        assertFalse(ReflectUtils.isCompatible(new Class[]{short.class, int.class}, new Object[]{(short) 1}));
        assertFalse(ReflectUtils.isCompatible(new Class[]{double.class}, new Object[]{"hello"}));
        assertTrue(ReflectUtils.isCompatible(new Class[]{double.class}, new Object[]{1.2}));
    }

    @Test
    void testGetCodeBase() {
        assertNull(ReflectUtils.getCodeBase(null));
        assertNull(ReflectUtils.getCodeBase(String.class));
        assertNotNull(ReflectUtils.getCodeBase(ReflectUtils.class));
    }

    @Test
    void testGetName() {
        // getName
        assertEquals("boolean", ReflectUtils.getName(boolean.class));
        assertEquals("int[][][]", ReflectUtils.getName(int[][][].class));
        assertEquals("java.lang.Object[][]", ReflectUtils.getName(Object[][].class));
    }

    @Test
    void testGetDesc() {
        // getDesc
        assertEquals("Z", ReflectUtils.getDesc(boolean.class));
        assertEquals("[[[I", ReflectUtils.getDesc(int[][][].class));
        assertEquals("[[Ljava/lang/Object;", ReflectUtils.getDesc(Object[][].class));
    }

    @Test
    void testName2desc() {
        // name2desc
        assertEquals("Z", ReflectUtils.name2desc(ReflectUtils.getName(boolean.class)));
        assertEquals("[[[I", ReflectUtils.name2desc(ReflectUtils.getName(int[][][].class)));
        assertEquals("[[Ljava/lang/Object;", ReflectUtils.name2desc(ReflectUtils.getName(Object[][].class)));
    }

    @Test
    void testDesc2name() {
        // desc2name
        assertEquals("short[]", ReflectUtils.desc2name(ReflectUtils.getDesc(short[].class)));
        assertEquals("boolean[]", ReflectUtils.desc2name(ReflectUtils.getDesc(boolean[].class)));
        assertEquals("byte[]", ReflectUtils.desc2name(ReflectUtils.getDesc(byte[].class)));
        assertEquals("char[]", ReflectUtils.desc2name(ReflectUtils.getDesc(char[].class)));
        assertEquals("double[]", ReflectUtils.desc2name(ReflectUtils.getDesc(double[].class)));
        assertEquals("float[]", ReflectUtils.desc2name(ReflectUtils.getDesc(float[].class)));
        assertEquals("int[]", ReflectUtils.desc2name(ReflectUtils.getDesc(int[].class)));
        assertEquals("long[]", ReflectUtils.desc2name(ReflectUtils.getDesc(long[].class)));
        assertEquals("int", ReflectUtils.desc2name(ReflectUtils.getDesc(int.class)));
        assertEquals("void", ReflectUtils.desc2name(ReflectUtils.getDesc(void.class)));
        assertEquals("java.lang.Object[][]", ReflectUtils.desc2name(ReflectUtils.getDesc(Object[][].class)));
    }

    @Test
    void testGetGenericClass() {
        assertThat(ReflectUtils.getGenericClass(Foo1.class), sameInstance(String.class));
    }

    @Test
    void testGetGenericClassWithIndex() {
        assertThat(ReflectUtils.getGenericClass(Foo1.class, 0), sameInstance(String.class));
        assertThat(ReflectUtils.getGenericClass(Foo1.class, 1), sameInstance(Integer.class));
        assertThat(ReflectUtils.getGenericClass(Foo2.class, 0), sameInstance(List.class));
        assertThat(ReflectUtils.getGenericClass(Foo2.class, 1), sameInstance(int.class));
        assertThat(ReflectUtils.getGenericClass(Foo3.class, 0), sameInstance(Foo1.class));
        assertThat(ReflectUtils.getGenericClass(Foo3.class, 1), sameInstance(Foo2.class));
    }

    @Test
    void testGetMethodName() throws Exception {
        assertThat(ReflectUtils.getName(Foo2.class.getDeclaredMethod("hello", int[].class)),
            equalTo("java.util.List hello(int[])"));
    }

    @Test
    void testGetSignature() throws Exception {
        Method m = Foo2.class.getDeclaredMethod("hello", int[].class);
        assertThat(ReflectUtils.getSignature("greeting", m.getParameterTypes()), equalTo("greeting([I)"));
    }

    @Test
    void testGetConstructorName() {
        Constructor c = Foo2.class.getConstructors()[0];
        assertThat(ReflectUtils.getName(c), equalTo("(java.util.List,int[])"));
    }

    @Test
    void testName2Class() throws Exception {
        assertEquals(boolean.class, ReflectUtils.name2class("boolean"));
        assertEquals(boolean[].class, ReflectUtils.name2class("boolean[]"));
        assertEquals(int[][].class, ReflectUtils.name2class(ReflectUtils.getName(int[][].class)));
        assertEquals(ReflectUtilsTest[].class, ReflectUtils.name2class(ReflectUtils.getName(ReflectUtilsTest[].class)));
    }

    @Test
    void testGetDescMethod() throws Exception {
        assertThat(ReflectUtils.getDesc(Foo2.class.getDeclaredMethod("hello", int[].class)),
            equalTo("hello([I)Ljava/util/List;"));
    }

    @Test
    void testGetDescConstructor() {
        assertThat(ReflectUtils.getDesc(Foo2.class.getConstructors()[0]), equalTo("(Ljava/util/List;[I)V"));
    }

    @Test
    void testGetDescWithoutMethodName() throws Exception {
        assertThat(ReflectUtils.getDescWithoutMethodName(Foo2.class.getDeclaredMethod("hello", int[].class)),
            equalTo("([I)Ljava/util/List;"));
    }

    @Test
    void testFindMethodByMethodName1() throws Exception {
        assertNotNull(ReflectUtils.findMethodByMethodName(Foo.class, "hello"));
    }

    @Test
    void testFindMethodByMethodName2() {
        Assertions.assertThrows(IllegalStateException.class, () -> {
            ReflectUtils.findMethodByMethodName(Foo2.class, "hello");
        });

    }

    @Test
    void testFindConstructor() throws Exception {
        Constructor constructor = ReflectUtils.findConstructor(Foo3.class, Foo2.class);
        assertNotNull(constructor);
    }

    @Test
    void testIsInstance() {
        assertTrue(ReflectUtils.isInstance(new Foo1(), Foo.class.getName()));
    }

    @Test
    void testIsBeanPropertyReadMethod() throws Exception {
        Method method = EmptyClass.class.getMethod("getProperty");
        assertTrue(ReflectUtils.isBeanPropertyReadMethod(method));
        method = EmptyClass.class.getMethod("getProperties");
        assertFalse(ReflectUtils.isBeanPropertyReadMethod(method));
        method = EmptyClass.class.getMethod("isProperty");
        assertFalse(ReflectUtils.isBeanPropertyReadMethod(method));
        method = EmptyClass.class.getMethod("getPropertyIndex", int.class);
        assertFalse(ReflectUtils.isBeanPropertyReadMethod(method));
    }

    @Test
    void testGetPropertyNameFromBeanReadMethod() throws Exception {
        Method method = EmptyClass.class.getMethod("getProperty");
        assertEquals("property", ReflectUtils.getPropertyNameFromBeanReadMethod(method));
        method = EmptyClass.class.getMethod("isSet");
        assertEquals("set", ReflectUtils.getPropertyNameFromBeanReadMethod(method));
    }

    @Test
    void testIsBeanPropertyWriteMethod() throws Exception {
        Method method = EmptyClass.class.getMethod("setProperty", EmptyProperty.class);
        assertTrue(ReflectUtils.isBeanPropertyWriteMethod(method));
        method = EmptyClass.class.getMethod("setSet", boolean.class);
        assertTrue(ReflectUtils.isBeanPropertyWriteMethod(method));
    }

    @Test
    void testGetPropertyNameFromBeanWriteMethod() throws Exception {
        Method method = EmptyClass.class.getMethod("setProperty", EmptyProperty.class);
        assertEquals("property", ReflectUtils.getPropertyNameFromBeanWriteMethod(method));
    }

    @Test
    void testIsPublicInstanceField() throws Exception {
        Field field = EmptyClass.class.getDeclaredField("set");
        assertTrue(ReflectUtils.isPublicInstanceField(field));
        field = EmptyClass.class.getDeclaredField("property");
        assertFalse(ReflectUtils.isPublicInstanceField(field));
    }

    @Test
    void testGetBeanPropertyFields() {
        Map<String, Field> map = ReflectUtils.getBeanPropertyFields(EmptyClass.class);
        assertThat(map.size(), is(2));
        assertThat(map, hasKey("set"));
        assertThat(map, hasKey("property"));
        for (Field f : map.values()) {
            if (!f.isAccessible()) {
                fail();
            }
        }
    }

    @Test
    void testGetBeanPropertyReadMethods() {
        Map<String, Method> map = ReflectUtils.getBeanPropertyReadMethods(EmptyClass.class);
        assertThat(map.size(), is(2));
        assertThat(map, hasKey("set"));
        assertThat(map, hasKey("property"));
        for (Method m : map.values()) {
            if (!m.isAccessible()) {
                fail();
            }
        }
    }

    @Test
    void testDesc2Class() throws Exception {
        assertEquals(void.class, ReflectUtils.desc2class("V"));
        assertEquals(boolean.class, ReflectUtils.desc2class("Z"));
        assertEquals(boolean[].class, ReflectUtils.desc2class("[Z"));
        assertEquals(byte.class, ReflectUtils.desc2class("B"));
        assertEquals(char.class, ReflectUtils.desc2class("C"));
        assertEquals(double.class, ReflectUtils.desc2class("D"));
        assertEquals(float.class, ReflectUtils.desc2class("F"));
        assertEquals(int.class, ReflectUtils.desc2class("I"));
        assertEquals(long.class, ReflectUtils.desc2class("J"));
        assertEquals(short.class, ReflectUtils.desc2class("S"));
        assertEquals(String.class, ReflectUtils.desc2class("Ljava.lang.String;"));
        assertEquals(int[][].class, ReflectUtils.desc2class(ReflectUtils.getDesc(int[][].class)));
        assertEquals(ReflectUtilsTest[].class, ReflectUtils.desc2class(ReflectUtils.getDesc(ReflectUtilsTest[].class)));

        String desc;
        Class<?>[] cs;

        cs = new Class<?>[]{int.class, getClass(), String.class, int[][].class, boolean[].class};
        desc = ReflectUtils.getDesc(cs);
        assertSame(cs, ReflectUtils.desc2classArray(desc));

        cs = new Class<?>[]{};
        desc = ReflectUtils.getDesc(cs);
        assertSame(cs, ReflectUtils.desc2classArray(desc));

        cs = new Class<?>[]{void.class, String[].class, int[][].class, ReflectUtilsTest[][].class};
        desc = ReflectUtils.getDesc(cs);
        assertSame(cs, ReflectUtils.desc2classArray(desc));
    }

    protected void assertSame(Class<?>[] cs1, Class<?>[] cs2) throws Exception {
        assertEquals(cs1.length, cs2.length);
        for (int i = 0; i < cs1.length; i++)
            assertEquals(cs1[i], cs2[i]);
    }

    @Test
    void testFindMethodByMethodSignature() throws Exception {
        Method m = ReflectUtils.findMethodByMethodSignature(TestedClass.class, "method1", null);

        assertEquals("method1", m.getName());
        Class<?>[] parameterTypes = m.getParameterTypes();
        assertEquals(1, parameterTypes.length);
        assertEquals(int.class, parameterTypes[0]);
    }

    @Test
    void testFindMethodByMethodSignature_override() throws Exception {
        {
            Method m = ReflectUtils.findMethodByMethodSignature(TestedClass.class,
                "overrideMethod", new String[]{"int"});

            assertEquals("overrideMethod", m.getName());
            Class<?>[] parameterTypes = m.getParameterTypes();
            assertEquals(1, parameterTypes.length);
            assertEquals(int.class, parameterTypes[0]);
        }
        {
            Method m = ReflectUtils.findMethodByMethodSignature(TestedClass.class,
                "overrideMethod", new String[]{"java.lang.Integer"});

            assertEquals("overrideMethod", m.getName());
            Class<?>[] parameterTypes = m.getParameterTypes();
            assertEquals(1, parameterTypes.length);
            assertEquals(Integer.class, parameterTypes[0]);
        }
    }

    @Test
    void testFindMethodByMethodSignatureOverrideMoreThan1() throws Exception {
        try {
            ReflectUtils.findMethodByMethodSignature(TestedClass.class, "overrideMethod", null);
            fail();
        } catch (IllegalStateException expected) {
            assertThat(expected.getMessage(), containsString("Not unique method for method name("));
        }
    }

    @Test
    void testFindMethodByMethodSignatureNotFound() throws Exception {
        try {
            ReflectUtils.findMethodByMethodSignature(TestedClass.class, "doesNotExist", null);
            fail();
        } catch (NoSuchMethodException expected) {
            assertThat(expected.getMessage(), containsString("No such method "));
            assertThat(expected.getMessage(), containsString("in class"));
        }
    }

    @Test
    void testGetEmptyObject() {
        assertTrue(ReflectUtils.getEmptyObject(Collection.class) instanceof Collection);
        assertTrue(ReflectUtils.getEmptyObject(List.class) instanceof List);
        assertTrue(ReflectUtils.getEmptyObject(Set.class) instanceof Set);
        assertTrue(ReflectUtils.getEmptyObject(Map.class) instanceof Map);
        assertTrue(ReflectUtils.getEmptyObject(Object[].class) instanceof Object[]);

        assertEquals("", ReflectUtils.getEmptyObject(String.class));
        assertEquals((short) 0, ReflectUtils.getEmptyObject(short.class));
        assertEquals((byte) 0, ReflectUtils.getEmptyObject(byte.class));
        assertEquals(0, ReflectUtils.getEmptyObject(int.class));
        assertEquals(0L, ReflectUtils.getEmptyObject(long.class));
        assertEquals((float) 0, ReflectUtils.getEmptyObject(float.class));
        assertEquals((double) 0, ReflectUtils.getEmptyObject(double.class));
        assertEquals('\0', ReflectUtils.getEmptyObject(char.class));
        assertEquals(Boolean.FALSE, ReflectUtils.getEmptyObject(boolean.class));

        EmptyClass object = (EmptyClass) ReflectUtils.getEmptyObject(EmptyClass.class);
        assertNotNull(object);
        assertNotNull(object.getProperty());
    }

    @Test
    void testForName1() {
        assertThat(ReflectUtils.forName(ReflectUtils.class.getName()), sameInstance(ReflectUtils.class));
    }

    @Test
    void testForName2() {
        Assertions.assertThrows(IllegalStateException.class, () -> {
            ReflectUtils.forName("a.c.d.e.F");
        });
    }

    @Test
    void testGetReturnTypes() throws Exception {
        Class<TypeClass> clazz = TypeClass.class;

        Type[] types = ReflectUtils.getReturnTypes(clazz.getMethod("getFuture"));
        Assertions.assertEquals("java.lang.String", types[0].getTypeName());
        Assertions.assertEquals("java.lang.String", types[1].getTypeName());

        Type[] types1 = ReflectUtils.getReturnTypes(clazz.getMethod("getString"));
        Assertions.assertEquals("java.lang.String", types1[0].getTypeName());
        Assertions.assertEquals("java.lang.String", types1[1].getTypeName());

        Type[] types2 = ReflectUtils.getReturnTypes(clazz.getMethod("getT"));
        Assertions.assertEquals("java.lang.String", types2[0].getTypeName());
        Assertions.assertEquals("T", types2[1].getTypeName());

        Type[] types3 = ReflectUtils.getReturnTypes(clazz.getMethod("getS"));
        Assertions.assertEquals("java.lang.Object", types3[0].getTypeName());
        Assertions.assertEquals("S", types3[1].getTypeName());

        Type[] types4 = ReflectUtils.getReturnTypes(clazz.getMethod("getListFuture"));
        Assertions.assertEquals("java.util.List", types4[0].getTypeName());
        Assertions.assertEquals("java.util.List<java.lang.String>", types4[1].getTypeName());

        Type[] types5 = ReflectUtils.getReturnTypes(clazz.getMethod("getGenericWithUpperFuture"));
        // T extends String, the first arg should be the upper bound of param
        Assertions.assertEquals("java.lang.String", types5[0].getTypeName());
        Assertions.assertEquals("T", types5[1].getTypeName());

        Type[] types6 = ReflectUtils.getReturnTypes(clazz.getMethod("getGenericFuture"));
        // default upper bound is Object
        Assertions.assertEquals("java.lang.Object", types6[0].getTypeName());
        Assertions.assertEquals("S", types6[1].getTypeName());
    }

    public interface TypeClass<T extends String, S> {

        CompletableFuture<String> getFuture();

        String getString();

        T getT();

        S getS();

        CompletableFuture<List<String>> getListFuture();

        CompletableFuture<T> getGenericWithUpperFuture();

        CompletableFuture<S> getGenericFuture();
    }

    public static class EmptyClass {
        private EmptyProperty property;
        public boolean set;
        public static String s;
        private transient int i;

        public EmptyProperty getProperty() {
            return property;
        }

        public EmptyProperty getPropertyIndex(int i) {
            return property;
        }

        public static EmptyProperty getProperties() {
            return null;
        }

        public void isProperty() {

        }

        public boolean isSet() {
            return set;
        }

        public void setProperty(EmptyProperty property) {
            this.property = property;
        }

        public void setSet(boolean set) {
            this.set = set;
        }
    }

    public static class EmptyProperty {
    }

    static class TestedClass {
        public void method1(int x) {
        }

        public void overrideMethod(int x) {
        }

        public void overrideMethod(Integer x) {
        }

        public void overrideMethod(String s) {
        }

        public void overrideMethod(String s1, String s2) {
        }
    }


    interface Foo<A, B> {
        A hello(B b);
    }

    static class Foo1 implements Foo<String, Integer> {
        @Override
        public String hello(Integer integer) {
            return null;
        }
    }

    static class Foo2 implements Foo<List<String>, int[]> {
        public Foo2(List<String> list, int[] ints) {
        }

        @Override
        public List<String> hello(int[] ints) {
            return null;
        }
    }

    static class Foo3 implements Foo<Foo1, Foo2> {
        public Foo3(Foo foo) {
        }

        @Override
        public Foo1 hello(Foo2 foo2) {
            return null;
        }
    }
}
