/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.uberfire.wbtest.gwttest;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import com.google.common.base.Predicate;
import com.google.gwt.user.client.Timer;
import org.gwtbootstrap3.client.GwtBootstrap3EntryPoint;
import org.jboss.errai.enterprise.client.cdi.AbstractErraiCDITest;
import org.uberfire.wbtest.client.api.UncaughtExceptionAlerter;

public abstract class AbstractUberFireGwtTest extends AbstractErraiCDITest {

    /**
     * If set to true (eg. by a subclass) then the async polling logic in the {@link #pollWhile(Predicate)} system will
     * log status updates to System.out.
     */
    protected boolean debugAsyncTesting = false;

    @Override
    public String getModuleName() {
        return "org.uberfire.wbtest.UberFireClientGwtTest";
    }

    @Override
    protected void gwtSetUp() throws Exception {
        UncaughtExceptionAlerter.disable();

        disableBus = true;

        // let GwtBootstrap inject its scripts (otherwise, its widgets will blow up during the test)
        //new Bootstrap().onModuleLoad();
        new GwtBootstrap3EntryPoint().onModuleLoad();

        super.gwtSetUp();
    }

    /**
     * Entry point for building step-by-step asynchronous tests. Use the initial predicate to test for workbench startup
     * completion, then chain an arbitrary number of additional actions to execute afterward: JUnit assertions,
     * framework actions such as {@code PlaceManager.goTo()}, and even additional predicates to poll on.
     * <p>
     * The async processing will log its progress on stdout if the field {@link #debugAsyncTesting} is set to
     * {@code true}.
     * @param predicate the predicate to test. Must not be null. Will be called over and over (every 500 ms) until it returns
     * false.
     * @return a builder object for chaining the additional actions that will be executed once {@code predicate} returns
     * false.
     */
    protected AfterPolling pollWhile(final Predicate<Void> predicate) {
        final List<Object> chainedActions = new ArrayList<Object>();

        delayTestFinish(15000);

        new Timer() {
            private final long startTime = System.currentTimeMillis();

            @Override
            public void run() {
                debugPrint("Testing predicate");
                if (!predicate.apply(null)) {
                    final ListIterator<Object> chain = chainedActions.listIterator();
                    new Timer() {
                        @Override
                        public void run() {
                            if (chain.hasNext()) {
                                Object next = chain.next();
                                if (next instanceof Runnable) {
                                    debugPrint("Running supplied task");
                                    ((Runnable) next).run();
                                    schedule(0);
                                } else if (next instanceof Predicate) {
                                    debugPrint("Testing intermediate predicate");
                                    if (((Predicate<?>) next).apply(null)) {
                                        debugPrint("Intermediate predicate is true. Will try again in 500ms.");
                                        chain.previous();
                                        schedule(500);
                                    } else {
                                        debugPrint("Intermediate predicate became false. Continuing.");
                                        schedule(0);
                                    }
                                } else if (next instanceof Integer) {
                                    debugPrint("Delaying " + next + " ms");
                                    schedule((Integer) next);
                                } else {
                                    throw new AssertionError("Unknown entry in doAfter list: " + next);
                                }
                            } else {
                                debugPrint("Test passed!");
                                finishTest();
                            }
                        }
                    }.schedule(0);
                } else {
                    if (System.currentTimeMillis() - startTime > 5000) {
                        debugPrint("Still waiting on pollWhile() condition...");
                    }
                    schedule(500);
                }
            }
        }.schedule(500);

        return new AfterPolling() {
            @Override
            public AfterPolling thenDo(Runnable runnable) {
                chainedActions.add(runnable);
                return this;
            }

            @Override
            public AfterPolling thenPollWhile(Predicate<Void> isTrue) {
                chainedActions.add(isTrue);
                return this;
            }

            @Override
            public AfterPolling thenDelay(int delayMillis) {
                chainedActions.add(delayMillis);
                return this;
            }
        };
    }

    private void debugPrint(String msg) {
        if (debugAsyncTesting) {
            System.out.println(msg);
        }
    }

    /**
     * Builder for chaining additional tasks to perform after a {@link AbstractUberFireGwtTest#pollWhile(Predicate)}
     * condition has been satisfied.
     */
    public interface AfterPolling {

        /**
         * Adds the given arbitrary code to be executed after all previous steps in the chain have been satisfied.
         * This is a good place to put JUnit assertions.
         * @param runnable the code to run. Must not be null.
         * @return this object again, for chaining more steps.
         */
        AfterPolling thenDo(Runnable runnable);

        /**
         * Adds a new predicate to test every 500 ms. The next step in the chain will not be executed until the given
         * predicate returns false.
         * @param isTrue the predicate to poll every 500ms. Not null.
         * @return this object again, for chaining more steps.
         */
        AfterPolling thenPollWhile(Predicate<Void> isTrue);

        /**
         * Adds a delay of the given length to the chain. The next step in the chain will be executed after the given
         * time has elapsed.
         * @param delayMillis the amount of time to delay (in milliseconds). Must be a nonnegative number.
         * @return this object again, for chaining more steps.
         */
        AfterPolling thenDelay(int delayMillis);
    }
}
