using System.Collections.Concurrent;
using DragonISO.Engine.Interop;
using DragonISO.Engine.Pipeline;

namespace DragonISO.Engine.Tests.Fakes;

/// <summary>
/// In-memory test double for <see cref="INdiInterop"/>. Tests configure source lists and frame
/// queues; the fake feeds those into engine code as if a real NDI runtime were present.
/// </summary>
public sealed class FakeNdiInterop : INdiInterop
{
    public List<string> Sources { get; } = new();
    public ConcurrentDictionary<string, ConcurrentQueue<RawFrame>> ReceiverFrames { get; } = new();
    public ConcurrentDictionary<string, List<ProcessedFrame>> SentFrames { get; } = new();
    /// <summary>Optional per-source audio peak queue. Each <see cref="CaptureAudioPeak"/> dequeues one entry; null if empty (matches the production "timeout" behavior).</summary>
    public ConcurrentDictionary<string, ConcurrentQueue<double>> ReceiverAudioPeaks { get; } = new();
    public string RuntimeVersion { get; set; } = "6.0.0";
    public Dictionary<string, int> ReceiverCreatedCount { get; } = new();
    public Dictionary<string, int> SenderCreatedCount { get; } = new();

    /// <summary>Last <c>groups</c> string seen by <see cref="CreateFinder"/>; null = default Public.</summary>
    public string? LastFinderGroups { get; private set; }
    /// <summary>Per-output <c>groups</c> string seen by <see cref="CreateSender"/>; null = default Public.</summary>
    public Dictionary<string, string?> SenderGroups { get; } = new();

    /// <summary>
    /// Number of finders created so far (initial construction + every rebuild).
    /// Lets tests assert how many times the discovery service tried to rebuild.
    /// </summary>
    public int FinderCreatedCount { get; private set; }

    /// <summary>
    /// Optional hook invoked at the top of every <see cref="CreateFinder"/> 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.
    /// </summary>
    public Action<int>? CreateFinderHook { get; set; }

    public NdiFindHandle CreateFinder(string? groups = null)
    {
        FinderCreatedCount++;
        CreateFinderHook?.Invoke(FinderCreatedCount);
        LastFinderGroups = groups;
        return new FakeFindHandle();
    }

    public IReadOnlyList<string> 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<RawFrame>());
        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<ProcessedFrame>());
        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() { }
    }
}
