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

namespace DragonISO.Engine.Tests.Pipeline;

public class FrameProcessorTests
{
    private static readonly FrameProcessingSettings Settings1080p30 =
        new(TargetFramerate.Fps30, TargetResolution.R1080p, AspectMode.Pillarbox, AudioMode.Auto);

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

    private static FrameProcessor NewProcessor(
        FakeFrameClock clock,
        Channel<RawFrame> input,
        Channel<ProcessedFrame> output,
        FrameProcessingSettings? settings = null)
        => new(
            settings: settings ?? Settings1080p30,
            scaler: new PassthroughFrameScaler(),
            slateRenderer: new SolidFrameRenderer(),
            clock: clock,
            input: input.Reader,
            output: output.Writer,
            slateThreshold: TimeSpan.FromSeconds(2.5),
            logger: NullLogger<FrameProcessor>.Instance);

    [Fact]
    public async Task ProcessOnce_NewFrameAvailable_EmitsScaledFrame()
    {
        var clock = new FakeFrameClock();
        var input = Channel.CreateBounded<RawFrame>(4);
        var output = Channel.CreateUnbounded<ProcessedFrame>();
        var proc = NewProcessor(clock, input, output);

        input.Writer.TryWrite(MakeFrame(640, 360, ts: 100));
        clock.Advance(TimeSpan.FromMilliseconds(34));
        await proc.ProcessOnceAsync(CancellationToken.None);

        output.Reader.TryRead(out var frame).Should().BeTrue();
        frame!.Width.Should().Be(1920);
        frame.Height.Should().Be(1080);
    }

    [Fact]
    public async Task ProcessOnce_NoNewFrame_ReEmitsLastFrame()
    {
        var clock = new FakeFrameClock();
        var input = Channel.CreateBounded<RawFrame>(4);
        var output = Channel.CreateUnbounded<ProcessedFrame>();
        var proc = NewProcessor(clock, input, output);

        input.Writer.TryWrite(MakeFrame(640, 360, ts: 100));
        clock.Advance(TimeSpan.FromMilliseconds(34));
        await proc.ProcessOnceAsync(CancellationToken.None);
        output.Reader.TryRead(out _).Should().BeTrue();

        clock.Advance(TimeSpan.FromMilliseconds(34));
        await proc.ProcessOnceAsync(CancellationToken.None);

        output.Reader.TryRead(out var second).Should().BeTrue();
        second!.Width.Should().Be(1920);
        proc.Stats.FramesDuplicated.Should().Be(1);
    }

    [Fact]
    public async Task ProcessOnce_NoFrameForLongerThanSlateThreshold_EmitsSlate()
    {
        var clock = new FakeFrameClock();
        var input = Channel.CreateBounded<RawFrame>(4);
        var output = Channel.CreateUnbounded<ProcessedFrame>();
        var proc = NewProcessor(clock, input, output);

        input.Writer.TryWrite(MakeFrame(640, 360, ts: 100));
        clock.Advance(TimeSpan.FromMilliseconds(34));
        await proc.ProcessOnceAsync(CancellationToken.None);
        output.Reader.TryRead(out _);

        clock.Advance(TimeSpan.FromSeconds(3));
        await proc.ProcessOnceAsync(CancellationToken.None);

        output.Reader.TryRead(out var slate).Should().BeTrue();
        slate!.Width.Should().Be(1920);
        slate.Height.Should().Be(1080);
        slate.Pixels.Span[0].Should().Be(0x80);
    }

    [Fact]
    public async Task ProcessOnce_PicksNewestFrame_DropsOlder()
    {
        var clock = new FakeFrameClock();
        var input = Channel.CreateBounded<RawFrame>(4);
        var output = Channel.CreateUnbounded<ProcessedFrame>();
        var proc = NewProcessor(clock, input, output);

        input.Writer.TryWrite(MakeFrame(640, 360, ts: 100));
        input.Writer.TryWrite(MakeFrame(640, 360, ts: 200));
        input.Writer.TryWrite(MakeFrame(640, 360, ts: 300));
        clock.Advance(TimeSpan.FromMilliseconds(34));
        await proc.ProcessOnceAsync(CancellationToken.None);

        proc.Stats.FramesIn.Should().Be(3);
        proc.Stats.FramesDropped.Should().Be(2);
    }
}
