#include "CadenceTelemetry.h"
#include "CadenceTelemetryJson.h"

#include <chrono>
#include <cstdint>
#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 freeCount = 0;
	std::size_t completedCount = 0;
	std::size_t scheduledCount = 0;
	uint64_t completedFrames = 0;
	uint64_t scheduledFrames = 0;
	uint64_t completedDrops = 0;
	uint64_t acquireMisses = 0;
};

struct FakeExchange
{
	FakeExchangeMetrics metrics;
	FakeExchangeMetrics Metrics() const { return metrics; }
};

struct FakeOutputThreadMetrics
{
	uint64_t completedPollMisses = 0;
	uint64_t scheduleFailures = 0;
};

struct FakeOutputThread
{
	FakeOutputThreadMetrics metrics;
	FakeOutputThreadMetrics Metrics() const { return metrics; }
};

struct FakeOutputMetrics
{
	uint64_t completions = 0;
	uint64_t displayedLate = 0;
	uint64_t dropped = 0;
	uint64_t scheduleFailures = 0;
	bool actualBufferedFramesAvailable = false;
	uint64_t actualBufferedFrames = 0;
	double scheduleCallMilliseconds = 0.0;
	bool scheduleLeadAvailable = false;
	int64_t playbackStreamTime = 0;
	uint64_t playbackFrameIndex = 0;
	uint64_t nextScheduleFrameIndex = 0;
	int64_t scheduleLeadFrames = 0;
	uint64_t scheduleRealignmentCount = 0;
};

struct FakeOutput
{
	FakeOutputMetrics metrics;
	FakeOutputMetrics Metrics() const { return metrics; }
};

struct FakeRenderThreadMetrics
{
	uint64_t clockOverruns = 0;
	uint64_t skippedFrames = 0;
	uint64_t shaderBuildsCommitted = 0;
	uint64_t shaderBuildFailures = 0;
	double renderFrameMilliseconds = 0.0;
	double renderFrameBudgetUsedPercent = 0.0;
	double renderFrameMaxMilliseconds = 0.0;
	double readbackQueueMilliseconds = 0.0;
	double completedReadbackCopyMilliseconds = 0.0;
	uint64_t inputFramesReceived = 0;
	uint64_t inputFramesDropped = 0;
	uint64_t inputConsumeMisses = 0;
	uint64_t inputUploadMisses = 0;
	std::size_t inputReadyFrames = 0;
	std::size_t inputReadingFrames = 0;
	double inputLatestAgeMilliseconds = 0.0;
	double inputUploadMilliseconds = 0.0;
	bool inputFormatSupported = true;
	bool inputSignalPresent = false;
};

struct FakeRenderThread
{
	FakeRenderThreadMetrics metrics;
	FakeRenderThreadMetrics GetMetrics() const { return metrics; }
};

void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
{
	RenderCadenceCompositor::CadenceTelemetry telemetry;
	FakeExchange exchange;
	exchange.metrics.freeCount = 7;
	exchange.metrics.completedCount = 1;
	exchange.metrics.scheduledCount = 4;
	exchange.metrics.completedFrames = 100;
	exchange.metrics.scheduledFrames = 96;
	exchange.metrics.completedDrops = 2;
	exchange.metrics.acquireMisses = 3;

	FakeOutput output;
	output.metrics.actualBufferedFramesAvailable = true;
	output.metrics.actualBufferedFrames = 4;
	output.metrics.scheduleLeadAvailable = true;
	output.metrics.playbackStreamTime = 10010;
	output.metrics.playbackFrameIndex = 10;
	output.metrics.nextScheduleFrameIndex = 14;
	output.metrics.scheduleLeadFrames = 4;
	output.metrics.scheduleRealignmentCount = 1;

	FakeOutputThread outputThread;
	outputThread.metrics.completedPollMisses = 12;
	outputThread.metrics.scheduleFailures = 0;

	FakeRenderThread renderThread;
	renderThread.metrics.clockOverruns = 5;
	renderThread.metrics.skippedFrames = 8;
	renderThread.metrics.shaderBuildsCommitted = 1;
	renderThread.metrics.shaderBuildFailures = 0;
	renderThread.metrics.renderFrameMilliseconds = 2.5;
	renderThread.metrics.renderFrameBudgetUsedPercent = 15.0;
	renderThread.metrics.renderFrameMaxMilliseconds = 4.0;
	renderThread.metrics.readbackQueueMilliseconds = 0.6;
	renderThread.metrics.completedReadbackCopyMilliseconds = 1.2;
	renderThread.metrics.inputFramesReceived = 9;
	renderThread.metrics.inputFramesDropped = 2;
	renderThread.metrics.inputConsumeMisses = 3;
	renderThread.metrics.inputUploadMisses = 4;
	renderThread.metrics.inputReadyFrames = 1;
	renderThread.metrics.inputReadingFrames = 0;
	renderThread.metrics.inputLatestAgeMilliseconds = 4.5;
	renderThread.metrics.inputUploadMilliseconds = 0.25;
	renderThread.metrics.inputFormatSupported = true;
	renderThread.metrics.inputSignalPresent = true;

	const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
	Expect(snapshot.freeFrames == 7, "free frame count is sampled");
	Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
	Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
	Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
	Expect(snapshot.completedDrops == 2, "completed drops are sampled");
	Expect(snapshot.acquireMisses == 3, "acquire misses are sampled");
	Expect(snapshot.clockOverruns == 5, "clock overrun count is sampled");
	Expect(snapshot.clockSkippedFrames == 8, "clock skipped frame count is sampled");
	Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
	Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
	Expect(snapshot.renderFrameMilliseconds == 2.5, "render frame timing is sampled");
	Expect(snapshot.renderFrameBudgetUsedPercent == 15.0, "render budget percentage is sampled");
	Expect(snapshot.renderFrameMaxMilliseconds == 4.0, "render frame max timing is sampled");
	Expect(snapshot.readbackQueueMilliseconds == 0.6, "readback queue timing is sampled");
	Expect(snapshot.completedReadbackCopyMilliseconds == 1.2, "completed readback copy timing is sampled");
	Expect(snapshot.inputFramesReceived == 9, "input received count is sampled");
	Expect(snapshot.inputFramesDropped == 2, "input dropped count is sampled");
	Expect(snapshot.inputConsumeMisses == 3, "input consume miss count is sampled");
	Expect(snapshot.inputUploadMisses == 4, "input upload miss count is sampled");
	Expect(snapshot.inputReadyFrames == 1, "input ready frame count is sampled");
	Expect(snapshot.inputReadingFrames == 0, "input reading frame count is sampled");
	Expect(snapshot.inputLatestAgeMilliseconds == 4.5, "input latest age is sampled");
	Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
	Expect(snapshot.inputFormatSupported, "input format support is sampled");
	Expect(snapshot.inputSignalPresent, "input signal present is sampled");
	Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
	Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
	Expect(snapshot.deckLinkScheduleLeadAvailable, "schedule lead availability is sampled");
	Expect(snapshot.deckLinkPlaybackStreamTime == 10010, "playback stream time is sampled");
	Expect(snapshot.deckLinkPlaybackFrameIndex == 10, "playback frame index is sampled");
	Expect(snapshot.deckLinkNextScheduleFrameIndex == 14, "next schedule frame index is sampled");
	Expect(snapshot.deckLinkScheduleLeadFrames == 4, "schedule lead frames are sampled");
	Expect(snapshot.deckLinkScheduleRealignments == 1, "schedule realignment count is sampled");
}

void TestTelemetryComputesRatesFromDeltas()
{
	RenderCadenceCompositor::CadenceTelemetry telemetry;
	FakeExchange exchange;
	FakeOutput output;
	FakeOutputThread outputThread;
	FakeRenderThread renderThread;

	exchange.metrics.completedFrames = 10;
	exchange.metrics.scheduledFrames = 10;
	(void)telemetry.Sample(exchange, output, outputThread, renderThread);

	std::this_thread::sleep_for(std::chrono::milliseconds(5));

	exchange.metrics.completedFrames = 20;
	exchange.metrics.scheduledFrames = 19;
	const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
	Expect(snapshot.sampleSeconds > 0.0, "second telemetry sample has elapsed time");
	Expect(snapshot.renderFps > 0.0, "render fps is computed from completed frame delta");
	Expect(snapshot.scheduleFps > 0.0, "schedule fps is computed from scheduled frame delta");
}

void TestTelemetrySerializesToJson()
{
	RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot;
	snapshot.sampleSeconds = 1.0;
	snapshot.renderFps = 59.94;
	snapshot.scheduleFps = 60.0;
	snapshot.freeFrames = 7;
	snapshot.completedFrames = 1;
	snapshot.scheduledFrames = 4;
	snapshot.renderedTotal = 120;
	snapshot.scheduledTotal = 118;
	snapshot.completedPollMisses = 3;
	snapshot.scheduleFailures = 0;
	snapshot.completedDrops = 4;
	snapshot.acquireMisses = 5;
	snapshot.completions = 117;
	snapshot.displayedLate = 1;
	snapshot.dropped = 2;
	snapshot.clockOverruns = 3;
	snapshot.clockSkippedFrames = 5;
	snapshot.shaderBuildsCommitted = 1;
	snapshot.shaderBuildFailures = 0;
	snapshot.renderFrameMilliseconds = 2.5;
	snapshot.renderFrameBudgetUsedPercent = 15.0;
	snapshot.renderFrameMaxMilliseconds = 4.0;
	snapshot.readbackQueueMilliseconds = 0.6;
	snapshot.completedReadbackCopyMilliseconds = 1.2;
	snapshot.inputFramesReceived = 10;
	snapshot.inputFramesDropped = 1;
	snapshot.inputConsumeMisses = 2;
	snapshot.inputUploadMisses = 3;
	snapshot.inputReadyFrames = 1;
	snapshot.inputReadingFrames = 0;
	snapshot.inputLatestAgeMilliseconds = 3.5;
	snapshot.inputUploadMilliseconds = 0.75;
	snapshot.inputFormatSupported = true;
	snapshot.inputSignalPresent = true;
	snapshot.inputCaptureFps = 59.94;
	snapshot.inputConvertMilliseconds = 4.25;
	snapshot.inputSubmitMilliseconds = 0.35;
	snapshot.inputNoSignalFrames = 2;
	snapshot.inputUnsupportedFrames = 3;
	snapshot.inputSubmitMisses = 4;
	snapshot.inputCaptureFormat = "UYVY8";
	snapshot.deckLinkBufferedAvailable = true;
	snapshot.deckLinkBuffered = 4;
	snapshot.deckLinkScheduleCallMilliseconds = 1.25;
	snapshot.deckLinkScheduleLeadAvailable = true;
	snapshot.deckLinkScheduleLeadFrames = 4;
	snapshot.deckLinkPlaybackFrameIndex = 10;
	snapshot.deckLinkNextScheduleFrameIndex = 14;
	snapshot.deckLinkPlaybackStreamTime = 10010;
	snapshot.deckLinkScheduleRealignments = 1;

	const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
	const std::string expected =
		"{\"sampleSeconds\":1,\"renderFps\":59.94,\"scheduleFps\":60,"
		"\"free\":7,\"completed\":1,\"scheduled\":4,"
		"\"renderedTotal\":120,\"scheduledTotal\":118,"
		"\"completedPollMisses\":3,\"scheduleFailures\":0,"
		"\"completedDrops\":4,\"acquireMisses\":5,"
		"\"completions\":117,\"late\":1,\"dropped\":2,"
		"\"clockOverruns\":3,\"clockSkippedFrames\":5,"
		"\"clockOveruns\":3,\"clockSkipped\":5,"
		"\"shaderCommitted\":1,\"shaderFailures\":0,"
		"\"renderFrameMs\":2.5,\"renderFrameBudgetUsedPercent\":15,"
		"\"renderFrameMaxMs\":4,\"readbackQueueMs\":0.6,"
		"\"completedReadbackCopyMs\":1.2,"
		"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
		"\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"
		"\"inputReadyFrames\":1,\"inputReadingFrames\":0,"
		"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
		"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
		"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
		"\"inputSubmitMs\":0.35,\"inputNoSignalFrames\":2,"
		"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
		"\"inputCaptureFormat\":\"UYVY8\","
		"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
		"\"scheduleCallMs\":1.25,"
		"\"deckLinkScheduleLeadAvailable\":true,"
		"\"deckLinkScheduleLeadFrames\":4,"
		"\"deckLinkPlaybackFrameIndex\":10,"
		"\"deckLinkNextScheduleFrameIndex\":14,"
		"\"deckLinkPlaybackStreamTime\":10010,"
		"\"deckLinkScheduleRealignments\":1}";
	Expect(json == expected, "telemetry snapshot serializes to stable JSON");
}

void TestUnavailableDeckLinkBufferSerializesAsNull()
{
	RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot;
	snapshot.deckLinkBufferedAvailable = false;

	const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
	Expect(
		json.find("\"deckLinkBufferedAvailable\":false,\"deckLinkBuffered\":null") != std::string::npos,
		"unavailable DeckLink buffer depth serializes as null");
}
}

int main()
{
	TestTelemetrySamplesCompletedPollMissesAndShaderCounts();
	TestTelemetryComputesRatesFromDeltas();
	TestTelemetrySerializesToJson();
	TestUnavailableDeckLinkBufferSerializesAsNull();

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

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