using System.Threading;
using System.Windows.Threading;
using System.Windows.Media.Imaging;
using Xunit;
using ytLive.Services;

namespace ytLive.Tests;

/// <summary>
/// ScreenCaptureManager is the app-wide owner of screen-capture sessions,
/// refcounted by target key and coalesced onto the UI dispatcher. These tests
/// pin that contract with a fake IScreenCaptureSource (the WinRT pool/session
/// layer is exercised only on Windows at runtime).
/// </summary>
public class ScreenCaptureManagerTests
{
    private sealed class FakeScreenSource : IScreenCaptureSource
    {
        private readonly List<string>? _started;
        private readonly List<string>? _stopped;

        public string Key { get; }
        public event Action<VideoFrame>? FrameAvailable;

        public FakeScreenSource(string key, List<string>? started = null, List<string>? stopped = null)
        {
            Key = key;
            _started = started;
            _stopped = stopped;
        }

        public Task StartAsync()
        {
            _started?.Add(Key);
            return Task.CompletedTask;
        }

        public Task StopAsync()
        {
            _stopped?.Add(Key);
            return Task.CompletedTask;
        }

        public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
    }

    private sealed class FailingScreenSource : IScreenCaptureSource
    {
        public string Key { get; }
        public event Action<VideoFrame>? FrameAvailable;

        public FailingScreenSource(string key) => Key = key;

        public Task StartAsync() => throw new InvalidOperationException("access denied");

        public Task StopAsync() => Task.CompletedTask;

        public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
    }

    // A real Dispatcher pumping on a background STA thread, so the manager's
    // BeginInvoke queue can be drained deterministically from the test thread.
    private sealed class DispatcherPump : IDisposable
    {
        private readonly Thread _thread;
        private readonly ManualResetEventSlim _ready = new(false);
        private Dispatcher? _dispatcher;

        public Dispatcher Dispatcher => _dispatcher!;

        public DispatcherPump()
        {
            _thread = new Thread(() =>
            {
                _dispatcher = Dispatcher.CurrentDispatcher;
                _ready.Set();
                Dispatcher.Run();
            });
            _thread.IsBackground = true;
            _thread.SetApartmentState(ApartmentState.STA);
            _thread.Start();
            _ready.Wait();
        }

        // Queued behind anything already posted at Render/higher, so pending
        // bitmap copies run first.
        public void Drain() => Dispatcher.Invoke(() => { }, DispatcherPriority.ContextIdle);

        public void Dispose()
        {
            Dispatcher.InvokeShutdown();
            _thread.Join(3000);
        }
    }

    private static byte[] Pixels(params byte[] raw) => raw;

    [Fact]
    public async Task Refcount_TwoAcquires_OneCaptureUntilLastRelease()
    {
        var started = new List<string>();
        var stopped = new List<string>();
        var manager = new ScreenCaptureManager(key => new FakeScreenSource(key, started, stopped));

        Assert.True(await manager.AcquireAsync("monitor:0"));
        Assert.True(await manager.AcquireAsync("monitor:0"));
        Assert.Single(started);

        await manager.ReleaseAsync("monitor:0");
        Assert.Empty(stopped);

        await manager.ReleaseAsync("monitor:0");
        Assert.Single(stopped);
    }

    [Fact]
    public async Task ReleaseAll_StopsEvenWithMultipleRefs()
    {
        var started = new List<string>();
        var stopped = new List<string>();
        var manager = new ScreenCaptureManager(key => new FakeScreenSource(key, started, stopped));

        await manager.AcquireAsync("monitor:0");
        await manager.AcquireAsync("monitor:0");
        await manager.ReleaseAllAsync("monitor:0");

        Assert.Single(stopped);
    }

    [Fact]
    public async Task Acquire_FailingSource_ReturnsFalseAndRaisesCaptureFailed()
    {
        var manager = new ScreenCaptureManager(key => new FailingScreenSource(key));
        string? failedKey = null;
        manager.CaptureFailed += (key, _) => failedKey = key;

        Assert.False(await manager.AcquireAsync("monitor:0"));
        Assert.Equal("monitor:0", failedKey);
    }

    [Fact]
    public async Task Acquire_EmptyKey_ReturnsFalse()
    {
        var manager = new ScreenCaptureManager(key => new FakeScreenSource(key));
        Assert.False(await manager.AcquireAsync(" "));
    }

    [Fact]
    public async Task GetLatestFrame_ReturnsLatestPump_UntilReleased()
    {
        FakeScreenSource? captured = null;
        var manager = new ScreenCaptureManager(key => captured = new FakeScreenSource(key));

        Assert.Null(manager.GetLatestFrame("monitor:0"));

        Assert.True(await manager.AcquireAsync("monitor:0"));
        Assert.Null(manager.GetLatestFrame("monitor:0")); // nothing pumped yet

        var first = new VideoFrame(2, 2, Pixels(1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255));
        var second = new VideoFrame(2, 2, Pixels(5, 0, 0, 255, 6, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255));
        captured!.Pump(first);
        captured.Pump(second);

        Assert.Same(second, manager.GetLatestFrame("monitor:0"));

        await manager.ReleaseAsync("monitor:0");
        Assert.Null(manager.GetLatestFrame("monitor:0"));
    }

    // The one integration test for this branch: one target creates one shared
    // WriteableBitmap, published once, and back-to-back frames coalesce to the
    // latest (a single pending UI copy per session).
    [Fact]
    public async Task Frames_ShareOneBitmap_AndCoalesceToLatest()
    {
        using var pump = new DispatcherPump();
        FakeScreenSource? captured = null;
        var manager = new ScreenCaptureManager(key => captured = new FakeScreenSource(key), pump.Dispatcher);

        WriteableBitmap? published = null;
        var publishedCount = 0;
        manager.PreviewBitmapChanged += (_, bitmap) => { published = bitmap; publishedCount++; };

        Assert.True(await manager.AcquireAsync("monitor:0"));

        var first = new VideoFrame(2, 2, Pixels(1, 0, 0, 255, 2, 0, 0, 255, 3, 0, 0, 255, 4, 0, 0, 255));
        captured!.Pump(first);
        pump.Drain();

        Assert.NotNull(published);
        Assert.Equal(1, publishedCount);

        var dims = pump.Dispatcher.Invoke(() => new[] { published!.PixelWidth, published.PixelHeight });
        Assert.Equal(new[] { 2, 2 }, dims);

        var second = new VideoFrame(2, 2, Pixels(5, 0, 0, 255, 6, 0, 0, 255, 7, 0, 0, 255, 8, 0, 0, 255));
        var third = new VideoFrame(2, 2, Pixels(9, 0, 0, 255, 10, 0, 0, 255, 11, 0, 0, 255, 12, 0, 0, 255));
        captured.Pump(second);
        captured.Pump(third);
        pump.Drain();

        // The bitmap is frozen to the dispatcher thread; copy its pixels there.
        var result = pump.Dispatcher.Invoke(() =>
        {
            var bytes = new byte[16];
            published!.CopyPixels(new System.Windows.Int32Rect(0, 0, 2, 2), bytes, 8, 0);
            return bytes;
        });
        Assert.Equal(third.BgraPixels, result);
    }
}
