/*
 * Copyright (c) 2008 Kasper Nielsen.
 *
 * 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.cakeframework.test.petridish;

import static java.util.Objects.requireNonNull;

import java.util.concurrent.TimeUnit;

/**
 *
 */
public interface Dish {

    /**
     * Advances the state machine
     *
     * @param transition
     *            the transition to perform
     * @return this state machine
     * @throws AssertionError
     *             if the from-transition is not identically to the current state
     */
    Dish advance(String transition);

    default Dish advanceIf(boolean condition, String transition) {
        if (condition) {
            advance(transition);
        }
        return this;
    }

    /**
     * Asserts that this state machine is in the end state as indicated by {@link #isEndState()}. If it is not,
     * {@link AssertionError} is thrown.
     */
    void assertInEndState();

    /**
     * Asserts that this state machine is in the initial state as indicated by {@link #isInitialState()}. If it is not,
     * {@link AssertionError} is thrown.
     */
    void assertInInitialOrEndState();

    /**
     * Asserts that this state machine is in the initial state as indicated by {@link #isInitialState()}. If it is not,
     * {@link AssertionError} is thrown.
     */
    void assertInInitialState();

    /**
     * Asserts that this state machine is in the specified state as indicated by {@link #isState()}. If it is not,
     * {@link AssertionError} is thrown.
     */
    void assertInState(String state);

    /**
     * Awaits for the state machine to transition to the specified state. If that does not occur within
     * {@link #getDefaultTimeout() the default timeout} this method throws an {@link AssertionError} and the state
     * machine transitions to a failed state. If the state machine is already in the specified state this method returns
     * immediately.
     * <p>
     * If the thread is interrupted while waiting an {@link AssertionError} exception will be thrown and this state
     * machine will transition to a failed state.
     * <p>
     * If the state machine is in the failed state or transitions to it while awaiting. Will throw an
     * {@link AssertionError}.
     *
     * @param state
     *            the state to await
     * @return this state machine
     * @throws AssertionError
     *             if the state machine is not in the specified state within the default timeout
     * @throws NullPointerException
     *             if the specified state is null
     */
    Dish await(String state);

    /**
     * Equivalent to calling {@link #await(String)} followed by {@link #advance(String)}. For example, invoking this
     * method using <code>high->low<code> is equivalent to calling <code>await("high").advance("high->low)</code>.
     * <p>
     * No atomically guarantees are made. For example, if calling <code>awaitAndAdvance("high->medium")</code> in one
     * thread and <code>awaitAndAdvance("high->low")</code> in another. One of them will win and the other one will fail
     * when the state machine transitions to the <code>high</code> state.
     *
     * @param transition
     *            the transition
     * @return this state machine
     * @throws AssertionError
     *             if the state machine is not in the from state from the specified transition within the default
     *             timeout. Or if another thread success in changing the state
     * @throws NullPointerException
     *             if the specified state is null
     */
    Dish awaitAndAdvance(String transition);

    /**
     * Awaits for the state machine to transition to change to another state than the current state. If that does not
     * occur within {@link #getTimeout(TimeUnit) the default timeout} this method throws an {@link AssertionError} and
     * the state machine transitions to a failed state.
     *
     * @return the new state, or null if transitioning to the end state
     */
    String awaitStateChange();

    default void fail() {
        fail(Dish.class.getSimpleName() + " failed via fail()");
    }

    /**
     * Transitions the state machine to the failed state. Trying to use any of the await, transition or assert methods
     * will result in an AssertionError being thrown with the specified message.
     *
     *
     * @param message
     *            the fail message
     * @throws NullPointerException
     *             if the message is null
     */
    default void fail(String message) {
        fail(new AssertionError(requireNonNull(message, "message is null")));
    }

    /**
     * Transitions the state machine to the failed state. Trying to use any of the await, transition or assert methods
     * will result in an AssertionError being thrown with the specified message and the specified cause as the
     * {@link Throwable#getCause() cause}.
     *
     *
     * @param cause
     *            the cause
     * @throws NullPointerException
     *             if the message or cause is null
     */
    void fail(Throwable cause);

    /**
     * Returns the current state, or {@code null} if in the current state is either the initial- or end-state
     *
     * @return the current state, or {@code null} if in the initial- or end-state
     */
    String getState();

    /**
     * Returns the default timeout used in {@link #await(String)} and {@link #awaitAndAdvance(String)}. The time unit of
     * the timeout is returned by {@link #getDefaultTimeoutUnit()}.
     *
     * @return the default timeout
     * @see #getDefaultTimeoutUnit()
     */
    long getTimeout(TimeUnit unit);

    /** Returns <tt>true</tt> if in the end-state otherwise <tt>false</tt>. */
    boolean isEndState();

    /** Returns <tt>true</tt> if this state machine in a fail state, otherwise <tt>false</tt>. */
    boolean isFailState();

    /**
     * Returns <tt>true</tt> if the state machine is in either the initial state (no transitions has been performed) or
     * the end state. Otherwise, <tt>false</tt>.
     *
     * @return true if the state machine is in the initial or end state, otherwise false
     */
    boolean isInitialOrEndState();

    /**
     * Returns <tt>true</tt> if the state machine is in the initial state (no transitions has been performed).
     * Otherwise, <tt>false</tt>.
     *
     * @return true if the state machine is in the initial state, otherwise false
     */
    boolean isInitialState();

    /**
     * Returns <tt>true</tt> if the state machine is in the specified state. Otherwise, <tt>false</tt>.
     *
     * @return true if the state machine is in the specified state, otherwise false
     * @throws NullPointerException
     *             if the specified state is null
     */
    boolean isState(String state);

    /**
     * Attaches a note for the current thread and time. This is mainly used for debugging purposes.
     *
     * @param note
     *            the node to attach
     * @return this state machine
     * @throws NullPointerException
     *             if the specified note is null
     */
    Dish note(String note);

    /**
     * Spawns a new test thread. A state machine cannot transition to the end state before all spawned threads has
     * successfully terminated.
     *
     * @param threadName
     *            the name of the thread being spawned
     * @param runnable
     *            the runnable
     * @return this state machine
     */
    Dish spawn(String threadName, TsmRunnable runnable);

    /**
     * Spawns the specified amount of threads each using the same (stateless) specified runnable.
     *
     * @param amount
     *            the number of threads to spawn
     * @param threadNamePrefix
     *            the name prefix of the spawned threads. For example if "foo" is specified. All the spawned threads
     *            will be named <code>foo-1, foo-2, foo-3, ...</code>
     * @param runnable
     * @return this state machine
     * @throws IllegalArgumentException
     *             if amount is non-positive (<1)
     * @throws NullPointerException
     *             if threadNamePrefix or runnable is null
     */
    Dish spawnAll(int amount, String threadNamePrefix, TsmRunnable runnable);
}
