using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
using ytLive.Services;

namespace ytLive.Tests;

public class CameraManagerTests
{
    private sealed class FakeFrameSource : ICameraFrameSource
    {
        private readonly List<string> _started;
        private readonly List<string> _stopped;
        private readonly VideoFrame? _pumpOnStart;
        private readonly string? _failOnStart;

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

        public FakeFrameSource(string deviceId, List<string> started, List<string> stopped,
            VideoFrame? pumpOnStart = null, string? failOnStart = null)
        {
            DeviceId = deviceId;
            _started = started;
            _stopped = stopped;
            _pumpOnStart = pumpOnStart;
            _failOnStart = failOnStart;
        }

        public Task StartAsync()
        {
            if (_failOnStart != null) throw new InvalidOperationException(_failOnStart);
            _started.Add(DeviceId);
            if (_pumpOnStart != null) FrameAvailable?.Invoke(_pumpOnStart);
            return Task.CompletedTask;
        }

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

        public void Pump(VideoFrame frame) => FrameAvailable?.Invoke(frame);
        public void Fail(string message) => SourceFailed?.Invoke(message);
    }

    // The refcount/coalescing tests model a camera whose frame pump is driven by
    // the test after acquire, so they skip the first-frame proof (TimeSpan.Zero);
    // the proof itself is exercised by the dedicated tests below.
    private static CameraManager CreateManager(List<string> started, List<string> stopped)
        => new(new FakeEnumerator(), id => new FakeFrameSource(id, started, stopped),
            null, TimeSpan.Zero);

    private sealed class FakeEnumerator : ICameraEnumerator
    {
        public Task<IReadOnlyList<CameraDeviceInfo>> GetCamerasAsync()
            => Task.FromResult<IReadOnlyList<CameraDeviceInfo>>(new[]
            {
                new CameraDeviceInfo("dev1", "Logitech C920"),
            });
    }

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

        Assert.True(await manager.AcquireAsync("dev1"));
        Assert.True(await manager.AcquireAsync("dev1"));
        Assert.Single(started);

        await manager.ReleaseAsync("dev1");
        Assert.Empty(stopped);

        await manager.ReleaseAsync("dev1");
        Assert.Single(stopped);
        Assert.Null(manager.GetLatestFrame("dev1"));
    }

    [Fact]
    public async Task FramePump_UpdatesLatestFrame()
    {
        var started = new List<string>();
        var stopped = new List<string>();
        FakeFrameSource? captured = null;
        var manager = new CameraManager(
            new FakeEnumerator(),
            id => captured = new FakeFrameSource(id, started, stopped),
            null, TimeSpan.Zero);

        await manager.AcquireAsync("dev1");
        var frame = new VideoFrame(2, 2, new byte[16]);
        captured!.Pump(frame);

        Assert.Same(frame, manager.GetLatestFrame("dev1"));
    }

    [Fact]
    public async Task Acquire_FailingCamera_ReturnsFalseAndStops()
    {
        var manager = new CameraManager(
            new FakeEnumerator(),
            id => new FakeFrameSource(id, new List<string>(), new List<string>(), failOnStart: "camera in use"),
            null, TimeSpan.Zero);
        string? failedDevice = null;
        manager.CameraFailed += (device, _) => failedDevice = device;

        Assert.False(await manager.AcquireAsync("dev1"));
        Assert.Equal("dev1", failedDevice);
    }

    [Fact]
    public async Task Acquire_EmptyDeviceId_ReturnsFalse()
    {
        var manager = CreateManager(new List<string>(), new List<string>());
        Assert.False(await manager.AcquireAsync(" "));
    }

    // ─── First-frame proof (the integration test for this change) ───

    [Fact]
    public async Task Acquire_SilentCamera_NoFirstFrame_FailsAndSurfacesCameraFailed()
    {
        var stopped = new List<string>();
        var manager = new CameraManager(
            new FakeEnumerator(),
            id => new FakeFrameSource(id, new List<string>(), stopped),
            null, TimeSpan.FromMilliseconds(150));
        string? failedDevice = null;
        string? failedMessage = null;
        manager.CameraFailed += (device, message) =>
        {
            failedDevice = device;
            failedMessage = message;
        };

        // The reader "starts" fine but never delivers a frame — the exact
        // silent-empty-box scenario. Must fail, be reported, and be rolled back.
        Assert.False(await manager.AcquireAsync("dev1"));
        Assert.Equal("dev1", failedDevice);
        Assert.Contains("no frames", failedMessage);
        Assert.Null(manager.GetLatestFrame("dev1"));
        Assert.Contains("dev1", stopped);
    }

    [Fact]
    public async Task Acquire_FirstFrameProvesAlive_ReturnsTrueWithoutWaitingTimeout()
    {
        var manager = new CameraManager(
            new FakeEnumerator(),
            id => new FakeFrameSource(id, new List<string>(), new List<string>(),
                pumpOnStart: new VideoFrame(2, 2, new byte[16])),
            null, TimeSpan.FromSeconds(10));

        var started = await manager.AcquireAsync("dev1");

        Assert.True(started);
        Assert.NotNull(manager.GetLatestFrame("dev1"));
    }

    [Fact]
    public async Task Acquire_SourceFailureAfterStart_SurfacesCameraFailedAndRollsBack()
    {
        var stopped = new List<string>();
        FakeFrameSource? source = null;
        var manager = new CameraManager(
            new FakeEnumerator(),
            id => source = new FakeFrameSource(id, new List<string>(), stopped,
                pumpOnStart: new VideoFrame(2, 2, new byte[16])),
            null, TimeSpan.Zero);
        string? failedMessage = null;
        manager.CameraFailed += (_, message) => failedMessage = message;

        Assert.True(await manager.AcquireAsync("dev1"));
        source!.Fail("capture failed (0x8007001F): device not available");

        Assert.Contains("device not available", failedMessage);
        Assert.Null(manager.GetLatestFrame("dev1"));
        Assert.Contains("dev1", stopped);
    }
}
