#include "RuntimeEvent.h"
#include "RuntimeEventCoalescingQueue.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeEventQueue.h"
#include "RuntimeEventType.h"
#include "RuntimeEventPayloads.h"
#include "RuntimeEventTestHarness.h"

#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string_view>
#include <variant>

namespace
{
int gFailures = 0;

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

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

void TestRuntimeEventTypeNames()
{
	Expect(RuntimeEventTypeName(RuntimeEventType::Unknown) == "Unknown", "unknown event type has a stable name");
	Expect(RuntimeEventTypeName(RuntimeEventType::OscCommitRequested) == "OscCommitRequested", "control event name is stable");
	Expect(RuntimeEventTypeName(RuntimeEventType::RuntimeMutationAccepted) == "RuntimeMutationAccepted", "runtime event name is stable");
	Expect(RuntimeEventTypeName(RuntimeEventType::ShaderBuildPrepared) == "ShaderBuildPrepared", "shader build event name is stable");
	Expect(RuntimeEventTypeName(RuntimeEventType::RenderSnapshotPublished) == "RenderSnapshotPublished", "render event name is stable");
	Expect(RuntimeEventTypeName(RuntimeEventType::BackendStateChanged) == "BackendStateChanged", "backend event name is stable");
	Expect(RuntimeEventTypeName(RuntimeEventType::QueueDepthChanged) == "QueueDepthChanged", "telemetry event name is stable");
}

void TestRuntimeEventPayloadTypes()
{
	OscCommitRequestedEvent oscCommit;
	oscCommit.routeKey = "layer-1\namount";
	oscCommit.layerKey = "layer-1";
	oscCommit.parameterKey = "amount";
	oscCommit.generation = 42;
	Expect(RuntimeEventPayloadType(oscCommit) == RuntimeEventType::OscCommitRequested, "OSC commit payload maps to OSC commit event type");
	Expect(oscCommit.generation == 42, "OSC commit payload keeps generation");

	RuntimeMutationEvent acceptedMutation;
	acceptedMutation.action = "SetLayerShader";
	acceptedMutation.accepted = true;
	acceptedMutation.shaderBuildRequested = true;
	acceptedMutation.persistenceRequested = true;
	Expect(RuntimeEventPayloadType(acceptedMutation) == RuntimeEventType::RuntimeMutationAccepted, "accepted mutation payload maps to accepted event type");
	Expect(acceptedMutation.shaderBuildRequested, "mutation payload carries shader build follow-up");
	Expect(acceptedMutation.persistenceRequested, "mutation payload carries persistence follow-up");

	RuntimeMutationEvent rejectedMutation;
	rejectedMutation.accepted = false;
	rejectedMutation.errorMessage = "Unknown layer.";
	Expect(RuntimeEventPayloadType(rejectedMutation) == RuntimeEventType::RuntimeMutationRejected, "rejected mutation payload maps to rejected event type");
	Expect(rejectedMutation.errorMessage == "Unknown layer.", "mutation payload carries rejection error");

	RuntimePersistenceRequestedEvent persistence;
	persistence.request = PersistenceRequest::RuntimeStateRequest("UpdateLayerParameter");
	Expect(RuntimeEventPayloadType(persistence) == RuntimeEventType::RuntimePersistenceRequested, "runtime persistence payload maps to persistence event type");
	Expect(persistence.request.targetKind == PersistenceTargetKind::RuntimeState, "runtime persistence payload carries target kind");
	Expect(persistence.request.reason == "UpdateLayerParameter", "runtime persistence payload carries request reason");
	Expect(persistence.request.debounceAllowed, "runtime persistence payload carries debounce policy");
	Expect(persistence.request.debounceKey == "runtime-state", "runtime persistence payload carries debounce key");

	PersistenceSnapshot persistenceSnapshot;
	persistenceSnapshot.targetKind = PersistenceTargetKind::RuntimeState;
	persistenceSnapshot.reason = persistence.request.reason;
	persistenceSnapshot.contents = "{}";
	Expect(persistenceSnapshot.reason == "UpdateLayerParameter", "persistence snapshot carries capture reason");
	Expect(persistenceSnapshot.contents == "{}", "persistence snapshot carries serialized content");

	FileChangeDetectedEvent fileChange;
	fileChange.path = "PollRuntimeStoreChanges";
	fileChange.shaderPackageCandidate = true;
	Expect(RuntimeEventPayloadType(fileChange) == RuntimeEventType::FileChangeDetected, "file change payload maps to file change event type");
	Expect(fileChange.shaderPackageCandidate, "file change payload carries shader package candidate flag");

	ManualReloadRequestedEvent manualReload;
	manualReload.preserveFeedbackState = true;
	manualReload.reason = "RequestShaderReload";
	Expect(RuntimeEventPayloadType(manualReload) == RuntimeEventType::ManualReloadRequested, "manual reload payload maps to manual reload event type");
	Expect(manualReload.preserveFeedbackState, "manual reload payload carries feedback preservation policy");

	ShaderBuildEvent preparedBuild;
	preparedBuild.phase = RuntimeEventShaderBuildPhase::Prepared;
	preparedBuild.inputWidth = 1920;
	preparedBuild.inputHeight = 1080;
	Expect(RuntimeEventPayloadType(preparedBuild) == RuntimeEventType::ShaderBuildPrepared, "shader build payload maps by phase");
	Expect(preparedBuild.inputWidth == 1920 && preparedBuild.inputHeight == 1080, "shader build payload carries input dimensions");

	RenderResetEvent appliedReset;
	appliedReset.scope = RuntimeEventRenderResetScope::TemporalHistoryAndFeedback;
	appliedReset.applied = true;
	Expect(RuntimeEventPayloadType(appliedReset) == RuntimeEventType::RenderResetApplied, "render reset payload maps applied state");
	Expect(appliedReset.scope == RuntimeEventRenderResetScope::TemporalHistoryAndFeedback, "render reset payload carries reset scope");

	RenderSnapshotPublishRequestedEvent snapshotRequest;
	snapshotRequest.outputWidth = 1920;
	snapshotRequest.outputHeight = 1080;
	snapshotRequest.reason = "test";
	Expect(RuntimeEventPayloadType(snapshotRequest) == RuntimeEventType::RenderSnapshotPublishRequested, "render snapshot request payload maps to request event");

	RenderSnapshotPublishedEvent snapshotPublished;
	snapshotPublished.snapshotVersion = 3;
	snapshotPublished.parameterVersion = 4;
	snapshotPublished.layerCount = 2;
	Expect(RuntimeEventPayloadType(snapshotPublished) == RuntimeEventType::RenderSnapshotPublished, "render snapshot published payload maps to published event");
	Expect(snapshotPublished.layerCount == 2, "render snapshot published payload carries layer count");

	OutputFrameCompletedEvent completedFrame;
	completedFrame.result = "Completed";
	Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputFrameCompleted, "completed output frame payload maps to completed event");
	completedFrame.result = "DisplayedLate";
	Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputLateFrameDetected, "late output frame payload maps to late event");
	completedFrame.result = "Dropped";
	Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputDroppedFrameDetected, "dropped output frame payload maps to dropped event");

	TimingSampleRecordedEvent timingSample;
	timingSample.subsystem = "RuntimeEventDispatcher";
	timingSample.metric = "dispatchDuration";
	timingSample.value = 0.5;
	timingSample.unit = "ms";
	Expect(RuntimeEventPayloadType(timingSample) == RuntimeEventType::TimingSampleRecorded, "timing sample payload maps to timing event");

	QueueDepthChangedEvent queueDepth;
	queueDepth.queueName = "runtime-events";
	queueDepth.depth = 1;
	queueDepth.capacity = 16;
	Expect(RuntimeEventPayloadType(queueDepth) == RuntimeEventType::QueueDepthChanged, "queue depth payload maps to queue depth event");

	SubsystemWarningEvent warning;
	warning.subsystem = "VideoBackend";
	warning.warningKey = "late-frame";
	Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningRaised, "warning payload maps to raised event by default");
	warning.cleared = true;
	Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningCleared, "warning payload maps to cleared event when marked cleared");
}

void TestRuntimeEventEnvelope()
{
	const auto createdAt = std::chrono::steady_clock::now();

	OscCommitRequestedEvent oscCommit;
	oscCommit.routeKey = "layer-1\namount";
	oscCommit.layerKey = "layer-1";
	oscCommit.parameterKey = "amount";
	oscCommit.generation = 7;

	RuntimeEvent event = MakeRuntimeEvent(oscCommit, "ControlServices", 12, createdAt);
	Expect(event.type == RuntimeEventType::OscCommitRequested, "runtime event infers type from payload");
	Expect(event.sequence == 12, "runtime event stores sequence");
	Expect(event.source == "ControlServices", "runtime event stores source");
	Expect(event.createdAt == createdAt, "runtime event stores creation time");
	Expect(event.HasPayload(), "runtime event reports payload presence");
	Expect(event.PayloadMatchesType(), "runtime event payload matches inferred type");

	const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload);
	Expect(payload && payload->generation == 7, "runtime event stores typed payload in variant");

	event.type = RuntimeEventType::RuntimeMutationAccepted;
	Expect(!event.PayloadMatchesType(), "runtime event detects mismatched payload and type");

	RuntimeEvent empty;
	Expect(!empty.HasPayload(), "default runtime event has no payload");
	Expect(empty.PayloadMatchesType(), "default runtime event unknown type matches empty payload");

	RuntimeMutationEvent acceptedMutation;
	acceptedMutation.accepted = true;
	acceptedMutation.action = "SetLayerBypass";
	RuntimeEvent acceptedEvent = MakeRuntimeEvent(acceptedMutation, "RuntimeCoordinator", 13, createdAt);
	Expect(acceptedEvent.type == RuntimeEventType::RuntimeMutationAccepted, "runtime event handles payloads with dynamic accepted mapping");

	RuntimeMutationEvent rejectedMutation;
	rejectedMutation.accepted = false;
	rejectedMutation.action = "SetLayerBypass";
	RuntimeEvent rejectedEvent = MakeRuntimeEvent(rejectedMutation, "RuntimeCoordinator", 14, createdAt);
	Expect(rejectedEvent.type == RuntimeEventType::RuntimeMutationRejected, "runtime event handles payloads with dynamic rejected mapping");
}

void TestRuntimeEventQueue()
{
	RuntimeEventQueue queue(2);
	const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);

	RuntimeStateBroadcastRequestedEvent firstPayload;
	firstPayload.reason = "first";
	RuntimeStateBroadcastRequestedEvent secondPayload;
	secondPayload.reason = "second";
	RuntimeStateBroadcastRequestedEvent thirdPayload;
	thirdPayload.reason = "third";

	Expect(queue.Push(MakeRuntimeEvent(firstPayload, "test", 1, createdAt)), "queue accepts first event");
	Expect(queue.Push(MakeRuntimeEvent(secondPayload, "test", 2, createdAt)), "queue accepts second event");
	Expect(!queue.Push(MakeRuntimeEvent(thirdPayload, "test", 3, createdAt)), "queue rejects event when capacity is full");

	RuntimeEventQueueMetrics fullMetrics = queue.GetMetrics(std::chrono::steady_clock::now());
	Expect(fullMetrics.depth == 2, "queue metrics report depth");
	Expect(fullMetrics.capacity == 2, "queue metrics report capacity");
	Expect(fullMetrics.droppedCount == 1, "queue metrics report dropped count");
	Expect(fullMetrics.oldestEventAgeMilliseconds >= 0.0, "queue metrics report oldest event age");

	RuntimeEvent event;
	Expect(queue.TryPop(event), "queue pops first event");
	Expect(event.sequence == 1, "queue preserves FIFO order");

	std::vector<RuntimeEvent> drained = queue.Drain();
	Expect(drained.size() == 1, "queue drains remaining event");
	Expect(drained[0].sequence == 2, "drained event preserves FIFO order");
	Expect(queue.Depth() == 0, "queue is empty after drain");
}

void TestRuntimeEventDispatcher()
{
	RuntimeEventDispatcher dispatcher(4);
	int allHandlerCount = 0;
	int broadcastHandlerCount = 0;
	int failureHandlerCount = 0;

	dispatcher.SubscribeAll([&](const RuntimeEvent& event) {
		Expect(event.sequence != 0, "dispatcher assigns sequence before all-handler dispatch");
		++allHandlerCount;
	});
	dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent& event) {
		Expect(event.type == RuntimeEventType::RuntimeStateBroadcastRequested, "dispatcher invokes type-specific handler for matching event");
		++broadcastHandlerCount;
	});
	dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent&) {
		++failureHandlerCount;
		throw std::runtime_error("test handler failure");
	});

	RuntimeStateBroadcastRequestedEvent broadcast;
	broadcast.reason = "test";
	Expect(dispatcher.PublishPayload(broadcast, "test"), "dispatcher publishes payload");
	Expect(dispatcher.QueueDepth() == 1, "dispatcher reports queued depth");

	RuntimeEventDispatchResult result = dispatcher.DispatchPending();
	Expect(result.dispatchedEvents == 1, "dispatcher reports dispatched event count");
	Expect(result.handlerInvocations == 3, "dispatcher reports handler invocation count");
	Expect(result.handlerFailures == 1, "dispatcher catches and reports handler failures");
	Expect(allHandlerCount == 1, "dispatcher invoked all-handler");
	Expect(broadcastHandlerCount == 1, "dispatcher invoked type-specific handler");
	Expect(failureHandlerCount == 1, "dispatcher invoked failing handler");
	Expect(dispatcher.QueueDepth() == 0, "dispatcher queue is empty after dispatch");

	RuntimeEvent mismatched = MakeRuntimeEvent(broadcast, "test");
	mismatched.type = RuntimeEventType::ShaderBuildRequested;
	Expect(!dispatcher.Publish(mismatched), "dispatcher rejects mismatched event type and payload");

	RuntimeEventDispatcher tinyDispatcher(1);
	RuntimeMutationEvent acceptedMutation;
	acceptedMutation.accepted = true;
	Expect(tinyDispatcher.PublishPayload(acceptedMutation, "test"), "tiny dispatcher accepts first FIFO event");
	Expect(!tinyDispatcher.PublishPayload(acceptedMutation, "test"), "tiny dispatcher rejects FIFO event when queue is full");
	RuntimeEventQueueMetrics metrics = tinyDispatcher.GetQueueMetrics();
	Expect(metrics.droppedCount == 1, "dispatcher exposes queue drop metrics");
}

void TestRuntimeEventDispatcherCoalescing()
{
	RuntimeEventDispatcher dispatcher(4);
	std::string seenReason;
	std::string seenShaderMessage;
	double seenTimingValue = 0.0;
	int broadcastHandlerCount = 0;
	int shaderHandlerCount = 0;
	int timingHandlerCount = 0;

	dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent& event) {
		const auto* payload = std::get_if<RuntimeStateBroadcastRequestedEvent>(&event.payload);
		if (payload)
			seenReason = payload->reason;
		++broadcastHandlerCount;
	});
	dispatcher.Subscribe(RuntimeEventType::ShaderBuildRequested, [&](const RuntimeEvent& event) {
		const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload);
		if (payload)
			seenShaderMessage = payload->message;
		++shaderHandlerCount;
	});
	dispatcher.Subscribe(RuntimeEventType::TimingSampleRecorded, [&](const RuntimeEvent& event) {
		const auto* payload = std::get_if<TimingSampleRecordedEvent>(&event.payload);
		if (payload)
			seenTimingValue = payload->value;
		++timingHandlerCount;
	});

	RuntimeStateBroadcastRequestedEvent first;
	first.reason = "parameter";
	RuntimeStateBroadcastRequestedEvent second;
	second.reason = "reload";

	Expect(dispatcher.PublishPayload(first, "RuntimeCoordinator"), "dispatcher accepts first coalescable event");
	Expect(dispatcher.PublishPayload(second, "RuntimeCoordinator"), "dispatcher coalesces second matching event");
	RuntimeEventQueueMetrics queuedMetrics = dispatcher.GetQueueMetrics();
	Expect(queuedMetrics.depth == 1, "dispatcher reports coalesced event depth");
	Expect(queuedMetrics.coalescedCount == 1, "dispatcher reports coalesced event count");

	RuntimeEventDispatchResult result = dispatcher.DispatchPending();
	Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced event");
	Expect(broadcastHandlerCount == 1, "dispatcher invokes handler once for coalesced event");
	Expect(seenReason == "reload", "dispatcher dispatches latest coalesced payload");

	ShaderBuildEvent shaderFirst;
	shaderFirst.phase = RuntimeEventShaderBuildPhase::Requested;
	shaderFirst.inputWidth = 1920;
	shaderFirst.inputHeight = 1080;
	shaderFirst.preserveFeedbackState = true;
	shaderFirst.message = "first";
	ShaderBuildEvent shaderSecond = shaderFirst;
	shaderSecond.message = "second";
	Expect(dispatcher.PublishPayload(shaderFirst, "RuntimeCoordinator"), "dispatcher accepts first shader build request");
	Expect(dispatcher.PublishPayload(shaderSecond, "RuntimeCoordinator"), "dispatcher coalesces matching shader build request");
	result = dispatcher.DispatchPending();
	Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced shader build request");
	Expect(shaderHandlerCount == 1, "dispatcher invokes shader handler once for matching coalesced request");
	Expect(seenShaderMessage == "second", "dispatcher dispatches latest shader build request payload");

	TimingSampleRecordedEvent timingFirst;
	timingFirst.subsystem = "RuntimeEventDispatcher";
	timingFirst.metric = "dispatchDuration";
	timingFirst.value = 1.0;
	timingFirst.unit = "ms";
	TimingSampleRecordedEvent timingSecond = timingFirst;
	timingSecond.value = 2.0;
	Expect(dispatcher.PublishPayload(timingFirst, "HealthTelemetry"), "dispatcher accepts first timing sample");
	Expect(dispatcher.PublishPayload(timingSecond, "HealthTelemetry"), "dispatcher coalesces matching timing sample");
	result = dispatcher.DispatchPending();
	Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced timing sample");
	Expect(timingHandlerCount == 1, "dispatcher invokes timing handler once for matching coalesced sample");
	Expect(seenTimingValue == 2.0, "dispatcher dispatches latest timing sample payload");
}

void TestRuntimeEventCoalescingQueue()
{
	RuntimeEventCoalescingQueue queue(2);
	const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);

	OscValueReceivedEvent first;
	first.routeKey = "layer-1\namount";
	first.layerKey = "layer-1";
	first.parameterKey = "amount";
	first.valueJson = "0.1";
	first.generation = 1;

	OscValueReceivedEvent replacement = first;
	replacement.valueJson = "0.9";
	replacement.generation = 2;

	OscValueReceivedEvent otherRoute;
	otherRoute.routeKey = "layer-2\namount";
	otherRoute.layerKey = "layer-2";
	otherRoute.parameterKey = "amount";
	otherRoute.valueJson = "0.3";
	otherRoute.generation = 3;

	OscValueReceivedEvent overflow;
	overflow.routeKey = "layer-3\namount";
	overflow.layerKey = "layer-3";
	overflow.parameterKey = "amount";
	overflow.valueJson = "0.4";
	overflow.generation = 4;

	Expect(queue.Push(MakeRuntimeEvent(first, "ControlServices", 1, createdAt)), "coalescing queue accepts first keyed event");
	Expect(queue.Push(MakeRuntimeEvent(replacement, "ControlServices", 2, std::chrono::steady_clock::now())), "coalescing queue replaces matching keyed event");
	Expect(queue.Push(MakeRuntimeEvent(otherRoute, "ControlServices", 3, createdAt)), "coalescing queue accepts second keyed event");
	Expect(!queue.Push(MakeRuntimeEvent(overflow, "ControlServices", 4, createdAt)), "coalescing queue rejects new key when full");

	RuntimeEventCoalescingQueueMetrics metrics = queue.GetMetrics(std::chrono::steady_clock::now());
	Expect(metrics.depth == 2, "coalescing queue metrics report unique-key depth");
	Expect(metrics.capacity == 2, "coalescing queue metrics report capacity");
	Expect(metrics.coalescedCount == 1, "coalescing queue metrics report coalesced count");
	Expect(metrics.droppedCount == 1, "coalescing queue metrics report dropped count");
	Expect(metrics.oldestEventAgeMilliseconds >= 0.0, "coalescing queue metrics report oldest event age");

	std::vector<RuntimeEvent> drained = queue.Drain();
	Expect(drained.size() == 2, "coalescing queue drains unique events");
	Expect(drained[0].sequence == 2, "coalescing queue keeps latest replacement event");
	Expect(drained[1].sequence == 3, "coalescing queue preserves first-seen key order");

	const auto* latestPayload = std::get_if<OscValueReceivedEvent>(&drained[0].payload);
	Expect(latestPayload && latestPayload->valueJson == "0.9", "coalescing queue keeps latest payload value");
	Expect(queue.Depth() == 0, "coalescing queue is empty after drain");
}

void TestRuntimeEventCoalescingCustomKey()
{
	RuntimeEventCoalescingQueue queue(4, [](const RuntimeEvent& event) {
		return std::string(RuntimeEventTypeName(event.type));
	});

	RuntimeStateBroadcastRequestedEvent first;
	first.reason = "parameter";
	RuntimeStateBroadcastRequestedEvent second;
	second.reason = "reload";

	Expect(queue.Push(MakeRuntimeEvent(first, "RuntimeCoordinator", 10)), "custom-key coalescing queue accepts first event");
	Expect(queue.Push(MakeRuntimeEvent(second, "RuntimeCoordinator", 11)), "custom-key coalescing queue coalesces second event by type");

	std::vector<RuntimeEvent> drained = queue.Drain();
	Expect(drained.size() == 1, "custom-key coalescing queue drains one coalesced event");
	Expect(drained[0].sequence == 11, "custom-key coalescing queue keeps latest event");

	const auto* payload = std::get_if<RuntimeStateBroadcastRequestedEvent>(&drained[0].payload);
	Expect(payload && payload->reason == "reload", "custom-key coalescing queue keeps latest typed payload");
}

void TestRuntimeEventTestHarness()
{
	RuntimeEventTestHarness harness;

	RuntimeStateBroadcastRequestedEvent broadcast;
	broadcast.reason = "parameter";
	RuntimeEventDispatchResult firstDispatch = harness.PublishAndDispatch(broadcast, "RuntimeCoordinator");
	Expect(firstDispatch.dispatchedEvents == 1, "test harness publishes and dispatches payloads");
	Expect(harness.SeenCount() == 1, "test harness records dispatched events");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeStateBroadcastRequested) == 1, "test harness counts seen events by type");

	const RuntimeEvent* seenBroadcast = harness.LastSeen(RuntimeEventType::RuntimeStateBroadcastRequested);
	Expect(seenBroadcast && seenBroadcast->source == "RuntimeCoordinator", "test harness returns last seen event by type");

	harness.ClearSeen();
	Expect(harness.SeenCount() == 0, "test harness clears seen events");

	OscValueReceivedEvent first;
	first.routeKey = "layer-1\namount";
	first.layerKey = "layer-1";
	first.parameterKey = "amount";
	first.valueJson = "0.1";
	first.generation = 1;

	OscValueReceivedEvent replacement = first;
	replacement.valueJson = "0.8";
	replacement.generation = 2;

	Expect(harness.PublishCoalesced(first, "ControlServices", 20), "test harness accepts first coalesced payload");
	Expect(harness.PublishCoalesced(replacement, "ControlServices", 21), "test harness accepts replacement coalesced payload");
	RuntimeEventDispatchResult coalescedDispatch = harness.FlushCoalescedAndDispatch();
	Expect(coalescedDispatch.dispatchedEvents == 1, "test harness dispatches one coalesced event");
	Expect(harness.SeenCount(RuntimeEventType::OscValueReceived) == 1, "test harness records coalesced event");

	const RuntimeEvent* seenOsc = harness.LastSeen(RuntimeEventType::OscValueReceived);
	const auto* seenPayload = seenOsc ? std::get_if<OscValueReceivedEvent>(&seenOsc->payload) : nullptr;
	Expect(seenPayload && seenPayload->valueJson == "0.8", "test harness keeps latest coalesced payload");
}

void TestAcceptedMutationFollowUps()
{
	RuntimeEventTestHarness harness;

	RuntimeMutationEvent mutation;
	mutation.action = "SetLayerShader";
	mutation.accepted = true;
	mutation.runtimeStateChanged = true;
	mutation.runtimeStateBroadcastRequired = true;
	mutation.shaderBuildRequested = true;
	mutation.persistenceRequested = true;

	RuntimeStateChangedEvent stateChanged;
	stateChanged.reason = mutation.action;
	stateChanged.persistenceRequested = true;

	RuntimePersistenceRequestedEvent persistence;
	persistence.request = PersistenceRequest::RuntimeStateRequest(mutation.action);

	RuntimeReloadRequestedEvent reload;
	reload.reason = mutation.action;
	reload.preserveFeedbackState = false;

	ShaderBuildEvent build;
	build.phase = RuntimeEventShaderBuildPhase::Requested;
	build.succeeded = true;
	build.message = "Shader rebuild queued.";

	Expect(harness.Publish(mutation, "RuntimeCoordinator"), "accepted mutation event publishes");
	Expect(harness.Publish(stateChanged, "RuntimeCoordinator"), "state changed follow-up publishes");
	Expect(harness.Publish(persistence, "RuntimeCoordinator"), "persistence follow-up publishes");
	Expect(harness.Publish(reload, "RuntimeCoordinator"), "reload follow-up publishes");
	Expect(harness.Publish(build, "RuntimeCoordinator"), "shader build follow-up publishes");

	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 5, "accepted mutation dispatches every expected follow-up");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationAccepted) == 1, "accepted mutation fact is observed");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeStateChanged) == 1, "accepted mutation publishes state changed follow-up");
	Expect(harness.SeenCount(RuntimeEventType::RuntimePersistenceRequested) == 1, "accepted mutation publishes persistence follow-up");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "accepted mutation publishes reload follow-up");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "accepted mutation publishes shader build follow-up");

	const RuntimeEvent* persistenceEvent = harness.LastSeen(RuntimeEventType::RuntimePersistenceRequested);
	const auto* persistencePayload = persistenceEvent ? std::get_if<RuntimePersistenceRequestedEvent>(&persistenceEvent->payload) : nullptr;
	Expect(persistencePayload && persistencePayload->request.reason == "SetLayerShader", "persistence follow-up preserves mutation action reason");
	Expect(persistencePayload && persistencePayload->request.debounceKey == "runtime-state", "persistence follow-up preserves debounce key");
}

void TestAppLevelBroadcastAndBuildCoalescing()
{
	RuntimeEventTestHarness harness;

	RuntimeMutationEvent firstMutation;
	firstMutation.action = "SetLayerShader";
	firstMutation.accepted = true;
	firstMutation.runtimeStateChanged = true;
	firstMutation.runtimeStateBroadcastRequired = true;
	firstMutation.shaderBuildRequested = true;

	RuntimeMutationEvent secondMutation = firstMutation;
	secondMutation.action = "LoadStackPreset";

	RuntimeStateBroadcastRequestedEvent firstBroadcast;
	firstBroadcast.reason = "SetLayerShader";
	RuntimeStateBroadcastRequestedEvent secondBroadcast;
	secondBroadcast.reason = "LoadStackPreset";

	ShaderBuildEvent firstBuild;
	firstBuild.phase = RuntimeEventShaderBuildPhase::Requested;
	firstBuild.inputWidth = 1920;
	firstBuild.inputHeight = 1080;
	firstBuild.preserveFeedbackState = false;
	firstBuild.message = "first build request";
	ShaderBuildEvent secondBuild = firstBuild;
	secondBuild.message = "second build request";

	Expect(harness.Publish(firstMutation, "RuntimeCoordinator"), "first accepted mutation fact publishes");
	Expect(harness.Publish(firstBroadcast, "RuntimeUpdateController"), "first broadcast request publishes through app dispatcher");
	Expect(harness.Publish(firstBuild, "RuntimeCoordinator"), "first shader build request publishes through app dispatcher");
	Expect(harness.Publish(secondMutation, "RuntimeCoordinator"), "second accepted mutation fact publishes");
	Expect(harness.Publish(secondBroadcast, "RuntimeUpdateController"), "second broadcast request coalesces through app dispatcher");
	Expect(harness.Publish(secondBuild, "RuntimeCoordinator"), "second shader build request coalesces through app dispatcher");

	RuntimeEventQueueMetrics metrics = harness.Dispatcher().GetQueueMetrics();
	Expect(metrics.depth == 4, "app dispatcher keeps FIFO facts plus coalesced broadcast/build requests");
	Expect(metrics.coalescedCount == 2, "app dispatcher reports broadcast and build coalescing");

	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 4, "app dispatcher dispatches FIFO facts plus one broadcast and one build request");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationAccepted) == 2, "app dispatcher preserves every accepted mutation fact");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeStateBroadcastRequested) == 1, "app dispatcher coalesces broadcast requests");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "app dispatcher coalesces matching shader build requests");

	const RuntimeEvent* broadcastEvent = harness.LastSeen(RuntimeEventType::RuntimeStateBroadcastRequested);
	const auto* broadcastPayload = broadcastEvent ? std::get_if<RuntimeStateBroadcastRequestedEvent>(&broadcastEvent->payload) : nullptr;
	Expect(broadcastPayload && broadcastPayload->reason == "LoadStackPreset", "app dispatcher dispatches latest broadcast request");

	const RuntimeEvent* buildEvent = harness.LastSeen(RuntimeEventType::ShaderBuildRequested);
	const auto* buildPayload = buildEvent ? std::get_if<ShaderBuildEvent>(&buildEvent->payload) : nullptr;
	Expect(buildPayload && buildPayload->message == "second build request", "app dispatcher dispatches latest shader build request");
}

void TestManualReloadBridgeEvents()
{
	RuntimeEventTestHarness harness;

	ManualReloadRequestedEvent manualReload;
	manualReload.preserveFeedbackState = true;
	manualReload.reason = "RequestShaderReload";

	RuntimeReloadRequestedEvent runtimeReload;
	runtimeReload.preserveFeedbackState = true;
	runtimeReload.reason = "RequestShaderReload";

	ShaderBuildEvent shaderBuild;
	shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
	shaderBuild.preserveFeedbackState = true;
	shaderBuild.message = "Shader rebuild queued.";

	Expect(harness.Publish(manualReload, "RuntimeCoordinator"), "manual reload ingress event publishes");
	Expect(harness.Publish(runtimeReload, "RuntimeCoordinator"), "manual reload bridge publishes runtime reload request");
	Expect(harness.Publish(shaderBuild, "RuntimeCoordinator"), "manual reload bridge publishes shader build request");

	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 3, "manual reload bridge dispatches ingress and follow-up events");
	Expect(harness.SeenCount(RuntimeEventType::ManualReloadRequested) == 1, "manual reload ingress event is observed");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "manual reload runtime reload follow-up is observed");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "manual reload shader build follow-up is observed");

	const RuntimeEvent* reloadEvent = harness.LastSeen(RuntimeEventType::RuntimeReloadRequested);
	const auto* reloadPayload = reloadEvent ? std::get_if<RuntimeReloadRequestedEvent>(&reloadEvent->payload) : nullptr;
	Expect(reloadPayload && reloadPayload->preserveFeedbackState, "manual reload bridge preserves feedback policy in runtime reload event");
}

void TestFileReloadBridgeEvents()
{
	RuntimeEventTestHarness harness;

	FileChangeDetectedEvent fileChange;
	fileChange.path = "PollRuntimeStoreChanges";
	fileChange.shaderPackageCandidate = true;

	RuntimeReloadRequestedEvent runtimeReload;
	runtimeReload.preserveFeedbackState = false;
	runtimeReload.reason = "PollRuntimeStoreChanges";

	ShaderBuildEvent shaderBuild;
	shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
	shaderBuild.preserveFeedbackState = false;
	shaderBuild.message = "Shader rebuild queued.";

	Expect(harness.Publish(fileChange, "RuntimeCoordinator"), "file change ingress event publishes");
	Expect(harness.Publish(runtimeReload, "RuntimeCoordinator"), "file change bridge publishes runtime reload request");
	Expect(harness.Publish(shaderBuild, "RuntimeCoordinator"), "file change bridge publishes shader build request");

	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 3, "file reload bridge dispatches ingress and follow-up events");
	Expect(harness.SeenCount(RuntimeEventType::FileChangeDetected) == 1, "file change ingress event is observed");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "file reload runtime reload follow-up is observed");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "file reload shader build follow-up is observed");

	const RuntimeEvent* fileEvent = harness.LastSeen(RuntimeEventType::FileChangeDetected);
	const auto* filePayload = fileEvent ? std::get_if<FileChangeDetectedEvent>(&fileEvent->payload) : nullptr;
	Expect(filePayload && filePayload->shaderPackageCandidate, "file reload bridge marks shader package candidate changes");
}

void TestRejectedMutationHasNoDownstreamFollowUps()
{
	RuntimeEventTestHarness harness;

	RuntimeMutationEvent mutation;
	mutation.action = "SetLayerShader";
	mutation.accepted = false;
	mutation.errorMessage = "Unknown shader id: missing";

	Expect(harness.Publish(mutation, "RuntimeCoordinator"), "rejected mutation event publishes");
	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 1, "rejected mutation dispatches only the rejection fact");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation fact is observed");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeStateChanged) == 0, "rejected mutation has no state follow-up");
	Expect(harness.SeenCount(RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation has no persistence follow-up");
	Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 0, "rejected mutation has no reload follow-up");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 0, "rejected mutation has no shader build follow-up");

	const RuntimeEvent* rejectedEvent = harness.LastSeen(RuntimeEventType::RuntimeMutationRejected);
	const auto* rejectedPayload = rejectedEvent ? std::get_if<RuntimeMutationEvent>(&rejectedEvent->payload) : nullptr;
	Expect(rejectedPayload && rejectedPayload->errorMessage == "Unknown shader id: missing", "rejected mutation preserves error message");
}

void TestShaderBuildGenerationEventMatching()
{
	RuntimeEventTestHarness harness;
	std::size_t handledBuilds = 0;
	uint64_t handledGeneration = 0;

	harness.Dispatcher().Subscribe(RuntimeEventType::ShaderBuildPrepared, [&](const RuntimeEvent& event) {
		const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload);
		if (!payload || payload->generation != 7)
			return;

		++handledBuilds;
		handledGeneration = payload->generation;
	});

	ShaderBuildEvent stale;
	stale.phase = RuntimeEventShaderBuildPhase::Prepared;
	stale.generation = 6;
	stale.succeeded = true;

	ShaderBuildEvent current = stale;
	current.generation = 7;

	Expect(harness.Publish(stale, "ShaderBuildQueue"), "stale shader build event publishes");
	Expect(harness.Publish(current, "ShaderBuildQueue"), "current shader build event publishes");
	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.dispatchedEvents == 2, "shader build readiness events dispatch in order");
	Expect(harness.SeenCount(RuntimeEventType::ShaderBuildPrepared) == 2, "both shader build readiness facts are observable");
	Expect(handledBuilds == 1, "generation-aware handler applies only the expected build once");
	Expect(handledGeneration == 7, "generation-aware handler records the expected generation");
}

void TestHandlerFailureCanBecomeTelemetryEvent()
{
	RuntimeEventTestHarness harness;
	harness.Dispatcher().Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [](const RuntimeEvent&) {
		throw std::runtime_error("handler failed");
	});

	RuntimeStateBroadcastRequestedEvent broadcast;
	broadcast.reason = "test";
	Expect(harness.Publish(broadcast, "test"), "broadcast event publishes before failing handler");
	RuntimeEventDispatchResult result = harness.DispatchPending();
	Expect(result.handlerFailures == 1, "dispatcher reports handler failure for telemetry");

	TimingSampleRecordedEvent timing;
	timing.subsystem = "RuntimeEventDispatcher";
	timing.metric = "handlerFailures";
	timing.value = static_cast<double>(result.handlerFailures);
	timing.unit = "count";
	Expect(harness.Publish(timing, "HealthTelemetry"), "handler failure timing sample publishes");
	harness.DispatchPending();
	Expect(harness.SeenCount(RuntimeEventType::TimingSampleRecorded) == 1, "handler failure can be observed as telemetry event");
}
}

int main()
{
	TestRuntimeEventTypeNames();
	TestRuntimeEventPayloadTypes();
	TestRuntimeEventEnvelope();
	TestRuntimeEventQueue();
	TestRuntimeEventDispatcher();
	TestRuntimeEventDispatcherCoalescing();
	TestRuntimeEventCoalescingQueue();
	TestRuntimeEventCoalescingCustomKey();
	TestRuntimeEventTestHarness();
	TestAcceptedMutationFollowUps();
	TestAppLevelBroadcastAndBuildCoalescing();
	TestManualReloadBridgeEvents();
	TestFileReloadBridgeEvents();
	TestRejectedMutationHasNoDownstreamFollowUps();
	TestShaderBuildGenerationEventMatching();
	TestHandlerFailureCanBecomeTelemetryEvent();

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

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