using System.Threading.Channels;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Sessions;

namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;

/// <summary>
///     Concurrency and fan-out tests for <see cref="SessionEventDistributor"/>, the
///     Session Resilience epic's per-session event pump. One pump drains the source
///     exactly once and fans every event to N independent per-subscriber channels.
///     Every async wait is bounded so a fan-out or shutdown deadlock fails fast.
/// </summary>
public sealed class SessionEventDistributorTests
{
    private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5);

    [Fact]
    public async Task TwoSubscribers_BothReceiveFannedEventsInOrder()
    {
        Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
        await using SessionEventDistributor distributor = CreateDistributor(source.Reader);
        await distributor.StartAsync(CancellationToken.None);

        using IEventSubscriberLease leaseA = distributor.Register();
        using IEventSubscriberLease leaseB = distributor.Register();

        source.Writer.TryWrite(Event(1));
        source.Writer.TryWrite(Event(2));

        MxEvent a1 = await ReadOneAsync(leaseA.Reader);
        MxEvent a2 = await ReadOneAsync(leaseA.Reader);
        MxEvent b1 = await ReadOneAsync(leaseB.Reader);
        MxEvent b2 = await ReadOneAsync(leaseB.Reader);

        Assert.Equal(1ul, a1.WorkerSequence);
        Assert.Equal(2ul, a2.WorkerSequence);
        Assert.Equal(1ul, b1.WorkerSequence);
        Assert.Equal(2ul, b2.WorkerSequence);
    }

    [Fact]
    public async Task DisposingOneLease_StopsItsDelivery_OtherKeepsReceiving()
    {
        Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
        await using SessionEventDistributor distributor = CreateDistributor(source.Reader);
        await distributor.StartAsync(CancellationToken.None);

        IEventSubscriberLease leaseA = distributor.Register();
        using IEventSubscriberLease leaseB = distributor.Register();

        source.Writer.TryWrite(Event(1));
        _ = await ReadOneAsync(leaseA.Reader);
        _ = await ReadOneAsync(leaseB.Reader);

        leaseA.Dispose();

        // A's reader must complete (no more delivery) after dispose.
        await AssertCompletedAsync(leaseA.Reader);

        // B still receives subsequent events.
        source.Writer.TryWrite(Event(2));
        MxEvent b2 = await ReadOneAsync(leaseB.Reader);
        Assert.Equal(2ul, b2.WorkerSequence);
    }

    [Fact]
    public async Task SubscriberRegisteredAfterStart_ReceivesEventsEmittedAfterRegistration()
    {
        Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
        await using SessionEventDistributor distributor = CreateDistributor(source.Reader);
        await distributor.StartAsync(CancellationToken.None);

        using IEventSubscriberLease leaseA = distributor.Register();
        source.Writer.TryWrite(Event(1));
        _ = await ReadOneAsync(leaseA.Reader);

        // Late subscriber: only sees events emitted after it registered.
        using IEventSubscriberLease leaseB = distributor.Register();
        source.Writer.TryWrite(Event(2));

        MxEvent b = await ReadOneAsync(leaseB.Reader);
        Assert.Equal(2ul, b.WorkerSequence);
    }

    [Fact]
    public async Task DisposingDistributor_CompletesAllSubscriberChannels_AndStopsPump()
    {
        Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
        SessionEventDistributor distributor = CreateDistributor(source.Reader);
        await distributor.StartAsync(CancellationToken.None);

        using IEventSubscriberLease leaseA = distributor.Register();
        using IEventSubscriberLease leaseB = distributor.Register();

        // Bounded so a shutdown hang fails fast.
        await distributor.DisposeAsync().AsTask().WaitAsync(ReadTimeout);

        await AssertCompletedAsync(leaseA.Reader);
        await AssertCompletedAsync(leaseB.Reader);
    }

    [Fact]
    public async Task Register_AfterDispose_ThrowsObjectDisposedException()
    {
        Channel<MxEvent> source = Channel.CreateUnbounded<MxEvent>();
        SessionEventDistributor distributor = CreateDistributor(source.Reader);
        await distributor.StartAsync(CancellationToken.None);

        await distributor.DisposeAsync().AsTask().WaitAsync(ReadTimeout);

        Assert.Throws<ObjectDisposedException>(() => distributor.Register());
    }

    private static SessionEventDistributor CreateDistributor(ChannelReader<MxEvent> source)
        => new(
            "session-test",
            ct => source.ReadAllAsync(ct),
            subscriberQueueCapacity: 64,
            NullLogger<SessionEventDistributor>.Instance);

    private static MxEvent Event(ulong sequence)
        => new() { SessionId = "session-test", WorkerSequence = sequence };

    private static async Task<MxEvent> ReadOneAsync(ChannelReader<MxEvent> reader)
    {
        await reader.WaitToReadAsync().AsTask().WaitAsync(ReadTimeout);
        Assert.True(reader.TryRead(out MxEvent? value));
        return value!;
    }

    private static async Task AssertCompletedAsync(ChannelReader<MxEvent> reader)
    {
        // Drain anything still buffered, then assert the channel is completed
        // (no further events). Bounded so a never-completing channel fails fast.
        await reader.Completion.WaitAsync(ReadTimeout);
    }
}
