/**
 * @file TestEvent.cpp
 * @brief Tests for ODEMx class Event
 */

#include "TestBase.h"

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

	/**
	 * @struct EventFixture
	 * @brief Helper struct providing set-up/tear-down of Event tests
	 *
	 * Structs like this are called fixtures. They are used to provide a
	 * mechanism for quickly setting up multiple tests that use the same
	 * variables. Since these variables are declared as public members
	 * and the UnitTest++ TEST_FIXTURE macro creates classes derived from
	 * their fixture struct, it is possible to use these variables directly
	 * in every test case.
	 *
	 * The constructor provides the set-up of a test's variables while the
	 * destructor handles the tear-down phase. This means that for every
	 * test case, a new object is created, which encapsulates all objects
	 * used during the test.
	 *
	 * For ODEMx tests most of these fixtures contain their own Simulation
	 * object because the DefaultSimulation is a static object and only
	 * exists once for the whole test program, i.e. all suites and tests.
	 * To avoid likely naming conflicts, scheduling problems and interference
	 * from different test simulations, a new context is advisable for each
	 * test.
	 */
	struct EventFixture
	{
		EventFixture():
			sim( "SimulationTest" ),
			log( TestLogConsumer::create() ),
			observer( false ),
			eventObj( sim, "EventTestObject" ),
			eventPtr( new EventTest( sim, "EventTestPointer", &observer ) ),
			type( typeid(Event) )
			{
				sim.addConsumer( log );
			}

		~EventFixture()
		{
			if( eventPtr ) delete eventPtr;
		}

		SimulationTest sim; ///< a user-defined simulation
		TestLogConsumer::Ptr log; ///< log consumer to check sim records
		EventTestObserver observer; ///< an observer for Event-specific calls
		EventTest eventObj; ///< an Event object
		EventTest* eventPtr; ///< pointer to a dynamically allocated Event object
		data::TypeInfo type;
	};

	/**
	 * @test odemx::Event construction and destruction
	 *
	 * Tests correct Event member initialization:
	 * @li Simulation context
	 * @li SchedType
	 * @li Label
	 * @li execution time
	 * @li scheduling priority
	 * @li observation calls of \c onCreate() and \c onDestroy()
	 *
	 * @note One object uses the constructor for DefaultSimulation, while
	 * the other one uses a given Simulation object.
	 */
	TEST_FIXTURE( EventFixture, ConstructionDestruction )
	{
		data::Label label = "EventTestConstructionDestruction";
		{
			EventTest evt( sim, label, &observer );
			CHECK_EQUAL( &sim, &evt.getSimulation() );
			CHECK_EQUAL( Sched::EVENT, evt.getSchedType() );
			CHECK_EQUAL( label, evt.getLabel() );
			CHECK_EQUAL( &observer, evt.getObservers().front() );
			CHECK_EQUAL( (base::SimTime) 0, evt.getExecutionTime() );
			CHECK_EQUAL( (base::SimTime) 0, evt.getPriority() );
			CHECK( log->getTraceRecord( "create", type ) );
			CHECK( observer.observed( "onCreate", evt.getLabel() ) );
		}
		CHECK( log->getTraceRecord( "destroy", type ) );
	}

	/**
	 * @test odemx::Event::schedule()
	 *
	 * Expected function call effects:
	 * @li Simulation is not empty, i.e. an object is scheduled in ExecutionList
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the current SimTime
	 * @li observation calls of \c onSchedule() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, Schedule )
	{
		CHECK( sim.getScheduler().getExecutionList().isEmpty() );

		// scheduling events always sets/changes the execution time
		eventPtr->schedule();
		CHECK( ! sim.getScheduler().getExecutionList().isEmpty() );
		CHECK( eventPtr->isScheduled() );
		CHECK( eventPtr->getExecutionTime() == sim.getTime() );
		CHECK( log->getTraceRecord( "schedule", type ) );
		CHECK( observer.observed( "onSchedule", eventPtr->getLabel() ) );
		CHECK( observer.observed( "onChangeExecutionTime", eventPtr->getLabel() ) );
	}

	/**
	 * @test odemx::Event::scheduleIn(SimTime)
	 *
	 * Expected function call effects:
	 * @li Simulation is not empty, i.e. an object is scheduled in ExecutionList
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the current SimTime + delay
	 * @li observation calls of \c onScheduleIn() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, ScheduleIn )
	{
		SimTime advanceTime = 15;
		sim.setCurrentTime( advanceTime );
		CHECK( sim.getTime() == advanceTime );

		SimTime delay = 512;
		SimTime oldTime = eventPtr->getExecutionTime();
		CHECK( sim.getScheduler().getExecutionList().isEmpty() );

		// scheduling events always sets/changes the execution time
		eventPtr->scheduleIn( delay );
		CHECK( ! sim.getScheduler().getExecutionList().isEmpty() );
		CHECK( eventPtr->isScheduled() );
		CHECK_EQUAL( sim.getTime() + delay, eventPtr->getExecutionTime() );
		CHECK( log->getTraceRecord( "schedule in", type ) );
		CHECK( observer.observed( "onScheduleIn", eventPtr->getLabel(), toString( delay ) ) );
		CHECK( observer.observed( "onChangeExecutionTime", eventPtr->getLabel(),
				toString( oldTime ), toString( sim.getTime() + delay ) ) );
	}

	/**
	 * @test odemx::Event::scheduleAt(SimTime)
	 *
	 * Expected function call effects:
	 * @li Simulation is not empty, i.e. an object is scheduled in ExecutionList
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the given absolute time
	 * @li observation calls of \c onScheduleAt() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, ScheduleAt )
	{
		SimTime absoluteTime = 27;
		SimTime oldTime = eventPtr->getExecutionTime();
		CHECK( sim.getScheduler().getExecutionList().isEmpty() );

		// scheduling events always sets/changes the execution time
		eventPtr->scheduleAt( absoluteTime );
		CHECK( ! sim.getScheduler().getExecutionList().isEmpty() );
		CHECK( eventPtr->isScheduled() );
		CHECK_EQUAL( absoluteTime, eventPtr->getExecutionTime() );
		CHECK( log->getTraceRecord( "schedule at", type ) );
		CHECK( observer.observed( "onScheduleAt", eventPtr->getLabel(), toString( absoluteTime ) ) );
		CHECK( observer.observed( "onChangeExecutionTime", eventPtr->getLabel(),
				toString( oldTime ), toString( absoluteTime ) ) );
	}

	/**
	 * @test odemx::Event::scheduleAppend()
	 *
	 * Expected function call effects:
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the current SimTime
	 * @li the Event is scheduled after another Event
	 * @li observation calls of \c onScheduleAppend() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, ScheduleAppend )
	{
		EventTest e( sim, "ScheduleAppendTestEvent" );
		e.schedule();

		// scheduling events always sets/changes the execution time
		eventPtr->scheduleAppend();
		CHECK( eventPtr->isScheduled() );
		CHECK( eventPtr->getExecutionTime() == sim.getTime() );

		CHECK( log->getTraceRecord( "schedule append", type ) );
		CHECK( observer.observed( "onScheduleAppend", eventPtr->getLabel() ) );

		// check correct order
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), &e );
		sim.step();
		CHECK( !e.isScheduled() );
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), eventPtr );
	}

	/**
	 * @test odemx::Event::scheduleAppendIn(SimTime)
	 *
	 * Expected function call effects:
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the current SimTime + delay
	 * @li the Event is scheduled after another Event
	 * @li observation calls of \c onScheduleAppendIn() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, ScheduleAppendIn )
	{
		EventTest e( sim, "ScheduleAppendInTestEvent" );
		e.schedule();

		SimTime delay = 512;
		SimTime oldTime = eventPtr->getExecutionTime();

		// scheduling events always sets/changes the execution time
		eventPtr->scheduleAppendIn( delay );
		CHECK( eventPtr->isScheduled() );
		CHECK_EQUAL( sim.getTime() + delay, eventPtr->getExecutionTime() );
		CHECK( log->getTraceRecord( "schedule append in", type ) );
		CHECK( observer.observed( "onScheduleAppendIn", eventPtr->getLabel(), toString( delay ) ) );
		CHECK( observer.observed( "onChangeExecutionTime", eventPtr->getLabel(),
				toString( oldTime ), toString( sim.getTime() + delay ) ) );

		// check correct order
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), &e );
		sim.step();
		CHECK( !e.isScheduled() );
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), eventPtr );
	}

	/**
	 * @test odemx::Event::scheduleAppendAt(SimTime)
	 *
	 * Expected function call effects:
	 * @li the Event object knows it is scheduled
	 * @li the Event execution time is equal to the given absolute time
	 * @li the Event is scheduled after another Event
	 * @li observation calls of \c onScheduleAppendAt() and \c onChangeExecutionTime()
	 */
	TEST_FIXTURE( EventFixture, ScheduleAppendAt )
	{
		EventTest e( sim, "ScheduleAppendInTestEvent" );
		e.schedule();

		SimTime absoluteTime = 15;
		SimTime oldTime = eventPtr->getExecutionTime();

		// scheduling events always sets/changes the execution time
		eventPtr->scheduleAppendAt( absoluteTime );
		CHECK( eventPtr->isScheduled() );
		CHECK_EQUAL( absoluteTime, eventPtr->getExecutionTime() );
		CHECK( log->getTraceRecord( "schedule append at", type ) );
		CHECK( observer.observed( "onScheduleAppendAt", eventPtr->getLabel(), toString( absoluteTime ) ) );
		CHECK( observer.observed( "onChangeExecutionTime", eventPtr->getLabel(),\
				toString( oldTime ), toString( absoluteTime ) ) );

		// check correct order
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), &e );
		sim.step();
		CHECK( !e.isScheduled() );
		CHECK_EQUAL( sim.getScheduler().getExecutionList().getNextSched(), eventPtr );
	}

	/**
	 * @test odemx::Event::removeFromSchedule()
	 *
	 * Expected function call effects:
	 * @li Simulation gets empty, i.e. the scheduled object is removed from the
	 * ExecutionList
	 * @li the Event object knows it is not scheduled
	 * @li observation call of \c onRemoveFromSchedule()
	 * @li error output when called on non-scheduled event
	 */
	TEST_FIXTURE( EventFixture, RemoveFromSchedule )
	{
		eventPtr->schedule();
		CHECK( ! sim.getScheduler().getExecutionList().isEmpty() );

		eventPtr->removeFromSchedule();
		CHECK( sim.getScheduler().getExecutionList().isEmpty() );
		CHECK( ! eventPtr->isScheduled() );
		CHECK( log->getTraceRecord( "remove from schedule", type ) );
		CHECK( observer.observed( "onRemoveFromSchedule", eventPtr->getLabel() ) );

		EventTest e( sim, "RemoveFromScheduleErrorEvent" );
		e.removeFromSchedule();
		CHECK( log->getErrorRecord( "Event::removeFromSchedule(): event is not scheduled", type ) );
	}

	/**
	 * @test odemx::Event::execute()
	 *
	 * Expected function call effects:
	 * @li \c eventAction() was executed
	 * @li the Event is removed from the schedule (ExecutionList)
	 * @li the Event knows it is not scheduled
	 * @li observation call of \c onExecuteEvent()
	 */
	TEST_FIXTURE( EventFixture, Execute )
	{
		eventPtr->schedule(); // must be scheduled for execution, else fatal error
		sim.step();
		CHECK( eventPtr->executed );
		CHECK( sim.getScheduler().getExecutionList().isEmpty() );
		CHECK( ! eventPtr->isScheduled() );
		CHECK( log->getTraceRecord( "execute event", type ) );
		CHECK( observer.observed( "onExecuteEvent", eventPtr->getLabel() ) );
	}

	/**
	 * @test odemx::Event::setPriority()
	 *
	 * Expected function call effects:
	 * @li return of old Event priority
	 * @li observation call of \c onChangePriority()
	 */
	TEST_FIXTURE( EventFixture, Priority )
	{
		Priority newPriority = 2;
		Priority oldPriority = eventPtr->setPriority( newPriority );
		CHECK_EQUAL( newPriority, eventPtr->getPriority() );
		CHECK( log->getTraceRecord( "change priority", type ) );
		CHECK( observer.observed( "onChangePriority", eventPtr->getLabel(),\
				toString( oldPriority ), toString( newPriority) ) );
	}

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