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

namespace DragonISO.Engine.Tests.Pipeline;

/// <summary>
/// Targets the IsoPipeline stats wiring (FPS ring buffer + drops/dups surfaced
/// from FrameProcessor). The production-ctor's runner pumps the receiver in a
/// background thread, so we drive the FrameProcessor directly here â€” that's
/// where FramesDropped and FramesDuplicated are computed.
/// </summary>
public class FrameProcessorStatsTests
{
    [Fact]
    public async Task FrameProcessor_DropsBackloggedFrames_WhenInputHasMultipleQueued()
    {
        // Arrange: a raw channel pre-filled with three frames before ProcessOnce runs.
        // The processor should keep only the newest (closest-frame strategy) and report
        // FramesDropped == 2 (the two it threw away).
        var raw = Channel.CreateUnbounded<RawFrame>();
        var processed = Channel.CreateUnbounded<ProcessedFrame>();
        var clock = new FakeClock();
        var settings = FrameProcessingSettings.Default;
        var processor = new FrameProcessor(
            settings,
            new ManagedNearestNeighborFrameScaler(),
            new SolidFrameRenderer(),
            clock,
            raw.Reader,
            processed.Writer,
            slateThreshold: TimeSpan.FromSeconds(2.5),
            NullLogger<FrameProcessor>.Instance);

        for (var i = 0; i < 3; i++)
            raw.Writer.TryWrite(MakeFrame(width: 320, height: 180, ticks: i));

        // Act
        await processor.ProcessOnceAsync(CancellationToken.None);

        // Assert
        var stats = processor.Stats;
        stats.FramesIn.Should().Be(3, because: "the processor counts every frame it pulled off the channel");
        stats.FramesOut.Should().Be(1, because: "closest-frame strategy emits one frame per tick");
        stats.FramesDropped.Should().Be(2, because: "two queued frames were superseded by the newest");
        stats.IncomingWidth.Should().Be(320);
        stats.IncomingHeight.Should().Be(180);
    }

    [Fact]
    public async Task FrameProcessor_DuplicatesLastFrame_WhenNoNewArrival()
    {
        // First tick: a single frame. Second tick: nothing new â€” should re-emit
        // the last frame (within slate threshold) and increment FramesDuplicated.
        var raw = Channel.CreateUnbounded<RawFrame>();
        var processed = Channel.CreateUnbounded<ProcessedFrame>();
        var clock = new FakeClock { NowTicks = 0 };
        var processor = new FrameProcessor(
            FrameProcessingSettings.Default,
            new ManagedNearestNeighborFrameScaler(),
            new SolidFrameRenderer(),
            clock,
            raw.Reader,
            processed.Writer,
            slateThreshold: TimeSpan.FromSeconds(2.5),
            NullLogger<FrameProcessor>.Instance);

        raw.Writer.TryWrite(MakeFrame(width: 320, height: 180, ticks: 0));
        await processor.ProcessOnceAsync(CancellationToken.None);

        // Advance clock 100ms; no new frame.
        clock.NowTicks = TimeSpan.FromMilliseconds(100).Ticks;
        await processor.ProcessOnceAsync(CancellationToken.None);

        var stats = processor.Stats;
        stats.FramesIn.Should().Be(1);
        stats.FramesOut.Should().Be(2);
        stats.FramesDuplicated.Should().Be(1, because: "the second tick re-emitted the last frame");
    }

    private static RawFrame MakeFrame(int width, int height, long ticks)
    {
        var bytes = new byte[width * height * 4];
        return new RawFrame(width, height, ticks, bytes, PixelFormat.Bgra);
    }

    /// <summary>Simple deterministic clock for processor tests.</summary>
    private sealed class FakeClock : IFrameClock
    {
        public long NowTicks { get; set; }
        public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken) => ValueTask.FromResult(true);
    }
}
