using System.Collections.Concurrent; using DragonISO.Engine.Interop; using DragonISO.Engine.Pipeline; namespace DragonISO.Engine.Tests.Fakes; /// /// In-memory test double for . Tests configure source lists and frame /// queues; the fake feeds those into engine code as if a real NDI runtime were present. /// public sealed class FakeNdiInterop : INdiInterop { public List Sources { get; } = new(); public ConcurrentDictionary> ReceiverFrames { get; } = new(); public ConcurrentDictionary> SentFrames { get; } = new(); /// Optional per-source audio peak queue. Each dequeues one entry; null if empty (matches the production "timeout" behavior). public ConcurrentDictionary> ReceiverAudioPeaks { get; } = new(); public string RuntimeVersion { get; set; } = "6.0.0"; public Dictionary ReceiverCreatedCount { get; } = new(); public Dictionary SenderCreatedCount { get; } = new(); /// Last groups string seen by ; null = default Public. public string? LastFinderGroups { get; private set; } /// Per-output groups string seen by ; null = default Public. public Dictionary SenderGroups { get; } = new(); /// /// Number of finders created so far (initial construction + every rebuild). /// Lets tests assert how many times the discovery service tried to rebuild. /// public int FinderCreatedCount { get; private set; } /// /// Optional hook invoked at the top of every call, /// receiving the 1-based creation ordinal. Throw from here to simulate a runtime /// that refuses to build a finder (e.g. the rebuild-failure path); return normally /// to allow creation. Null (default) = always succeed. /// public Action? CreateFinderHook { get; set; } public NdiFindHandle CreateFinder(string? groups = null) { FinderCreatedCount++; CreateFinderHook?.Invoke(FinderCreatedCount); LastFinderGroups = groups; return new FakeFindHandle(); } public IReadOnlyList GetCurrentSources(NdiFindHandle finder) { if (finder is FakeFindHandle { Disposed: true }) throw new ObjectDisposedException(nameof(FakeFindHandle), "GetCurrentSources called on a finder that was already disposed."); return Sources.ToArray(); } public NdiReceiverHandle CreateReceiver(string sourceFullName) { ReceiverCreatedCount[sourceFullName] = ReceiverCreatedCount.GetValueOrDefault(sourceFullName) + 1; ReceiverFrames.GetOrAdd(sourceFullName, _ => new ConcurrentQueue()); return new FakeReceiverHandle(sourceFullName); } public RawFrame? CaptureFrame(NdiReceiverHandle receiver, int timeoutMs) { var key = ((FakeReceiverHandle)receiver).Source; if (ReceiverFrames.TryGetValue(key, out var q) && q.TryDequeue(out var frame)) return frame; return null; // simulate timeout } public double? CaptureAudioPeak(NdiReceiverHandle receiver, int timeoutMs) { var key = ((FakeReceiverHandle)receiver).Source; if (ReceiverAudioPeaks.TryGetValue(key, out var q) && q.TryDequeue(out var peak)) return peak; return null; // no audio queued — simulate timeout } public NdiSenderHandle CreateSender(string outputName, string? groups = null) { SenderCreatedCount[outputName] = SenderCreatedCount.GetValueOrDefault(outputName) + 1; SenderGroups[outputName] = groups; SentFrames.GetOrAdd(outputName, _ => new List()); return new FakeSenderHandle(outputName); } public void SendFrame(NdiSenderHandle sender, ProcessedFrame frame) { var key = ((FakeSenderHandle)sender).Output; SentFrames[key].Add(frame); } public string GetRuntimeVersion() => RuntimeVersion; private sealed class FakeFindHandle : NdiFindHandle { public bool Disposed { get; private set; } public override void Dispose() => Disposed = true; } private sealed class FakeReceiverHandle : NdiReceiverHandle { public string Source { get; } public FakeReceiverHandle(string source) => Source = source; public override void Dispose() { } } private sealed class FakeSenderHandle : NdiSenderHandle { public string Output { get; } public FakeSenderHandle(string output) => Output = output; public override void Dispose() { } } }