/*
 * 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.ignite.internal.testframework;

import static java.lang.Thread.sleep;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.WRITE;
import static java.util.function.Function.identity;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.ignite.internal.lang.IgniteInternalException;
import org.apache.ignite.internal.lang.IgniteStringFormatter;
import org.apache.ignite.internal.lang.RunnableX;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
import org.apache.ignite.internal.thread.IgniteThreadFactory;
import org.apache.ignite.internal.thread.NamedThreadFactory;
import org.apache.ignite.internal.thread.ThreadOperation;
import org.apache.ignite.internal.util.ExceptionUtils;
import org.apache.ignite.lang.IgniteException;
import org.hamcrest.CustomMatcher;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.function.Executable;

/**
 * Utility class for tests.
 */
public final class IgniteTestUtils {
    private static final IgniteLogger LOG = Loggers.forClass(IgniteTestUtils.class);

    private static final int TIMEOUT_SEC = 30;

    /**
     * Set object field value via reflection.
     *
     * @param obj       Object to set field value to.
     * @param fieldName Field name to set value for.
     * @param val       New field value.
     * @throws IgniteInternalException In case of error.
     */
    public static void setFieldValue(Object obj, String fieldName, @Nullable Object val) throws IgniteInternalException {
        assert obj != null;
        assert fieldName != null;

        try {
            Class<?> cls = obj instanceof Class ? (Class<?>) obj : obj.getClass();

            Field field = cls.getDeclaredField(fieldName);

            boolean isFinal = (field.getModifiers() & Modifier.FINAL) != 0;

            boolean isStatic = (field.getModifiers() & Modifier.STATIC) != 0;

            /*
             * http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5.3
             * If a final field is initialized to a compile-time constant in the field declaration,
             *   changes to the final field may not be observed.
             */
            if (isFinal && isStatic) {
                throw new IgniteInternalException("Modification of static final field through reflection.");
            }

            if (!field.canAccess(obj)) {
                field.setAccessible(true);
            }

            field.set(obj, val);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new IgniteInternalException("Failed to set object field [obj=" + obj + ", field=" + fieldName + ']', e);
        }
    }

    /**
     * Set object field value via reflection.
     *
     * @param obj       Object to set field value to.
     * @param cls       Class to get field from.
     * @param fieldName Field name to set value for.
     * @param val       New field value.
     * @throws IgniteInternalException In case of error.
     */
    public static void setFieldValue(Object obj, Class<?> cls, String fieldName, Object val) throws IgniteInternalException {
        assert fieldName != null;

        try {
            Field field = cls.getDeclaredField(fieldName);

            if (!field.canAccess(obj)) {
                field.setAccessible(true);
            }

            boolean isFinal = (field.getModifiers() & Modifier.FINAL) != 0;

            boolean isStatic = (field.getModifiers() & Modifier.STATIC) != 0;

            /*
             * http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5.3
             * If a final field is initialized to a compile-time constant in the field declaration,
             *   changes to the final field may not be observed.
             */
            if (isFinal && isStatic) {
                throw new IgniteInternalException("Modification of static final field through reflection.");
            }

            field.set(obj, val);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new IgniteInternalException("Failed to set object field [obj=" + obj + ", field=" + fieldName + ']', e);
        }
    }

    /**
     * Finds a field in the given {@code target} object of the {@code declaredClass} type.
     *
     * @param target        target object from which to get field ({@code null} for static methods)
     * @param declaredClass class on which the field is declared
     * @param fieldName     name of the field
     * @return field
     */
    public static Field getField(@Nullable Object target, Class<?> declaredClass, String fieldName) {
        Field field;
        try {
            field = declaredClass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            throw new IgniteInternalException("Did not find a field", e);
        }

        if (!field.canAccess(target)) {
            field.setAccessible(true);
        }

        return field;
    }

    /**
     * Returns field value.
     *
     * @param target        target object from which to get field value ({@code null} for static methods)
     * @param declaredClass class on which the field is declared
     * @param fieldName     name of the field
     * @return field value
     */
    public static <T> T getFieldValue(@Nullable Object target, Class<?> declaredClass, String fieldName) {
        try {
            return (T) getField(target, declaredClass, fieldName).get(target);
        } catch (IllegalAccessException e) {
            throw new IgniteInternalException("Cannot get field value", e);
        }
    }

    /**
     * Get object field value via reflection.
     *
     * @param obj Object or class to get field value from.
     * @param fieldNames Field names to get value for: obj->field1->field2->...->fieldN.
     * @param <T> Expected field class.
     * @return Field value.
     * @throws IgniteInternalException In case of error.
     */
    public static <T> T getFieldValue(@Nullable Object obj, String... fieldNames) {
        assert obj != null;
        assert fieldNames != null;
        assert fieldNames.length >= 1;

        for (String fieldName : fieldNames) {
            Class<?> cls = obj instanceof Class ? (Class<?>) obj : obj.getClass();

            try {
                obj = getFieldValue(obj, cls, fieldName);
            } catch (IgniteInternalException e) {
                // Resolve inner class, if not an inner field.
                Class<?> innerCls = getInnerClass(cls, fieldName);

                if (innerCls == null) {
                    throw new IgniteInternalException("Failed to get object field [obj=" + obj
                            + ", fieldNames=" + Arrays.toString(fieldNames) + ']', e);
                }

                obj = innerCls;
            }
        }

        return (T) obj;
    }

    /**
     * Get inner class by its name from the enclosing class.
     *
     * @param parentCls Parent class to resolve inner class for.
     * @param innerClsName Name of the inner class.
     * @return Inner class.
     */
    @Nullable public static <T> Class<T> getInnerClass(Class<?> parentCls, String innerClsName) {
        for (Class<?> cls : parentCls.getDeclaredClasses()) {
            if (innerClsName.equals(cls.getSimpleName())) {
                return (Class<T>) cls;
            }
        }

        return null;
    }

    /**
     * Checks whether runnable throws exception, which is itself of a specified class.
     *
     * @param cls Expected exception class.
     * @param run Runnable to check.
     * @param errorMessageFragment Fragment of the error text in the expected exception, {@code null} if not to be checked.
     * @return Thrown throwable.
     */
    public static Throwable assertThrows(
            Class<? extends Throwable> cls,
            Executable run,
            @Nullable String errorMessageFragment
    ) {
        Throwable throwable = Assertions.assertThrows(cls, run);

        if (errorMessageFragment != null) {
            assertThat(throwable.getMessage(), containsString(errorMessageFragment));
        }

        return throwable;
    }

    /**
     * Checks whether runnable throws the correct {@link IgniteException}, which is itself of a specified class.
     *
     * @param expectedClass Expected exception class.
     * @param expectedErrorCode Expected error code of the {@link IgniteException}.
     * @param run Runnable to check.
     * @param errorMessageFragment Fragment of the error text in the expected exception, {@code null} if not to be checked.
     * @return Thrown throwable.
     */
    public static Throwable assertThrowsWithCode(
            Class<? extends IgniteException> expectedClass,
            int expectedErrorCode,
            Executable run,
            @Nullable String errorMessageFragment
    ) {
        try {
            run.execute();
        } catch (Throwable throwable) {
            try {
                assertInstanceOf(expectedClass, throwable);
            } catch (AssertionError err) {
                // An AssertionError from assertInstanceOf has nothing but a class name of the original exception.
                AssertionError assertionError = new AssertionError(err);

                assertionError.addSuppressed(throwable);

                throw assertionError;
            }

            IgniteException igniteException = (IgniteException) throwable;
            assertEquals(expectedErrorCode, igniteException.code(), "Invalid error code: " + igniteException.codeAsString());

            if (errorMessageFragment != null) {
                assertThat(throwable.getMessage(), containsString(errorMessageFragment));
            }

            return throwable;
        }

        throw new AssertionError("Exception has not been thrown.");
    }

    /**
     * Checks whether runnable throws exception, which is itself of a specified class, or has a cause of the specified class.
     *
     * @param run Runnable to check.
     * @param cls Expected exception class.
     * @return Thrown throwable.
     */
    public static Throwable assertThrowsWithCause(
            RunnableX run,
            Class<? extends Throwable> cls
    ) {
        return assertThrowsWithCause(run, cls, null);
    }

    /**
     * Checks whether runnable throws exception, which is itself of a specified class, or has a cause of the specified class.
     *
     * @param run Runnable to check.
     * @param cls Expected exception class.
     * @param msg Message text that should be in cause (if {@code null}, message won't be checked).
     * @return Thrown throwable.
     */
    public static Throwable assertThrowsWithCause(
            RunnableX run,
            Class<? extends Throwable> cls,
            @Nullable String msg
    ) {
        try {
            run.run();
        } catch (Throwable e) {
            if (!hasCause(e, cls, msg)) {
                fail("Exception is neither of a specified class, nor has a cause of the specified class: " + cls, e);
            }

            return e;
        }

        throw new AssertionError("Exception has not been thrown.");
    }

    /**
     * Checks if passed in {@code 'Throwable'} has given class in {@code 'cause'} hierarchy
     * <b>including</b> that throwable itself.
     *
     * <p>Note that this method follows includes {@link Throwable#getSuppressed()} into check.
     *
     * @param t   Throwable to check.
     * @param cls Cause classes to check.
     * @param messageFragment Fragment that must be a substring of a cause message (if {@code null}, message won't be checked).
     * @return {@code True} if one of the causing exception is an instance of passed in classes, {@code false} otherwise.
     */
    public static boolean hasCause(
            Throwable t,
            Class<?> cls,
            @Nullable String messageFragment
    ) {
        for (Throwable th = t; th != null; th = th.getCause()) {
            if (cls.isAssignableFrom(th.getClass())) {
                if (messageFragment == null) {
                    return true;
                }

                if (th.getMessage() != null && th.getMessage().contains(messageFragment)) {
                    return true;
                }
            }

            for (Throwable n : th.getSuppressed()) {
                if (hasCause(n, cls, messageFragment)) {
                    return true;
                }
            }

            if (th.getCause() == th) {
                break;
            }
        }

        return false;
    }

    /**
     * Checks if passed in {@code 'Throwable'} has given class in {@code 'cause'} hierarchy
     * <b>including</b> that throwable itself.
     *
     * <p>Note that this method follows includes {@link Throwable#getSuppressed()} into check.
     *
     * @param t   Throwable to check.
     * @param cls Cause classes to check.
     * @return reference to the cause error if found, otherwise returns {@code null}.
     */
    public static <T extends Throwable> @Nullable T cause(Throwable t, Class<T> cls) {
        return cause(t, cls, null);
    }

    /**
     * Checks if passed in {@code 'Throwable'} has given class in {@code 'cause'} hierarchy
     * <b>including</b> that throwable itself.
     *
     * <p>Note that this method follows includes {@link Throwable#getSuppressed()} into check.
     *
     * @param t   Throwable to check.
     * @param cls Cause classes to check.
     * @param msg Message text that should be in cause (if {@code null}, message won't be checked).
     * @return reference to the cause error if found, otherwise returns {@code null}.
     */
    public static <T extends Throwable> @Nullable T cause(
            Throwable t,
            Class<T> cls,
            @Nullable String msg
    ) {
        for (Throwable th = t; th != null; th = th.getCause()) {
            if (cls.isAssignableFrom(th.getClass())) {
                if (msg != null) {
                    if (th.getMessage() != null && th.getMessage().contains(msg)) {
                        return (T) th;
                    } else {
                        continue;
                    }
                }

                return (T) th;
            }

            if (th.getCause() == th) {
                break;
            }
        }

        return null;
    }

    /**
     * Runs runnable task asyncronously.
     *
     * @param task Runnable.
     * @return Future with task result.
     */
    public static CompletableFuture<Void> runAsync(RunnableX task) {
        return runAsync(task, "async-runnable-runner");
    }

    /**
     * Runs runnable task asyncronously.
     *
     * @param task Runnable.
     * @return Future with task result.
     */
    public static CompletableFuture<Void> runAsync(RunnableX task, String threadName) {
        return runAsync(() -> {
            try {
                task.run();
            } catch (Throwable e) {
                sneakyThrow(e);
            }

            return null;
        }, threadName);
    }

    /**
     * Runs callable task asyncronously.
     *
     * @param task Callable.
     * @return Future with task result.
     */
    public static <T> CompletableFuture<T> runAsync(Callable<T> task) {
        return runAsync(task, "async-callable-runner");
    }

    /**
     * Runs callable task asyncronously.
     *
     * @param task Callable.
     * @param threadName Thread name.
     * @return Future with task result.
     */
    public static <T> CompletableFuture<T> runAsync(Callable<T> task, String threadName) {
        ThreadFactory thrFactory = IgniteThreadFactory.withPrefix(threadName, LOG, ThreadOperation.values());

        CompletableFuture<T> fut = new CompletableFuture<T>();

        thrFactory.newThread(() -> {
            try {
                // Execute task.
                T res = task.call();

                fut.complete(res);
            } catch (Throwable e) {
                fut.completeExceptionally(e);
            }
        }).start();

        return fut;
    }

    /**
     * Executes an asynchronous operation in a thread that is allowed to execute any thread operation.
     *
     * @param operation Operation to execute.
     * @return Whatever the operation returns.
     */
    public static <T> CompletableFuture<T> bypassingThreadAssertionsAsync(Supplier<CompletableFuture<T>> operation) {
        return runAsync(operation::get).thenCompose(identity());
    }

    /**
     * Executes a synchronous operation in a thread that is allowed to execute any thread operation.
     *
     * @param operation Operation to execute.
     * @return Whatever the operation returns.
     */
    public static <T> T bypassingThreadAssertions(Supplier<T> operation) {
        return await(runAsync(operation::get));
    }

    /**
     * Executes a synchronous operation in a thread that is allowed to execute any thread operation.
     *
     * @param operation Operation to execute.
     */
    public static void bypassingThreadAssertions(Runnable operation) {
        await(runAsync(operation::run));
    }

    /**
     * Runs callable tasks each in separate threads.
     *
     * @param calls Callable tasks.
     * @param threadFactory Thread factory.
     * @return Execution time in milliseconds.
     * @throws Exception If failed.
     */
    public static long runMultiThreaded(Iterable<Callable<?>> calls, ThreadFactory threadFactory) throws Exception {
        Collection<Thread> threads = new ArrayList<>();

        Collection<CompletableFuture<?>> futures = new ArrayList<>();

        for (Callable<?> task : calls) {
            CompletableFuture<?> fut = new CompletableFuture<>();

            futures.add(fut);

            threads.add(threadFactory.newThread(() -> {
                try {
                    // Execute task.
                    task.call();

                    fut.complete(null);
                } catch (Throwable e) {
                    fut.completeExceptionally(e);
                }
            }));
        }

        long time = System.currentTimeMillis();

        for (Thread t : threads) {
            t.start();
        }

        // Wait threads finish their job.
        try {
            for (Thread t : threads) {
                t.join();
            }
        } catch (InterruptedException e) {
            for (Thread t : threads) {
                t.interrupt();
            }

            throw e;
        }

        time = System.currentTimeMillis() - time;

        for (CompletableFuture<?> fut : futures) {
            fut.join();
        }

        return time;
    }

    /**
     * Runs callable tasks in specified number of threads.
     *
     * @param call Callable.
     * @param threadNum Number of threads.
     * @param threadName Thread names.
     * @return Execution time in milliseconds.
     * @throws Exception If failed.
     */
    public static long runMultiThreaded(Callable<?> call, int threadNum, String threadName) throws Exception {
        List<Callable<?>> calls = Collections.nCopies(threadNum, call);

        NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, LOG);

        return runMultiThreaded(calls, threadFactory);
    }

    /**
     * Runs runnable object in specified number of threads.
     *
     * @param run Target runnable.
     * @param threadNum Number of threads.
     * @param threadName Thread name.
     * @return Future for the run. Future returns execution time in milliseconds.
     */
    public static CompletableFuture<Long> runMultiThreadedAsync(Runnable run, int threadNum, String threadName) {
        return runMultiThreadedAsync(() -> {
            run.run();

            return null;
        }, threadNum, threadName);
    }

    /**
     * Runs callable object in specified number of threads.
     *
     * @param call Callable.
     * @param threadNum Number of threads.
     * @param threadName Thread names.
     * @return Future for the run. Future returns execution time in milliseconds.
     */
    public static CompletableFuture<Long> runMultiThreadedAsync(Callable<?> call, int threadNum, String threadName) {
        List<Callable<?>> calls = Collections.<Callable<?>>nCopies(threadNum, call);

        NamedThreadFactory threadFactory = new NamedThreadFactory(threadName, LOG);

        return runAsync(() -> runMultiThreaded(calls, threadFactory));
    }

    /**
     * Waits for the condition.
     *
     * @param cond Condition.
     * @param timeoutMillis Timeout in milliseconds.
     * @return {@code True} if the condition was satisfied within the timeout.
     * @throws InterruptedException If waiting was interrupted.
     */
    public static boolean waitForCondition(BooleanSupplier cond, long timeoutMillis) throws InterruptedException {
        return waitForCondition(cond, 10, timeoutMillis);
    }

    /**
     * Waits for the condition.
     *
     * @param cond Condition.
     * @param sleepMillis Sleep im milliseconds.
     * @param timeoutMillis Timeout in milliseconds.
     * @return {@code True} if the condition was satisfied within the timeout.
     * @throws InterruptedException If waiting was interrupted.
     */
    @SuppressWarnings("BusyWait")
    public static boolean waitForCondition(BooleanSupplier cond, long sleepMillis, long timeoutMillis) throws InterruptedException {
        long stop = System.currentTimeMillis() + timeoutMillis;

        while (System.currentTimeMillis() < stop) {
            if (cond.getAsBoolean()) {
                return true;
            }

            sleep(sleepMillis);
        }

        return false;
    }

    /**
     * Returns random byte array.
     *
     * @param rnd Random generator.
     * @param len Byte array length.
     * @return Random byte array.
     */
    public static byte[] randomBytes(Random rnd, int len) {
        byte[] data = new byte[len];
        rnd.nextBytes(data);

        return data;
    }

    /**
     * Returns random string.
     *
     * @param rnd Random generator.
     * @param len String length.
     * @return Random string.
     */
    public static String randomString(Random rnd, int len) {
        StringBuilder sb = new StringBuilder();

        while (sb.length() < len) {
            char pt = (char) rnd.nextInt(Character.MAX_VALUE + 1);

            if (Character.isDefined(pt)
                    && Character.getType(pt) != Character.PRIVATE_USE
                    && !Character.isSurrogate(pt)
            ) {
                sb.append(pt);
            }
        }

        return sb.toString();
    }

    /**
     * Returns random {@link BigDecimal} with given scale and precision.
     *
     * @param rnd Random generator.
     * @param precision Precision of generating value.
     * @param scale Scale of generating value.
     * @return Random string.
     */
    public static BigDecimal randomBigDecimal(Random rnd, int precision, int scale) {
        BigInteger bd = IntStream.generate(() -> rnd.nextInt(9) + 1).limit(precision).mapToObj(BigInteger::valueOf)
                .reduce((n1, n2) -> n1.multiply(BigInteger.TEN).add(n2)).get();

        BigDecimal res = new BigDecimal(bd).divide(BigDecimal.TEN.pow(scale));
        if (rnd.nextBoolean()) {
            res = res.negate();
        }

        assert res.precision() == precision;
        assert res.scale() == scale;

        return res;
    }

    /**
     * Returns random {@link LocalTime} with given precision.
     *
     * @param rnd Random generator.
     * @param precision Precision of generating value. Precision can be between 0 and 9 includes both bounds.
     * @return Random {@link LocalTime} with given precision.
     */
    public static LocalTime randomTime(Random rnd, int precision) {
        assert precision >= 0 && precision <= 9 : "Valid precision for time should belong to a range [0:9]";

        LocalTime time = LocalTime.of(rnd.nextInt(24), rnd.nextInt(60), rnd.nextInt(60));

        long nanos = (precision > 0) ? 1 : 0;
        for (int i = 1; i < 9; i++) {
            nanos = nanos * 10 + (i + 1) * (i < precision ? 1 : 0);
        }

        return time.plusNanos(nanos);
    }

    /**
     * Creates a unique Ignite node name for the given test.
     *
     * @param testInfo Test info.
     * @param idx Node index.
     * @return Node name.
     */
    public static String testNodeName(TestInfo testInfo, int idx) {
        String testMethodName = testInfo.getTestMethod().map(Method::getName).orElse("null");
        String testClassName = testInfo.getTestClass().map(Class::getSimpleName).orElse("null");

        return IgniteStringFormatter.format("{}_{}_{}",
                        shortTestMethodName(testClassName),
                        shortTestMethodName(testMethodName),
                        idx);
    }

    /**
     * Returns a short version of the method name, such as "testShortMethodName" to "tsmn".
     *
     * @param methodName Method name for example "testShortMethodName".
     */
    public static String shortTestMethodName(String methodName) {
        assert !methodName.isBlank() : methodName;

        StringBuilder sb = new StringBuilder();

        char[] chars = methodName.trim().toCharArray();

        sb.append(chars[0]);

        for (int i = 1; i < chars.length; i++) {
            char c = chars[i];

            if (Character.isUpperCase(c)) {
                sb.append(c);
            }
        }

        return sb.toString().toLowerCase();
    }

    /**
     * Throw an exception as if it were unchecked.
     *
     * <p>This method erases type of the exception in the thrown clause, so checked exception could be thrown without need to wrap it with
     * unchecked one or adding a similar throws clause to the upstream methods.
     */
    public static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
        throw (E) e;
    }

    /**
     * Awaits completion of the given stage and returns its result.
     *
     * @param stage The stage.
     * @param timeout Maximum time to wait.
     * @param unit Time unit of the timeout argument.
     * @param <T> Type of the result returned by the stage.
     * @return A result of the stage.
     */
    @SuppressWarnings("UnusedReturnValue")
    public static <T> T await(CompletionStage<T> stage, long timeout, TimeUnit unit) {
        try {
            return stage.toCompletableFuture().get(timeout, unit);
        } catch (Throwable e) {
            // In tests, we generally interested in original exception,
            // rather than ExecutionException generated by the future's
            // contract. But topmost frame keeps important information
            // about failed operation in clients code. So, let's keep
            // this information in a kinda unusual yet sufficient for
            // further debug way
            Throwable original = ExceptionUtils.unwrapCause(e);

            if (original != e) {
                original.addSuppressed(new RuntimeException("This is a trimmed root"));
            }

            sneakyThrow(original);
        }

        throw new AssertionError("Should not get here");
    }

    /**
     * Awaits completion of the given stage and returns its result.
     *
     * @param stage The stage.
     * @param <T> Type of the result returned by the stage.
     * @return A result of the stage.
     */
    @SuppressWarnings("UnusedReturnValue")
    public static <T> T await(CompletionStage<T> stage) {
        return await(stage, TIMEOUT_SEC, TimeUnit.SECONDS);
    }

    /**
     * {@link #runRace(long, RunnableX...)} with default timeout of 10 seconds.
     */
    public static void runRace(RunnableX... actions) {
        runRace(TimeUnit.SECONDS.toMillis(10), actions);
    }

    /**
     * Runs all actions, each in a separate thread, having a {@link CyclicBarrier} before calling {@link RunnableX#run()}.
     * Waits for threads completion or fails with the assertion if timeout exceeded.
     *
     * @throws AssertionError In case of timeout or if any of the runnables thrown an exception.
     */
    public static void runRace(long timeoutMillis, RunnableX... actions) {
        int length = actions.length;

        if (length == 0) {
            return; // Nothing to run.
        }

        CyclicBarrier barrier = new CyclicBarrier(length);

        Set<Throwable> throwables = ConcurrentHashMap.newKeySet();

        Thread[] threads = IntStream.range(0, length).mapToObj(i -> new Thread(() -> {
            try {
                barrier.await();

                actions[i].run();
            } catch (Throwable e) {
                throwables.add(e);
            }
        })).toArray(Thread[]::new);

        Stream.of(threads).forEach(Thread::start);

        long endTs = System.currentTimeMillis() + timeoutMillis;

        try {
            for (Thread thread : threads) {
                thread.join(Math.max(1, endTs - System.currentTimeMillis()));

                if (thread.isAlive()) {
                    throw new InterruptedException(); // Interrupt all actions and fail the race.
                }
            }
        } catch (InterruptedException e) {
            for (Thread thread : threads) {
                thread.interrupt();
            }

            fail("Race operations took too long.");
        }

        if (!throwables.isEmpty()) {
            AssertionError assertionError = new AssertionError("One or several threads have failed.");

            for (Throwable throwable : throwables) {
                assertionError.addSuppressed(throwable);
            }

            throw assertionError;
        }
    }

    /**
     * Returns a file system path for a resource name.
     *
     * @param cls A class.
     * @param resourceName A resource name.
     * @return A file system path matching the path component of the resource URL.
     */
    public static String getResourcePath(Class<?> cls, String resourceName) {
        return getPath(cls.getClassLoader().getResource(resourceName)).toString();
    }

    /**
     * Converts a URL to a file system path.
     *
     * <p>This method is needed to get a proper file system name on the Windows platform. For portable code
     * it should be used instead of URL::getPath().
     *
     * <p>For example, given a URL <i>file:///C:/dir/file.ext</i>, this method returns a Windows-specific
     * path: <i>C:\dir\file.ext</i>.
     *
     * <p>While the URL::getPath() method returns a path that will not work for file system API on Windows:
     * <i>/C:/dir/file.ext</i>.
     *
     * <p>There is no such problem on UNIX-like systems where given an URL <i>file:///dir/file.ext</i>,
     * both methods return the same result: <i>/dir/file.ext</i>
     *
     * @param url A resource URL.
     * @return A file system path matching the path component of the URL.
     */
    public static Path getPath(URL url) {
        try {
            return Path.of(url.toURI());
        } catch (URISyntaxException e) {
            throw new RuntimeException(e); // Shouldn't happen if the URL is obtained from the class loader.
        }
    }

    /**
     * Adds escape characters before backslashes in a path (on Windows), e.g. for the HOCON config parser.
     *
     * <p>For example, given a path argument <i>C:\dir\file.ext</i>, this method returns <i>C:\\dir\\file.ext</i>.
     *
     * @param path A path string.
     * @return A path string with escaped backslashes.
     */
    public static String escapeWindowsPath(String path) {
        return path.replace("\\", "\\\\");
    }

    /**
     * Generate file with dummy content with provided size.
     *
     * @param file File path.
     * @param fileSize File size in bytes.
     * @throws IOException if an I/O error is thrown.
     */
    public static void fillDummyFile(Path file, long fileSize) throws IOException {
        try (SeekableByteChannel channel = Files.newByteChannel(file, WRITE, CREATE)) {
            channel.position(fileSize - 4);

            ByteBuffer buf = ByteBuffer.allocate(4).putInt(2);
            buf.rewind();
            channel.write(buf);
        }
    }

    /**
     * Run the closure in the given executor, wait for the result and get it synchronously.
     *
     * @param executor Executor.
     * @param closure Closure.
     * @return Closure result.
     */
    public static <T> T runInExecutor(ExecutorService executor, Supplier<T> closure) {
        Object[] arr = new Object[1];

        Future f = executor.submit(() -> {
            arr[0] = closure.get();
        });

        try {
            f.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

        return (T) arr[0];
    }

    /**
     * Predicate matcher.
     *
     * @param <DataT> Data type.
     */
    public static class PredicateMatcher<DataT> extends CustomMatcher<DataT> {
        /** Predicate. */
        private final Predicate<DataT> predicate;

        /**
         * Constructor.
         *
         * @param pred Predicate.
         * @param msg Error description.
         */
        public PredicateMatcher(Predicate<DataT> pred, String msg) {
            super(msg);

            predicate = pred;
        }

        /** {@inheritDoc} */
        @Override
        public boolean matches(Object o) {
            return predicate.test((DataT) o);
        }
    }

    /**
     * Derives a UUID from a string in a determenistic way in hope there will be no collisions of derived UUIDs for different strings
     * in practice.
     *
     * @param str String for which to derive a UUID.
     */
    public static UUID deriveUuidFrom(String str) {
        return new UUID(str.hashCode(), new StringBuilder(str).reverse().toString().hashCode());
    }
}
