#include "VideoPlayoutScheduler.h"

#include <cmath>
#include <iostream>

namespace
{
int gFailures = 0;

void Expect(bool condition, const char* message)
{
	if (condition)
		return;

	std::cerr << "FAIL: " << message << "\n";
	++gFailures;
}

void ExpectNear(double actual, double expected, double tolerance, const char* message)
{
	Expect(std::fabs(actual - expected) <= tolerance, message);
}

void TestScheduleAdvancesFromZero()
{
	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1001, 60000);

	const VideoIOScheduleTime first = scheduler.NextScheduleTime();
	const VideoIOScheduleTime second = scheduler.NextScheduleTime();
	const VideoIOScheduleTime third = scheduler.NextScheduleTime();

	Expect(first.streamTime == 0, "first frame starts at stream time zero");
	Expect(first.duration == 1001, "duration is preserved");
	Expect(first.timeScale == 60000, "time scale is preserved");
	Expect(second.streamTime == 1001, "second frame advances by one duration");
	Expect(third.streamTime == 2002, "third frame advances by two durations");
}

void TestLateAndDroppedRecoveryUsesMeasuredPressure()
{
	VideoPlayoutPolicy policy;
	policy.lateOrDropCatchUpFrames = 2;

	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000, policy);

	(void)scheduler.NextScheduleTime();
	VideoPlayoutRecoveryDecision lateDecision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate, 2);
	Expect(lateDecision.catchUpFrames == 1, "single late completion catches up by measured one-frame lag");
	Expect(lateDecision.lateStreak == 1, "late completion increments late streak");
	Expect(scheduler.NextScheduleTime().streamTime == 2000, "single late recovery advances by measured lag");

	VideoPlayoutRecoveryDecision dropDecision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped, 2);
	Expect(dropDecision.catchUpFrames == 2, "dropped completion catches up by measured drop pressure");
	Expect(dropDecision.lateStreak == 0, "dropped completion resets late streak");
	Expect(dropDecision.dropStreak == 1, "dropped completion increments drop streak");
	Expect(scheduler.NextScheduleTime().streamTime == 5000, "drop recovery advances by measured lag");
}

void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
{
	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000);

	(void)scheduler.NextScheduleTime();
	VideoPlayoutRecoveryDecision decision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped, 0);
	Expect(decision.measuredLagFrames > 0, "default policy still measures dropped-frame lag");
	Expect(decision.catchUpFrames == 0, "default policy does not skip schedule time");
	Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
}

void TestScheduleCursorCanAlignToPlaybackClock()
{
	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000);

	(void)scheduler.NextScheduleTime();
	scheduler.AlignNextScheduleTimeToPlayback(10000, 4);
	Expect(scheduler.NextScheduleTime().streamTime == 14000, "schedule cursor skips stale stream time after underfeed");

	scheduler.AlignNextScheduleTimeToPlayback(11000, 1);
	Expect(scheduler.NextScheduleTime().streamTime == 15000, "schedule cursor does not move backward");
}

void TestMeasuredRecoveryIsCappedByPolicy()
{
	VideoPlayoutPolicy policy;
	policy.lateOrDropCatchUpFrames = 1;

	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000, policy);

	(void)scheduler.NextScheduleTime();
	VideoPlayoutRecoveryDecision decision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped, 0);
	Expect(decision.measuredLagFrames > decision.catchUpFrames, "policy caps measured recovery");
	Expect(decision.catchUpFrames == 1, "drop recovery obeys policy cap");
	Expect(scheduler.NextScheduleTime().streamTime == 2000, "capped recovery advances by one frame");
}

void TestCleanCompletionTracksCompletedIndexAndClearsStreaks()
{
	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000);

	(void)scheduler.NextScheduleTime();
	(void)scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate, 2);
	VideoPlayoutRecoveryDecision decision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Completed, 2);
	Expect(decision.completedFrameIndex == 2, "completion accounting tracks completed index");
	Expect(decision.catchUpFrames == 0, "clean completion does not catch up");
	Expect(decision.lateStreak == 0, "clean completion clears late streak");
	Expect(decision.dropStreak == 0, "clean completion keeps drop streak clear");
}

void TestPolicyNormalization()
{
	VideoPlayoutPolicy policy;
	policy.outputFramePoolSize = 0;
	policy.targetPrerollFrames = 0;
	policy.targetReadyFrames = 5;
	policy.maxReadyFrames = 2;

	VideoPlayoutPolicy normalized = NormalizeVideoPlayoutPolicy(policy);
	Expect(normalized.targetPrerollFrames == 1, "policy normalization keeps at least one preroll frame");
	Expect(normalized.maxReadyFrames == normalized.targetReadyFrames, "policy normalization keeps max ready frames above target");
	Expect(normalized.outputFramePoolSize >= normalized.targetPrerollFrames + normalized.maxReadyFrames + normalized.minimumSpareDeviceFrames,
		"policy normalization keeps enough output frames for preroll and ready queue ownership");
}

void TestFrameBudgets()
{
	VideoPlayoutScheduler scheduler;
	scheduler.Configure(1000, 50000);
	ExpectNear(scheduler.FrameBudgetMilliseconds(), 20.0, 0.0001, "50 fps budget");

	scheduler.Configure(1001, 60000);
	ExpectNear(scheduler.FrameBudgetMilliseconds(), 16.6833, 0.0001, "59.94 fps budget");

	scheduler.Configure(1, 60);
	ExpectNear(scheduler.FrameBudgetMilliseconds(), 16.6667, 0.0001, "60 fps budget");
}
}

int main()
{
	TestScheduleAdvancesFromZero();
	TestLateAndDroppedRecoveryUsesMeasuredPressure();
	TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
	TestScheduleCursorCanAlignToPlaybackClock();
	TestMeasuredRecoveryIsCappedByPolicy();
	TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
	TestPolicyNormalization();
	TestFrameBudgets();

	if (gFailures != 0)
	{
		std::cerr << gFailures << " VideoPlayoutScheduler test failure(s).\n";
		return 1;
	}

	std::cout << "VideoPlayoutScheduler tests passed.\n";
	return 0;
}
