/**
 * @file TestCoroutine.cpp
 * @brief Tests for ODEMx class Coroutine
 */

#include "TestCoroutine.h"

/// @cond DOXYGEN_SKIP
SUITE( Coro )
{
/// @endcond

	/**
	 * @struct CoroutineFixture
	 * @brief Helper struct providing set-up/tear-down of Coroutine tests
	 *
	 * @copydetails EventFixture
	 */
	struct CoroutineFixture
	{
		CoroutineTestObserver observer; ///< observer to monitor Coroutine calls
		CoroutineContextTest context; ///< context object for Coroutine tests
		CoroutineTest coro1; ///< a test Coroutine object
		CoroutineTest coro2; ///< a test Coroutine object

		CoroutineFixture():
			observer( false ),
			coro1( "CoroutineTest1", 0, &observer ),
			coro2( "CoroutineTest2", &context, &observer )
			{}
	};

	/**
	 * @test odemx::Coroutine construction and destruction
	 *
	 * Tests correct Coroutine member initialization:
	 * @li Coroutine state
	 * @li parent
	 * @li caller
	 * @li label
	 * @li default context, or given context
	 * @li registration of the Coroutine with its context
	 * @li observation calls of \c onCreate() and \c onDestroy()
	 * @li observation call of \c onClear() for non-terminated Coroutine
	 *
	 * @note One object uses the constructor for DefaultContext, while
	 * the other one uses a given CoroutineContext object.
	 */
	TEST_FIXTURE( CoroutineFixture, ConstructionDestruction )
	{
		CHECK( coro1.getState() == Coroutine::CREATED );
		CHECK( coro1.getParent() == 0 );
		CHECK( coro1.getCaller() == 0 );
		CHECK( coro1.getContext() == getDefaultContext() );

		CHECK( coro2.getContext() == &context );
		CHECK( context.getNumberOfCoroutines() == 1 );

		CoroutineTest* c = new CoroutineTest( "CoroutineTestConstructionDestruction", 0, &observer );
		data::Label cLabel = c->getName();

		std::string addr = toString( c );
		CHECK( observer.observed( "onCreate", addr ) );

		delete c;
		CHECK( observer.observed( "onClear", addr ) );
		CHECK( observer.observed( "onDestroy", addr ) );
	}

	/**
	 * @test odemx::Coroutine::switchTo() and operator()
	 *
	 * Testing Coroutine switching requires a more elaborate test. Two objects
	 * of type CoroutineTest are created in order to switch back and forth
	 * between them. One is the initializer, which is started by the context,
	 * while the other one is its child.
	 *
	 * The CoroutineTest's behavior is simple: register a partner,
	 * enter a loop with a counter, and call its partner. Each time the
	 * coroutine is running, it just increases the counter and yields control
	 * to its partner. For monitoring purposes, the initializer returns briefly
	 * to the context, i.e. the test environment, after its first execution.
	 *
	 * Expected call effects of Coroutine::operator()():
	 * @li the initializer Coroutine is started
	 * @li observation calls of \c onInitialize(), \c onChangeState(),
	 * and \c onSwitchTo()
	 * @li the context is parent and caller of the initializer
	 * @li the loop count is still 0 (yielded to context before entering)
	 * @li the CoroutineState is RUNNABLE
	 * @li the Coroutine is not finished
	 * @li the partner is not started
	 *
	 * Expected call effects of Coroutine::switchTo():
	 * @li the child Coroutine is started
	 * @li child observation call of \c onChangeState() to RUNNABLE
	 * @li initializer is parent and caller
	 * @li observation call to \c onSwitchTo() from initializer to child
	 * and back
	 * @li both coroutines have a loop count of 10
	 * @li both coroutines have finished and are TERMINATED
	 * @li both made observation calls of \c onClear()
	 * @li the context's number of registered coroutines is \c 0
	 *
	 * @sa CoroutineTest
	 */
	TEST_FIXTURE( CoroutineFixture, SwitchTo )
	{
		CoroutineTest coro3( "CoroutineTest3", &context, &observer );
		CHECK( context.getNumberOfCoroutines() == 2 );

		// set up
		coro2.partner = &coro3;
		coro3.partner = &coro2;
		coro2.isInitializer = true;

		// test op() and thus the first call of switchTo()
		coro2();
		CHECK( observer.observed( "onInitialize", toString( &coro2 ) ) );
		CHECK( observer.observed( "onChangeState", toString( &coro2 ), "CREATED", "RUNNABLE" ) );
		CHECK( observer.observed( "onSwitchTo", toString( &coro2 ), toString( (CoroutineContext*) &context ) ) );
		CHECK( coro2.isStarted );

		// context is parent and caller
		CHECK( coro2.getParent() == 0 );
		CHECK( coro2.getCaller() == 0 );

		// check correct switching back to this context
		// make sure it did not finish, but instead halted execution
		CHECK( coro2.loopCount == 0 );
		CHECK( coro2.getState() == Coroutine::RUNNABLE );
		CHECK( ! coro2.isFinished );
		CHECK( ! coro3.isStarted );

		// test switching between coroutines, coro2 starts coro3
		coro2.switchTo();
		CHECK( coro3.isStarted );
		CHECK( observer.observed( "onChangeState", toString( &coro3 ), "CREATED", "RUNNABLE" ) );
		// coro2 is parent and caller
		CHECK( coro3.getParent() == &coro2 ); // parent is coro2, called from switchTo()
		CHECK( coro3.getCaller() == &coro2 );
		CHECK( observer.observed( "onSwitchTo", toString( &coro2 ), toString( &coro3 ) ) );
		CHECK( observer.observed( "onSwitchTo", toString( &coro3 ), toString( &coro2 ) ) );

		// check correct termination of coro2
		CHECK( coro2.loopCount == 10 );
		CHECK( coro2.isFinished );
		CHECK( observer.observed( "onClear", toString( &coro2 ) ) );
		CHECK( coro2.getState() == Coroutine::TERMINATED );

		// check correct termination of coro3
		CHECK( coro3.loopCount == 10 );
		CHECK( coro3.isFinished );
		CHECK( observer.observed( "onClear", toString( &coro3 ) ) );
		CHECK( coro3.getState() == Coroutine::TERMINATED );

		// check correct de-registration from context
		CHECK( context.getNumberOfCoroutines() == 0 );
	}

/// @cond DOXYGEN_SKIP
}
/// @endcond
