///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2023 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.internal.utils;

import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;

import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.DetailAST;

public final class TestUtil {

    /**
     * The stack size used in {@link TestUtil#getResultWithLimitedResources}.
     * This value should be as small as possible. Some JVM requires this value to be
     * at least 144k.
     *
     * @see <a href="https://www.baeldung.com/jvm-configure-stack-sizes">
     *      Configuring Stack Sizes in the JVM</a>
     */
    private static final int MINIMAL_STACK_SIZE = 147456;

    private TestUtil() {
    }

    /**
     * Verifies that utils class has private constructor and invokes it to satisfy code coverage.
     *
     * @param utilClass class to test for c-tor
     * @return true if constructor is expected.
     */
    public static boolean isUtilsClassHasPrivateConstructor(final Class<?> utilClass)
            throws ReflectiveOperationException {
        final Constructor<?> constructor = utilClass.getDeclaredConstructor();
        final boolean result = Modifier.isPrivate(constructor.getModifiers());
        constructor.setAccessible(true);
        constructor.newInstance();
        return result;
    }

    /**
     * Retrieves the specified field by its name in the class or its direct super.
     *
     * @param clss the class to retrieve the field for
     * @param fieldName the name of the field to retrieve
     * @return the class' field if found
     */
    public static Field getClassDeclaredField(Class<?> clss, String fieldName) {
        return Stream.<Class<?>>iterate(clss, Class::getSuperclass)
            .flatMap(cls -> Arrays.stream(cls.getDeclaredFields()))
            .filter(field -> fieldName.equals(field.getName()))
            .findFirst()
            .map(field -> {
                field.setAccessible(true);
                return field;
            })
            .orElseThrow(() -> {
                return new IllegalStateException(String.format(Locale.ROOT,
                        "Field '%s' not found in '%s'", fieldName, clss.getCanonicalName()));
            });
    }

    /**
     * Retrieves the specified method by its name in the class or its direct super.
     *
     * @param clss the class to retrieve the method for
     * @param methodName the name of the method to retrieve
     * @param parameters the expected number of parameters
     * @return the class' method
     */
    public static Method getClassDeclaredMethod(Class<?> clss, String methodName, int parameters) {
        return Stream.<Class<?>>iterate(clss, Class::getSuperclass)
            .flatMap(cls -> Arrays.stream(cls.getDeclaredMethods()))
            .filter(method -> {
                return methodName.equals(method.getName())
                    && parameters == method.getParameterCount();
            })
            .findFirst()
            .map(method -> {
                method.setAccessible(true);
                return method;
            })
            .orElseThrow(() -> {
                return new IllegalStateException(String.format(Locale.ROOT,
                        "Method '%s' with %d parameters not found in '%s'",
                        methodName, parameters, clss.getCanonicalName()));
            });
    }

    /**
     * Checks if stateful field is cleared during {@link AbstractCheck#beginTree} in check.
     *
     * @param check      check object which field is to be verified
     * @param astToVisit ast to pass into check methods
     * @param fieldName  name of the field to be checked
     * @param isClear    function for checking field state
     * @return {@code true} if state of the field is cleared
     */
    public static boolean isStatefulFieldClearedDuringBeginTree(AbstractCheck check,
                                                                DetailAST astToVisit,
                                                                String fieldName,
                                                                Predicate<Object> isClear) {
        check.beginTree(astToVisit);
        check.visitToken(astToVisit);
        check.beginTree(null);
        return isClear.test(getInternalState(check, fieldName));
    }

    /**
     * Checks if stateful field is cleared during {@link AbstractAutomaticBean}'s finishLocalSetup.
     *
     * @param filter filter object which field is to be verified
     * @param event event to pass into filter methods
     * @param fieldName name of the field to be checked
     * @param isClear function for checking field state
     * @return {@code true} if state of the field is cleared
     * @throws Exception if there was an error.
     */
    public static boolean isStatefulFieldClearedDuringLocalSetup(
            TreeWalkerFilter filter, TreeWalkerAuditEvent event,
            String fieldName, Predicate<Object> isClear) throws Exception {
        filter.accept(event);
        invokeMethod(filter, "finishLocalSetup");
        final Field resultField = getClassDeclaredField(filter.getClass(), fieldName);
        return isClear.test(resultField.get(filter));
    }

    /**
     * Returns the default PackageObjectFactory with the default package names.
     *
     * @return the default PackageObjectFactory.
     */
    public static PackageObjectFactory getPackageObjectFactory() throws CheckstyleException {
        final ClassLoader cl = TestUtil.class.getClassLoader();
        final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
        return new PackageObjectFactory(packageNames, cl);
    }

    /**
     * Finds node of specified type among root children, siblings, siblings children
     * on any deep level.
     *
     * @param root      DetailAST
     * @param predicate predicate
     * @return {@link Optional} of {@link DetailAST} node which matches the predicate.
     */
    public static Optional<DetailAST> findTokenInAstByPredicate(DetailAST root,
                                                                Predicate<DetailAST> predicate) {
        DetailAST curNode = root;
        while (!predicate.test(curNode)) {
            DetailAST toVisit = curNode.getFirstChild();
            while (curNode != null && toVisit == null) {
                toVisit = curNode.getNextSibling();
                if (toVisit == null) {
                    curNode = curNode.getParent();
                }
            }

            if (curNode == toVisit || curNode == root.getParent()) {
                curNode = null;
                break;
            }

            curNode = toVisit;
        }
        return Optional.ofNullable(curNode);
    }

    /**
     * <p>
     * Returns the JDK version as a number that is easy to compare.
     * </p>
     * <p>
     * For JDK "1.8" it will be 8; for JDK "11" it will be 11.
     * </p>
     *
     * @return JDK version as integer
     */
    public static int getJdkVersion() {
        String version = System.getProperty("java.specification.version");
        if (version.startsWith("1.")) {
            version = version.substring(2);
        }
        return Integer.parseInt(version);
    }

    /**
     * <p>
     * Adjusts the expected number of flushes for tests that call {@link OutputStream#close} method.
     * </p>
     * <p>
     * After <a href="https://bugs.openjdk.java.net/browse/JDK-8220477">JDK-8220477</a>
     * there is one additional flush from {@code sun.nio.cs.StreamEncoder#implClose}.
     * </p>
     *
     * @param flushCount flush count to adjust
     * @return adjusted flush count
     */
    public static int adjustFlushCountForOutputStreamClose(int flushCount) {
        int result = flushCount;
        if (getJdkVersion() >= 13) {
            ++result;
        }
        return result;
    }

    /**
     * Runs a given task with limited stack size and time duration, then
     * returns the result. See AbstractModuleTestSupport#verifyWithLimitedResources
     * for an example of how to use this method when task does not return a result, i.e.
     * the given method's return type is {@code void}.
     *
     * @param callable the task to execute
     * @param <V> return type of task - {@code Void} if task does not return result
     * @return result
     * @throws Exception if getting result fails
     */
    public static <V> V getResultWithLimitedResources(Callable<V> callable) throws Exception {
        final FutureTask<V> futureTask = new FutureTask<>(callable);
        final Thread thread = new Thread(null, futureTask,
                "LimitedStackSizeThread", MINIMAL_STACK_SIZE);
        thread.start();
        return futureTask.get(10, TimeUnit.SECONDS);
    }

    /**
     * Reads the value of a field using reflection. This method will traverse the
     * super class hierarchy until a field with name {@code fieldName} is found.
     *
     * @param instance the instance to read
     * @param fieldName the name of the field
     * @throws RuntimeException if the field  can't be read
     * @noinspection unchecked
     * @noinspectionreason unchecked - unchecked cast is ok on test code
     */
    public static <T> T getInternalState(Object instance, String fieldName) {
        try {
            final Field field = getClassDeclaredField(instance.getClass(), fieldName);
            return (T) field.get(instance);
        }
        catch (ReflectiveOperationException ex) {
            final String message = String.format(Locale.ROOT,
                    "Failed to get field '%s' for instance of class '%s'",
                    fieldName, instance.getClass().getSimpleName());
            throw new IllegalStateException(message, ex);
        }
    }

    /**
     * Reads the value of a static field using reflection. This method will traverse the
     * super class hierarchy until a field with name {@code fieldName} is found.
     *
     * @param clss the class of the field
     * @param fieldName the name of the field
     * @throws RuntimeException if the field  can't be read
     * @noinspection unchecked
     * @noinspectionreason unchecked - unchecked cast is ok on test code
     */
    public static <T> T getInternalStaticState(Class<?> clss, String fieldName) {
        try {
            final Field field = getClassDeclaredField(clss, fieldName);
            return (T) field.get(null);
        }
        catch (ReflectiveOperationException ex) {
            final String message = String.format(Locale.ROOT,
                    "Failed to get static field '%s' for class '%s'",
                    fieldName, clss);
            throw new IllegalStateException(message, ex);
        }
    }

    /**
     * Writes the value of a field using reflection. This method will traverse the
     * super class hierarchy until a field with name {@code fieldName} is found.
     *
     * @param instance the instance whose field to modify
     * @param fieldName the name of the field
     * @param value the new value of the field
     * @throws RuntimeException if the field  can't be changed
     */
    public static void setInternalState(Object instance, String fieldName, Object value) {
        try {
            final Field field = getClassDeclaredField(instance.getClass(), fieldName);
            field.set(instance, value);
        }
        catch (ReflectiveOperationException ex) {
            final String message = String.format(Locale.ROOT,
                    "Failed to set field '%s' for instance of class '%s'",
                    fieldName, instance.getClass().getSimpleName());
            throw new IllegalStateException(message, ex);
        }
    }

    /**
     * Invokes a private method for an instance.
     *
     * @param instance the instance whose method to invoke
     * @param methodToExecute the name of the method to invoke
     * @param arguments the optional arguments
     * @param <T> the type of the result
     * @return the method's result
     * @throws ReflectiveOperationException if the method invocation failed
     * @noinspection unchecked
     * @noinspectionreason unchecked - unchecked cast is ok on test code
     */
    public static <T> T invokeMethod(Object instance,
            String methodToExecute, Object... arguments) throws ReflectiveOperationException {
        final Class<?> clss = instance.getClass();
        final Method method = getClassDeclaredMethod(clss, methodToExecute, arguments.length);
        return (T) method.invoke(instance, arguments);
    }

    /**
     * Invokes a static private method for a class.
     *
     * @param clss the class whose static method to invoke
     * @param methodToExecute the name of the method to invoke
     * @param arguments the optional arguments
     * @param <T> the type of the result
     * @return the method's result
     * @throws ReflectiveOperationException if the method invocation failed
     * @noinspection unchecked
     * @noinspectionreason unchecked - unchecked cast is ok on test code
     */
    public static <T> T invokeStaticMethod(Class<?> clss,
            String methodToExecute, Object... arguments) throws ReflectiveOperationException {
        final Method method = getClassDeclaredMethod(clss, methodToExecute, arguments.length);
        return (T) method.invoke(null, arguments);
    }

    /**
     * Returns the inner class type by its name.
     *
     * @param declaringClass the class in which the inner class is declared
     * @param name the unqualified name (simple name) of the inner class
     * @return the inner class type
     * @throws ClassNotFoundException if the class not found
     * @noinspection unchecked
     * @noinspectionreason unchecked - unchecked cast is ok on test code
     */
    public static <T> Class<T> getInnerClassType(Class<?> declaringClass, String name)
            throws ClassNotFoundException {
        return (Class<T>) Class.forName(declaringClass.getName() + "$" + name);
    }

}
