/**
 * @file TestSimulation.cpp
 * @brief Tests for ODEMx class Simulation
 */

#include "TestBase.h"

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

	/**
	 * @struct SimulationFixture
	 * @brief Helper struct providing set-up/tear-down of Simulation tests
	 *
	 * @copydetails EventFixture
	 */
	struct SimulationFixture
	{
		SimulationFixture():
			observer( false ),
			sim( "SimulationTest", &observer ),
			log( TestLogConsumer::create() ),
			event( sim, "EventTest" ),
			process( sim, "ProcessTest" ),
			process2( sim, "ProcessTest" ),
			process3( sim, "ProcessTest" ),
			type( typeid(Simulation) )
			{
				sim.addConsumer( log );
			}

		SimulationTestObserver observer; ///< an onserver for monitoring Simulation calls
		SimulationTest sim; ///< a Simulation test object
		TestLogConsumer::Ptr log; ///< log consumer to check sim records
		EventTest event; ///< a helper Event object
		ProcessTest process; ///< a helper Process object
		ProcessTest process2; ///< a helper Process object
		ProcessTest process3; ///< a helper Process object
		data::TypeInfo type;
	};

	/**
	 * @test odemx::Simulation construction and destruction
	 *
	 * Tests correct member initialization:
	 * @li no current process
	 * @li no current Sched
	 * @li start time
	 * @li current time equals start time
	 * @li observation calls of \c onCreate() and \c onDestroy()
	 */
	TEST_FIXTURE( SimulationFixture, ConstructionDestruction )
	{
		// trace records cannot be observed during construction
		// because there is no consumer attached to sim's channels
		data::Label label = "SimulationTest1";
		SimulationTest sim( label, &observer );
		CHECK( sim.getCurrentProcess() == 0 );
		CHECK( sim.getCurrentSched() == 0 );
		CHECK( sim.getStartTime() == 0 );
		CHECK_EQUAL( sim.getTime(), sim.getStartTime() );

		SimTime startAt = 10000;
		label = "SimulationTest2";
		SimulationTest s2( label, startAt, &observer );
		CHECK_EQUAL( label, s2.getLabel() );
		CHECK_EQUAL( startAt, s2.getStartTime() );
		CHECK_EQUAL( s2.getTime(), s2.getStartTime() );
		CHECK( observer.lastObservation()->event == "onCreate" );
		CHECK( observer.lastObservation()->sender == label );
	}

	/**
	 * @test odemx::Simulation::setProcessAsXXX, management of Process lists
	 *
	 * A Simulation keeps track of all Processes created in its context.
	 * These objects are managed in lists according to their ProcessState,
	 * which can be CREATED, RUNNABLE, IDLE, TERMINATED, or CURRENT. Since
	 * there is only one current Process at a certain point in time, only
	 * the other four states are managed.
	 *
	 * In this test, a Process is walked through its states by calling the
	 * corresponding member functions, which in turn will move the object
	 * from one list to the next.
	 *
	 * Expected effect after each call:
	 * @li initially, \c process is in the list of created processes
	 * @li after \c activate(), it is in the list of runnable processes
	 * @li after \c sleep(), it is in the list of idle processes
	 * @li after \c interrupt(), it is in the list of runnable processes
	 * @li after running the Simulation, it is in the list of terminated
	 * processes
	 */
	TEST_FIXTURE( SimulationFixture, SetProcessAsXXX )
	{
		// process should be in the list of created processes
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process3.getLabel(), "CREATED" ) );

		// move to list of runnable (scheduled) processes
		process.activate();
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "RUNNABLE" ) );

		// move to list of idle (not scheduled) processes
		process.sleep();
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "IDLE" ) );

		// move to list of runnable (scheduled) processes
		process.interrupt();
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "RUNNABLE" ) );

		// move to list of terminated processes by execution
		sim.run();
		CHECK( process.executed );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "TERMINATED" ) );
	}

	/**
	 * @test odemx::Simulation::step()
	 *
	 * A Process and an Event are scheduled to be executed by the Simulation.
	 * Step is called twice to execute both objects.
	 *
	 * Expected function call effects after first call:
	 * @li the Simulation called the user-defined initialization function
	 * @li the Process was executed
	 * @li observation calls of \c onStep(), \c onInitialization(),
	 * \c onChangeTime(), \c onExecuteProcess(), \c onChangeCurrentProcess(),
	 * and two calls to \c onChangeProcessList(), i.e. RUNNABLE and TERMINATED
	 *
	 * Expected function call effects after second call:
	 * @li the Event was executed
	 * @li observation calls of \c onStep(),\c onChangeTime(), and
	 * \c onExecuteEvent()
	 * @li no calls of \c onInitialization(),\c onChangeCurrentProcess(),
	 * and \c onChangeProcessList()
	 */
	TEST_FIXTURE( SimulationFixture, Step )
	{
		observer.history.clear();
		process.activate();
		event.scheduleAppend();

		// check process execution and initialization
		sim.step();
		CHECK( sim.calledInit );
		CHECK( process.executed );
		CHECK( log->getTraceRecord( "step", type ) );
		CHECK( log->getTraceRecord( "init", type ) );
		CHECK( observer.observed( "onStep", sim.getLabel() ) );
		CHECK( observer.observed( "onInitialization", sim.getLabel() ) );
		CHECK( observer.observed( "onChangeTime", sim.getLabel(), "0", "0" ) );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "RUNNABLE" ) );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "TERMINATED" ) );
		observer.history.clear();

		// check event execution
		sim.step();
		CHECK( event.executed );
		CHECK( ! observer.observed( "onInitialization", sim.getLabel() ) );
		CHECK( ! observer.observed( "onChangeCurrentProcess", sim.getLabel() ) );
		CHECK( ! observer.observed( "onChangeProcessList", sim.getLabel() ) );
		CHECK( observer.observed( "onStep", sim.getLabel() ) );
		CHECK( observer.observed( "onChangeTime", sim.getLabel(), "0", "0" ) );
	}

	/**
	 * @test odemx::Simulation::run()
	 *
	 * A Process is scheduled to be executed by the Simulation.
	 *
	 * Expected function call effects:
	 * @li the Simulation called the user-defined initialization function
	 * @li the Process was executed
	 * @li observation calls of \c onRun(), \c onInitialization(),
	 * \c onChangeTime(), \c onExecuteProcess(), \c onChangeCurrentProcess(),
	 * and two calls to \c onChangeProcessList(), i.e. RUNNABLE and TERMINATED
	 */
	TEST_FIXTURE( SimulationFixture, Run )
	{
		observer.history.clear();
		process.activateIn( 1 );
		sim.run();
		CHECK( sim.calledInit );
		CHECK( process.executed );
		CHECK( log->getTraceRecord( "run", type ) );
		CHECK( log->getTraceRecord( "init", type ) );
		CHECK( observer.observed( "onRun", sim.getLabel() ) );
		CHECK( observer.observed( "onInitialization", sim.getLabel() ) );
		CHECK( observer.observed( "onChangeTime", sim.getLabel(), "0", "1" ) );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "RUNNABLE" ) );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "TERMINATED" ) );
	}

	/**
	 * @test odemx::Simulation::runUntil()
	 *
	 * Objects are scheduled to be executed by the Simulation at three
	 * different times:
	 * @li 499: \c process
	 * @li 500: \c event, process2
	 * @li 501: \c process3
	 *
	 * Expected function call effects after calling \c runUntil(499):
	 * @li the Simulation called the user-defined initialization function
	 * @li \c process was executed
	 * @li neither <tt>event, process2</tt> nor \c process3 were executed
	 * @li observation calls of \c onRunUntil(), \c onInitialization(),
	 * \c onChangeTime(), \c onExecuteProcess(), \c onChangeCurrentProcess(),
	 * and two calls to \c onChangeProcessList(), i.e. RUNNABLE and TERMINATED
	 *
	 * Expected function call effects after calling \c runUntil(500):
	 * @li \c event and \c process 2 were executed
	 * @li \c process3 was not executed
	 *
	 * Expected function call effects after calling \c runUntil(600):
	 * @li \c process3 was executed
	 * @li the Simulation is finished
	 */
	TEST_FIXTURE( SimulationFixture, RunUntil )
	{
		process.activateAt( 499 );
		event.scheduleAt( 500 );
		process2.activateAt( 500 );
		process3.activateAt( 501 );
		observer.history.clear();

		SimTime untilTime = 499;
		sim.runUntil( untilTime );
		CHECK( sim.calledInit );
		CHECK( process.executed );
		CHECK( ! process2.executed );
		CHECK( ! event.executed );
		CHECK( ! process3.executed );
		CHECK( log->getTraceRecord( "run until", type ) );
		CHECK( log->getTraceRecord( "init", type ) );
		CHECK( observer.observed( "onRunUntil", sim.getLabel(), toString( untilTime ) ) );
		CHECK( observer.observed( "onInitialization", sim.getLabel() ) );
		CHECK( observer.observed( "onChangeTime", sim.getLabel(), "0", toString( untilTime ) ) );
		CHECK( observer.observed( "onChangeProcessList", sim.getLabel(), process.getLabel(), "TERMINATED" ) );

		sim.runUntil( 500 );
		CHECK( process2.executed );
		CHECK( event.executed );
		CHECK( ! process3.executed );

		sim.runUntil( 600 );
		CHECK( process3.executed );
		CHECK( sim.isFinished() );
	}

	/**
	 * @test odemx::Simulation::exitSimulation()
	 *
	 * Precondition: a Process is scheduled.
	 *
	 * Expected function call effects:
	 * @li the Simulation is finished despite non-empty schedule
	 * @li observation call of \c onExitSimulation()
	 */
	TEST_FIXTURE( SimulationFixture, ExitSimulation )
	{
		observer.history.clear();
		CHECK( ! sim.isFinished() );

		process.activate();
		CHECK( ! sim.getScheduler().getExecutionList().isEmpty() );

		sim.exitSimulation();
		CHECK( log->getTraceRecord( "exit", type ) );
		CHECK( observer.observed( "onExitSimulation", sim.getLabel() ) );
		CHECK( sim.isFinished() );
	}

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