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 IsoPipelineTests
{
    private static ExponentialBackoff FastBackoff() =>
        new(maxAttempts: 5, initial: TimeSpan.FromMilliseconds(1), cap: TimeSpan.FromMilliseconds(10));

    private static Func<TimeSpan, CancellationToken, Task> NoDelay() =>
        (_, _) => Task.CompletedTask;

    [Fact]
    public async Task StartAsync_TransitionsIdleToReceiving()
    {
        var participant = Guid.NewGuid();
        var blocker = new TaskCompletionSource();
        var pipeline = new IsoPipeline(
            participant,
            ct => blocker.Task.WaitAsync(ct),
            FastBackoff(),
            NoDelay(),
            NullLoggerFactory.Instance);

        await pipeline.StartAsync();

        pipeline.State.Should().Be(IsoState.Receiving);

        blocker.SetResult();
        await pipeline.StopAsync();
    }

    [Fact]
    public async Task StopAsync_TransitionsBackToIdle()
    {
        var blocker = new TaskCompletionSource();
        var pipeline = new IsoPipeline(
            Guid.NewGuid(),
            ct => blocker.Task.WaitAsync(ct),
            FastBackoff(),
            NoDelay(),
            NullLoggerFactory.Instance);

        await pipeline.StartAsync();
        await pipeline.StopAsync();

        pipeline.State.Should().Be(IsoState.Idle);
    }

    [Fact]
    public async Task Supervisor_FailsOnce_ThenRecovers_RemainsReceiving()
    {
        int callCount = 0;
        var done = new TaskCompletionSource();
        var pipeline = new IsoPipeline(
            Guid.NewGuid(),
            ct =>
            {
                callCount++;
                if (callCount == 1) throw new InvalidOperationException("boom");
                return done.Task.WaitAsync(ct);
            },
            FastBackoff(),
            NoDelay(),
            NullLoggerFactory.Instance);

        await pipeline.StartAsync();
        await Task.Delay(50);          // let the supervisor cycle through the failure
        callCount.Should().BeGreaterOrEqualTo(2);
        pipeline.ConsecutiveFailures.Should().Be(1);
        pipeline.State.Should().Be(IsoState.Receiving);

        done.SetResult();
        await pipeline.StopAsync();
    }

    [Fact]
    public async Task ProductionCtor_PassesConfiguredFrameRate_ToSender()
    {
        // The operator configures 29.97 in settings; the NDI sender for every ISO
        // output must be created with that exact rational (30000/1001), not the
        // hardwired 59.94 default.
        var interop = new FakeNdiInterop();
        var config = new IsoPipelineConfig(
            Guid.NewGuid(),
            "Teams - Alice",
            "Alice-ISO",
            FrameProcessingSettings.Default with { Framerate = TargetFramerate.Fps29_97 });
        var pipeline = new IsoPipeline(
            config, interop, new ManagedNearestNeighborFrameScaler(), new FakeFrameClock(),
            FastBackoff(), NoDelay(), NullLoggerFactory.Instance);

        await pipeline.StartAsync();
        var deadline = DateTime.UtcNow.AddSeconds(2);
        while (!interop.SenderFrameRates.ContainsKey("Alice-ISO") && DateTime.UtcNow < deadline)
            await Task.Delay(10);
        await pipeline.StopAsync();

        interop.SenderFrameRates.Should().ContainKey("Alice-ISO");
        interop.SenderFrameRates["Alice-ISO"].Should().Be((30000, 1001));
    }

    [Fact]
    public async Task Supervisor_FailsRepeatedly_TransitionsToError()
    {
        var pipeline = new IsoPipeline(
            Guid.NewGuid(),
            _ => throw new InvalidOperationException("permanent"),
            FastBackoff(),
            NoDelay(),
            NullLoggerFactory.Instance);

        await pipeline.StartAsync();

        // Wait for supervisor to give up (max 5 attempts Ã— ~1ms delay)
        var deadline = DateTime.UtcNow.AddSeconds(2);
        while (pipeline.State != IsoState.Error && DateTime.UtcNow < deadline)
            await Task.Delay(20);

        pipeline.State.Should().Be(IsoState.Error);
        pipeline.ConsecutiveFailures.Should().Be(6); // 5 retries allowed, give up on 6th
    }
}
