/*-
 * #%L
 * JSQLParser library
 * %%
 * Copyright (C) 2004 - 2020 JSQLParser
 * %%
 * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
 * #L%
 */
package net.sf.jsqlparser.util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.ArrayUtils;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Assumptions;
import org.opentest4j.TestAbortedException;

/**
 * @author gitmotte
 */
public class ReflectionTestUtils {

    public static final Predicate<Method> GETTER_METHODS = m -> !void.class.isAssignableFrom(m.getReturnType())
            && m.getParameterCount() == 0
            && (m.getName().startsWith("get") || m.getName().startsWith("is"));

    public static final Predicate<Method> SETTER_METHODS = m -> void.class.isAssignableFrom(m.getReturnType())
            && m.getParameterCount() == 1
            && m.getName().startsWith("set");

    public static final Predicate<Method> CHAINING_METHODS = m -> m.getDeclaringClass()
            .isAssignableFrom(m.getReturnType())
            // could be prefixed with "with" or not, does not matter
            && m.getParameterCount() == 1;

    /**
     * Testing of setters, getters, with-/add-methods by calling them with random parameter-values
     * <ul>
     * <li>testing, whether return-value is the specific type (not the parent)
     * <li>testing, whether calling the methods do not throw any exceptions
     * </ul>
     *
     * @param objs
     * @param testMethodFilter - additional filter to skip some methods (by returning <code>false</code>).
     * Default-Filters: null     {@link #notDeclaredInObjectClass(Method)},
     *                         {@link #GETTER_METHODS}, {@link #SETTER_METHODS},
     *                         {@link #CHAINING_METHODS}
     */
    @SafeVarargs
    public static void testGetterSetterChaining(List<Object> objs, Predicate<Method>... testMethodFilter) {
        RandomUtils.pushObjects(objs);
        objs.forEach(o -> {
            testMethodInvocation(o, ReflectionTestUtils::anyReturnType, ReflectionTestUtils::reflectiveNonNullArgs,
                    ArrayUtils.insert(0, testMethodFilter, GETTER_METHODS, ReflectionTestUtils::notDeclaredInObjectClass));
            testMethodInvocation(o, ReflectionTestUtils::noReturnTypeValid, ReflectionTestUtils::reflectiveNonNullArgs,
                    ArrayUtils.insert(0, testMethodFilter, SETTER_METHODS, ReflectionTestUtils::notDeclaredInObjectClass));
            testMethodInvocation(o, ReflectionTestUtils::returnTypeThis, ReflectionTestUtils::reflectiveNonNullArgs,
                    ArrayUtils.insert(0, testMethodFilter, CHAINING_METHODS, ReflectionTestUtils::notDeclaredInObjectClass));
        });
    }

    private static boolean notDeclaredInObjectClass(Method m) {
        return !Object.class.equals(m.getDeclaringClass());
    }

    private static Object[] reflectiveNonNullArgs(Method m) {
        List<Object> params = new ArrayList<>();
        for (Parameter p : m.getParameters()) {
            Class<?> type = p.getType();
            Object value = RandomUtils.getRandomValueForType(type);
            Assumptions.assumeTrue(value != null, "cannot get random value for type " + type);
            params.add(value);
        }
        return params.toArray();
    }

    /**
     * @param returnValue
     * @param m
     * @return <code>true</code>, if the return-type is equals the method-declaring class
     */
    private static boolean returnTypeThis(Object returnValue, Method m) {
        return returnValue != null && m.getDeclaringClass().equals(returnValue.getClass());
    }

    /**
     * @param returnValue
     * @param m
     * @return always <code>true</code>
     */
    private static boolean anyReturnType(Object returnValue, Method m) {
        return true;
    }

    /**
     * @param returnValue
     * @param m
     * @return <code>true</code>, if returnValue is <code>null</code>
     */
    private static boolean noReturnTypeValid(Object returnValue, Method m) {
        return returnValue == null;
    }

    /**
     * @param object
     * @param argsFunction
     * @param methodFilters
     */
    @SafeVarargs
    public static void testMethodInvocation(Object object, BiPredicate<Object, Method> returnTypeCheck,
            Function<Method, Object[]> argsFunction,
            Predicate<Method>... methodFilters) {
        log(Level.INFO, "testing methods of class " + object.getClass());
        for (Method m : object.getClass().getMethods()) {
            boolean testMethod = true;
            for (Predicate<Method> f : methodFilters) {
                if (!f.test(m)) {
                    log(Level.FINE, "skip method " + m.toGenericString());
                    testMethod = false;
                    break;
                }
            }
            if (testMethod) {
                log(Level.INFO, "testing method " + m.toGenericString());
                try {
                    invoke(m, returnTypeCheck, argsFunction, object);
                } catch (Exception e) {
                    assertFalse(
                            false,
                            String.format("%s throws on invocation on object: %s", m.toGenericString(),
                                    object.getClass()));
                }
            }
        }
    }

    /**
     * Invoke one method of given object with args provided by #argsFunction, and test it's return-value
     *
     * @param method
     * @param returnValueCheck
     * @param argsFunction
     * @param object
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public static void invoke(Method method, BiPredicate<Object, Method> returnValueCheck,
            Function<Method, Object[]> argsFunction,
            Object object)
            throws IllegalAccessException, InvocationTargetException {
        try {
            Object returnValue = method.invoke(object, argsFunction.apply(method));
            if (!void.class.isAssignableFrom(method.getReturnType())) {
                assertTrue(returnValueCheck.test(returnValue, method), "unexpected return-value with type " + returnValue.getClass() + " for method "
                        + method.toGenericString());
            }
        } catch (TestAbortedException tae) {
            log(Level.INFO, "skip methods " + method.toGenericString() + ", detail: " + tae.getMessage());
        }
    }

    private static void log(Level level, String string) {
        if (Logger.getAnonymousLogger().isLoggable(level)) {
            System.out.println(string);
        }
    }

}
