/**
 * @file TestRes.cpp
 * @date Jul 30, 2008
 * @author Ronald Kluth
 *
 * @brief Tests for ODEMx class Res
 */

#include "TestSynchronization.h"
#include "../TestBase/TestBase.h"

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

	/**
	 * @struct ResFixture
	 * @brief Helper struct providing set-up/tear-down of Res tests
	 *
	 * @copydetails EventFixture
	 */
	struct ResFixture
	{
		SuiteBase::SimulationTest sim;
		TestLogConsumer::Ptr log;
		ResTestObserver observer;
		Res res;
		data::TypeInfo type;

		ResFixture()
		:	sim( "ResTestSim" ),
			log( TestLogConsumer::create() ),
			observer( false ),
			res( sim, "ResTest", 5, 20, &observer ),
			type( typeid(Res) )
			{
				sim.addConsumer( log );
			}
	};

	/**
	 * @test odemx::Res construction and destruction
	 *
	 * Expected effects:
	 * @li simulation environment is set for DefaultSimulation or user-definied
	 * simulation
	 * @li label is set
	 * @li initial tokens are set
	 * @li maximum tokens are set
	 * @li observer is set correctly
	 * @li construction and destruction can be observed
	 */
	TEST_FIXTURE( ResFixture, ConstructionDestruction )
	{
		observer.history.clear();

		std::size_t startTokens = 42;
		std::size_t maxTokens = 100;
		data::Label label = "ResTestConstructionWithUserSim";
		{
			Res testRes2( sim, label, startTokens, maxTokens, &observer );
			CHECK_EQUAL( label, testRes2.getLabel() );
			CHECK_EQUAL( startTokens, testRes2.getTokenNumber() );
			CHECK_EQUAL( maxTokens, testRes2.getTokenLimit() );
			CHECK_EQUAL( &observer, testRes2.getObservers().front() );
			CHECK( log->getTraceRecord( "create", type ) );
			CHECK( log->getStatisticsRecord( "parameter", "queue", type ) );
			CHECK( log->getStatisticsRecord( "parameter", "initial tokens", type ) );
			CHECK( log->getStatisticsRecord( "parameter", "initial token limit", type ) );
			CHECK( log->getStatisticsRecord( "update", "tokens", type ) );
			CHECK( observer.observed( "onCreate", testRes2.getLabel() ) );
		}
		CHECK( log->getTraceRecord( "destroy", type ) );
	}

	/**
	 * @test odemx::Res::acquire(unsigned int) success
	 *
	 * Expected function call effects:
	 * @li non-Process objects cause an error message
	 * @li the token number is decreased by the amount taken
	 * @li token number change and take success can be observed
	 * @li on success, the function returns the amount taken
	 * @li usage is registered in statistics
	 */
	TEST_FIXTURE( ResFixture, AcquireSucceed )
	{
		unsigned int oldTokenNumber = res.getTokenNumber();
		unsigned int takeCount = oldTokenNumber / 2 + 1;

		res.acquire( takeCount );
		CHECK( log->getErrorRecord( "Res::acquire(): called by non-Process object", type ) );

		ResTaker taker( sim, "ResTestTakeProcess", res, takeCount );
		taker.activate();
		sim.step();
		CHECK_EQUAL( oldTokenNumber - takeCount, res.getTokenNumber() );
		CHECK( log->getTraceRecord( "change token number", type ) );
		CHECK( log->getTraceRecord( "acquire succeeded", type ) );
		CHECK( log->getStatisticsRecord( "count", "users", type ) );
		CHECK( log->getStatisticsRecord( "update", "tokens", type ) );
		CHECK( log->getStatisticsRecord( "update", "wait time", type ) );
		CHECK( observer.observed( "onChangeTokenNumber", res.getLabel(),
				toString( oldTokenNumber ), toString( res.getTokenNumber() ) ) );
		CHECK( observer.observed( "onAcquireSucceed", res.getLabel(), toString( takeCount ) ) );
		CHECK_EQUAL( takeCount, taker.received );
	}

	/**
	 * @test odemx::Res::acquire(unsigned int) failure
	 *
	 * Expected function call effects:
	 * @li a failing call can be observed
	 * @li the Process is inserted into a queue
	 * @li the Process is suspended, i.e. in state IDLE
	 * @li on Process interrupt, the function returns 0 and the Process is
	 * removed from the waiting queue
	 * @li waiting processes are awakened properly upon token increase
	 */
	TEST_FIXTURE( ResFixture, AcquireFail )
	{
		unsigned int oldTokenNumber = res.getTokenNumber();
		unsigned int takeCount = oldTokenNumber + 1;

		ResTaker taker( sim, "ResTestTakeProcess", res, takeCount );
		taker.activate();
		sim.step();

		CHECK( log->getTraceRecord( "acquire failed", type ) );
		CHECK( observer.observed( "onAcquireFail", res.getLabel(), toString( takeCount ) ) );
		CHECK_EQUAL( (std::size_t)1, res.getWaitingProcesses().size() );
		CHECK_EQUAL( &taker, res.getWaitingProcesses().front() );
		CHECK_EQUAL( base::Process::IDLE, taker.getProcessState() );

		SuiteBase::InterruptEvent interrupt( sim, &taker );
		interrupt.schedule();
		sim.step();
		CHECK_EQUAL( base::Process::RUNNABLE, taker.getProcessState() );
		sim.step();
		CHECK_EQUAL( (unsigned int)0, taker.received );
		CHECK_EQUAL( (std::size_t)0, res.getWaitingProcesses().size() );

		ResTaker taker2( sim, "ResTestTakeProcess", res, takeCount );
		taker2.hold();
		ResTaker taker3( sim, "ResTestTakeProcess", res, takeCount );
		taker3.hold();
		sim.step();
		sim.step();
		CHECK_EQUAL( (std::size_t)2, res.getWaitingProcesses().size() );

		res.release( takeCount * 2 );
		sim.run();
		CHECK_EQUAL( takeCount, taker2.received );
		CHECK_EQUAL( takeCount, taker3.received );
		CHECK_EQUAL( (std::size_t)0, res.getWaitingProcesses().size() );
	}

	/**
	 * @test odemx::Res::acquire(unsigned int) block warning
	 *
	 * Expected function call effects:
	 * @li non-Process objects cause an error message
	 * @li the token number is decreased by the amount taken
	 * @li token number change and take success can be observed
	 * @li on success, the function returns the amount taken
	 * @li usage is registered in statistics
	 */
	TEST_FIXTURE( ResFixture, AcquireBlockWarning )
	{
		unsigned int takeCount = res.getTokenLimit() + 1;

		ResTaker taker( sim, "ResTestTakeProcess", res, takeCount );
		taker.activate();
		sim.step();
		CHECK( log->getWarningRecord( "Res::acquire(): requesting more than max tokens will always block", type ) );
	}

	/**
	 * @test odemx::Res::release(unsigned int)
	 *
	 * Expected function call effects:
	 * @li token number change and release call can be observed
	 * @li waiting processes are awakened properly upon token increase
	 */
	TEST_FIXTURE( ResFixture, Release )
	{
		unsigned int oldTokenNumber = res.getTokenNumber();
		unsigned int releaseCount = res.getTokenLimit() + 1;
		unsigned int actuallyReleased = res.getTokenLimit() - res.getTokenNumber();

		ResTaker taker( sim, "ResTestReleaseProcess", res, oldTokenNumber + 1 );
		taker.hold();
		sim.step();
		CHECK_EQUAL( (std::size_t)1, res.getWaitingProcesses().size() );

		res.release( releaseCount );
		CHECK( log->getErrorRecord( "Res::release(): limit reached, could not release all tokens", type ) );
		CHECK( log->getTraceRecord( "release failed", type ) );
		CHECK( log->getTraceRecord( "change token number", type ) );
		CHECK( log->getStatisticsRecord( "count", "providers", type ) );
		CHECK( log->getStatisticsRecord( "update", "tokens", type ) );
		CHECK( log->getTraceRecord( "release succeeded", type ) );
		CHECK( observer.observed( "onReleaseFail", res.getLabel(), toString( releaseCount ) ) );
		CHECK( observer.observed( "onChangeTokenNumber", res.getLabel(), \
				toString( oldTokenNumber ), toString( res.getTokenNumber() ) ) );
		CHECK( observer.observed( "onReleaseSucceed", res.getLabel(), toString( actuallyReleased ) ) );

		sim.run();
		CHECK_EQUAL( oldTokenNumber + 1, taker.received );
	}

	/**
	 * @test odemx::Res::control(unsigned int)
	 *
	 * Expected function call effects:
	 * @li the token limit is increased by the given amount
	 * @li the number of given tokens is successfully released to the resource
	 */
	TEST_FIXTURE( ResFixture, Control )
	{
		unsigned int increase = 23;
		unsigned int oldLimit = res.getTokenLimit();

		res.control( increase );
		CHECK_EQUAL( oldLimit + increase, res.getTokenLimit() );
		CHECK( log->getTraceRecord( "increase token limit", type ) );
		CHECK( observer.observed( "onControl", res.getLabel(), toString( increase ) ) );
	}

	/**
	 * @test odemx::Res::unControl(unsigned int)
	 *
	 * Expected function call effects:
	 * @li the token limit is decreased by the given amount
	 * @li the number of given tokens is successfully acquired from the resource
	 */
	TEST_FIXTURE( ResFixture, UnControl )
	{
		int decrease = res.getTokenNumber();
		unsigned int oldLimit = res.getTokenLimit();

		// make sure this Process must wait
		ResTestUnControlProcess interruptedUnController( sim, "ResTestUnControlProcess", res, res.getTokenNumber() + 1 );
		interruptedUnController.activate();
		sim.step();

		SuiteBase::InterruptEvent interrupt( sim, &interruptedUnController );
		interrupt.schedule();
		sim.step(); // event activation
		sim.step(); // process activation
		CHECK_EQUAL( 0, interruptedUnController.unControlReturnValue );

		ResTestUnControlProcess unController( sim, "ResTestUnControlProcess", res, decrease );
		unController.activate();
		sim.step();

		CHECK_EQUAL( decrease, unController.unControlReturnValue );
		CHECK_EQUAL( oldLimit - decrease, res.getTokenLimit() );
		CHECK( log->getTraceRecord( "decrease token limit", type ) );
		CHECK( observer.observed( "onUnControl", res.getLabel(), toString( decrease ) ) );
	}

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