#include "VideoOutputThread.h"

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

namespace
{
int gFailures = 0;

void Expect(bool condition, const char* message)
{
	if (condition)
		return;
	std::cerr << "FAIL: " << message << "\n";
	++gFailures;
}

struct FakeExchangeMetrics
{
	std::size_t scheduledCount = 0;
};

class FakeExchange
{
public:
	FakeExchange()
	{
		mFrame.bytes = mBytes;
		mFrame.rowBytes = 8;
		mFrame.width = 2;
		mFrame.height = 2;
		mFrame.pixelFormat = VideoIOPixelFormat::Bgra8;
	}

	FakeExchangeMetrics Metrics() const
	{
		FakeExchangeMetrics metrics;
		metrics.scheduledCount = mScheduled.load();
		return metrics;
	}

	bool ConsumeCompletedForSchedule(SystemFrame& frame)
	{
		bool expected = true;
		if (!mHasFrame.compare_exchange_strong(expected, false))
			return false;
		frame = mFrame;
		mScheduled.fetch_add(1);
		return true;
	}

	void ReleaseScheduledByBytes(void*)
	{
		mReleased.fetch_add(1);
	}

	std::atomic<std::size_t> mScheduled{ 0 };
	std::atomic<std::size_t> mReleased{ 0 };

private:
	std::atomic<bool> mHasFrame{ true };
	unsigned char mBytes[16] = {};
	SystemFrame mFrame;
};

class FakeOutputEdge final : public RenderCadenceCompositor::IVideoOutputEdge
{
public:
	bool StartScheduledPlayback(std::string&) override { return true; }
	bool ScheduleFrame(const VideoIOOutputFrame& frame) override
	{
		mLastWidth = frame.width;
		mScheduled.fetch_add(1);
		return mScheduleResult;
	}
	void Stop() override {}
	void ReleaseResources() override {}
	const VideoIOState& State() const override { return mState; }
	RenderCadenceCompositor::VideoOutputEdgeMetrics Metrics() const override { return mMetrics; }

	bool mScheduleResult = true;
	std::atomic<std::size_t> mScheduled{ 0 };
	unsigned mLastWidth = 0;

private:
	VideoIOState mState;
	RenderCadenceCompositor::VideoOutputEdgeMetrics mMetrics;
};

void TestSchedulesCompletedFrameThroughGenericEdge()
{
	FakeExchange exchange;
	FakeOutputEdge output;
	RenderCadenceCompositor::VideoOutputThreadConfig config;
	config.targetBufferedFrames = 1;
	config.idleSleep = std::chrono::milliseconds(1);

	RenderCadenceCompositor::VideoOutputThread<FakeExchange> thread(output, exchange, config);
	Expect(thread.Start(), "video output thread starts");
	std::this_thread::sleep_for(std::chrono::milliseconds(20));
	thread.Stop();

	const auto metrics = thread.Metrics();
	Expect(output.mScheduled.load() == 1, "generic output edge receives one scheduled frame");
	Expect(output.mLastWidth == 2, "scheduled frame data comes from exchange");
	Expect(metrics.scheduledFrames == 1, "thread scheduled frame metric increments");
	Expect(metrics.scheduleFailures == 0, "no schedule failure is reported");
	Expect(exchange.mReleased.load() == 0, "successful schedule keeps frame scheduled");
}

void TestReleasesFrameWhenGenericEdgeRejectsSchedule()
{
	FakeExchange exchange;
	FakeOutputEdge output;
	output.mScheduleResult = false;
	RenderCadenceCompositor::VideoOutputThreadConfig config;
	config.targetBufferedFrames = 1;
	config.idleSleep = std::chrono::milliseconds(1);

	RenderCadenceCompositor::VideoOutputThread<FakeExchange> thread(output, exchange, config);
	Expect(thread.Start(), "video output thread starts for failure path");
	std::this_thread::sleep_for(std::chrono::milliseconds(20));
	thread.Stop();

	const auto metrics = thread.Metrics();
	Expect(output.mScheduled.load() == 1, "generic output edge receives failed schedule attempt");
	Expect(metrics.scheduledFrames == 0, "failed schedule does not increment scheduled frames");
	Expect(metrics.scheduleFailures == 1, "schedule failure metric increments");
	Expect(exchange.mReleased.load() == 1, "failed schedule releases the frame back to exchange");
}
}

int main()
{
	TestSchedulesCompletedFrameThroughGenericEdge();
	TestReleasesFrameWhenGenericEdgeRejectsSchedule();

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

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