using System.Threading.Channels;
using Microsoft.Extensions.Logging.Abstractions;
using TeamsISO.Engine.Pipeline;
using TeamsISO.Engine.Tests.Fakes;

namespace TeamsISO.Engine.Tests.Pipeline;

public class NdiReceiverTests
{
    private const string Source = "PC1 (Teams - Jane)";

    private static RawFrame MakeFrame(long ts) =>
        new(640, 360, ts, new byte[640 * 360 * 4], PixelFormat.Bgra);

    [Fact]
    public void CaptureOnce_FrameAvailable_WritesToOutputAndIncrementsCounter()
    {
        var interop = new FakeNdiInterop();
        interop.ReceiverFrames.GetOrAdd(Source, _ => new System.Collections.Concurrent.ConcurrentQueue<RawFrame>())
            .Enqueue(MakeFrame(100));

        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.CaptureOnce(timeoutMs: 100);

        receiver.FramesCaptured.Should().Be(1);
        output.Reader.TryRead(out var captured).Should().BeTrue();
        captured!.TimestampTicks.Should().Be(100);
    }

    [Fact]
    public void CaptureOnce_NoFrameAvailable_DoesNothing()
    {
        var interop = new FakeNdiInterop();
        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.CaptureOnce(timeoutMs: 100);

        receiver.FramesCaptured.Should().Be(0);
        output.Reader.TryRead(out _).Should().BeFalse();
    }

    [Fact]
    public async Task RunAsync_HonorsCancellation_AndDisposesHandle()
    {
        var interop = new FakeNdiInterop();
        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        using var cts = new CancellationTokenSource();
        var runTask = receiver.RunAsync(cts.Token);

        await Task.Delay(50);
        cts.Cancel();
        await runTask;

        // Receiver was created exactly once
        interop.ReceiverCreatedCount[Source].Should().Be(1);
    }

    [Fact]
    public void ConsumeAudioPeak_NoFramesProcessed_ReturnsZero()
    {
        var interop = new FakeNdiInterop();
        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.ConsumeAudioPeak().Should().Be(0.0);
    }

    [Fact]
    public void CaptureAudioOnce_PublishesPeak_ConsumeReturnsAndResets()
    {
        var interop = new FakeNdiInterop();
        interop.ReceiverAudioPeaks.GetOrAdd(Source, _ => new System.Collections.Concurrent.ConcurrentQueue<double>())
            .Enqueue(0.42);

        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.CaptureAudioOnce(timeoutMs: 100);

        // Peek doesn't reset
        receiver.PeekAudioPeak().Should().BeApproximately(0.42, precision: 0.0001);

        // First Consume returns the peak
        receiver.ConsumeAudioPeak().Should().BeApproximately(0.42, precision: 0.0001);

        // Second Consume returns 0 — Consume has reset semantics
        receiver.ConsumeAudioPeak().Should().Be(0.0);
    }

    [Fact]
    public void CaptureAudioOnce_KeepsHighWaterMarkAcrossMultipleFrames()
    {
        // Three frames with peaks 0.3, 0.8, 0.2. Without reset, the receiver
        // must report the loudest (0.8) — that's the whole point of a peak
        // meter. The "latest peak" naive overwrite would lose the 0.8 if a
        // quieter 0.2 frame followed it.
        var interop = new FakeNdiInterop();
        var q = interop.ReceiverAudioPeaks.GetOrAdd(Source, _ => new System.Collections.Concurrent.ConcurrentQueue<double>());
        q.Enqueue(0.3);
        q.Enqueue(0.8);
        q.Enqueue(0.2);

        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.CaptureAudioOnce(timeoutMs: 100);
        receiver.CaptureAudioOnce(timeoutMs: 100);
        receiver.CaptureAudioOnce(timeoutMs: 100);

        receiver.ConsumeAudioPeak().Should().BeApproximately(0.8, precision: 0.0001);
    }

    [Fact]
    public void CaptureAudioOnce_NoFrameAvailable_LeavesHighWaterMarkUntouched()
    {
        // Establish a peak, then call CaptureAudioOnce when the fake has no
        // queued frames. The high-water mark must NOT be reset just because
        // a polling tick saw nothing — that would defeat the peak-hold
        // semantic between consumer reads.
        var interop = new FakeNdiInterop();
        interop.ReceiverAudioPeaks.GetOrAdd(Source, _ => new System.Collections.Concurrent.ConcurrentQueue<double>())
            .Enqueue(0.5);

        var output = Channel.CreateUnbounded<RawFrame>();
        var receiver = new NdiReceiver(interop, Source, output.Writer, NullLogger<NdiReceiver>.Instance);

        receiver.CaptureAudioOnce(timeoutMs: 100); // peak now 0.5
        receiver.CaptureAudioOnce(timeoutMs: 100); // no frame; should be no-op

        receiver.PeekAudioPeak().Should().BeApproximately(0.5, precision: 0.0001);
    }
}
