using System;
using System.Collections.Generic;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;

public sealed class MxAccessEventQueueTests
{
    /// <summary>Verifies that Enqueue assigns monotonic worker sequences and preserves event order.</summary>
    [Fact]
    public void Enqueue_AssignsMonotonicWorkerSequencesAndPreservesOrder()
    {
        MxAccessEventQueue queue = new(capacity: 4);

        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 10));
        queue.Enqueue(CreateEvent(MxEventFamily.OnWriteComplete, itemHandle: 11));

        Assert.Equal(2, queue.Count);
        Assert.Equal(2UL, queue.LastEventSequence);

        Assert.True(queue.TryDequeue(out WorkerEvent? dequeuedFirst));
        Assert.True(queue.TryDequeue(out WorkerEvent? dequeuedSecond));
        Assert.Equal(1UL, dequeuedFirst?.Event.WorkerSequence);
        Assert.Equal(2UL, dequeuedSecond?.Event.WorkerSequence);
        Assert.NotNull(dequeuedFirst?.Event.WorkerTimestamp);
        Assert.Equal(10, dequeuedFirst?.Event.ItemHandle);
        Assert.Equal(11, dequeuedSecond?.Event.ItemHandle);
        Assert.False(queue.TryDequeue(out _));
    }

    /// <summary>Verifies that Drain removes at most the requested number of events.</summary>
    [Fact]
    public void Drain_RemovesAtMostRequestedEvents()
    {
        MxAccessEventQueue queue = new(capacity: 4);
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 10));
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 11));
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 12));

        IReadOnlyList<WorkerEvent> drained = queue.Drain(maxEvents: 2);

        Assert.Equal(2, drained.Count);
        Assert.Equal(10, drained[0].Event.ItemHandle);
        Assert.Equal(11, drained[1].Event.ItemHandle);
        Assert.Equal(1, queue.Count);
    }

    /// <summary>Verifies that Drain with maxEvents 0 drains every queued event.</summary>
    [Fact]
    public void Drain_WithZeroMaxEvents_DrainsAllEvents()
    {
        MxAccessEventQueue queue = new(capacity: 4);
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 10));
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 11));
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 12));

        IReadOnlyList<WorkerEvent> drained = queue.Drain(maxEvents: 0);

        Assert.Equal(3, drained.Count);
        Assert.Equal(new[] { 10, 11, 12 }, new[]
        {
            drained[0].Event.ItemHandle,
            drained[1].Event.ItemHandle,
            drained[2].Event.ItemHandle,
        });
        Assert.Equal(0, queue.Count);
    }

    /// <summary>Verifies that draining an empty queue returns an empty list.</summary>
    [Fact]
    public void Drain_WhenQueueIsEmpty_ReturnsEmptyList()
    {
        MxAccessEventQueue queue = new(capacity: 4);

        Assert.Empty(queue.Drain(maxEvents: 0));
        Assert.Empty(queue.Drain(maxEvents: 5));
        Assert.Equal(0, queue.Count);
    }

    /// <summary>Verifies that Enqueue is rejected after a fault is recorded manually.</summary>
    [Fact]
    public void Enqueue_AfterRecordFault_ThrowsInvalidOperationException()
    {
        MxAccessEventQueue queue = new(capacity: 4);
        queue.RecordFault(new WorkerFault
        {
            Category = WorkerFaultCategory.MxaccessEventConversionFailed,
        });

        Assert.Throws<InvalidOperationException>(
            () => queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 10)));
        Assert.Equal(0, queue.Count);
    }

    /// <summary>Verifies that Enqueue records an overflow fault and rejects new events when capacity is exceeded.</summary>
    [Fact]
    public void Enqueue_WhenCapacityIsExceeded_RecordsOverflowFaultAndRejectsNewEvents()
    {
        MxAccessEventQueue queue = new(capacity: 1);
        queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 10));

        MxAccessEventQueueOverflowException overflow = Assert.Throws<MxAccessEventQueueOverflowException>(
            () => queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 11)));

        Assert.Equal(1, overflow.Capacity);
        Assert.True(queue.IsFaulted);
        Assert.Equal(WorkerFaultCategory.QueueOverflow, queue.Fault?.Category);
        Assert.Equal(ProtocolStatusCode.WorkerUnavailable, queue.Fault?.ProtocolStatus.Code);
        Assert.Throws<InvalidOperationException>(
            () => queue.Enqueue(CreateEvent(MxEventFamily.OnDataChange, itemHandle: 12)));
    }

    /// <summary>Verifies that RecordFault keeps the first recorded fault.</summary>
    [Fact]
    public void RecordFault_KeepsFirstFault()
    {
        MxAccessEventQueue queue = new(capacity: 1);
        queue.RecordFault(new WorkerFault
        {
            Category = WorkerFaultCategory.MxaccessEventConversionFailed,
        });
        queue.RecordFault(new WorkerFault
        {
            Category = WorkerFaultCategory.QueueOverflow,
        });

        Assert.True(queue.IsFaulted);
        Assert.Equal(WorkerFaultCategory.MxaccessEventConversionFailed, queue.Fault?.Category);
    }

    private static MxEvent CreateEvent(
        MxEventFamily family,
        int itemHandle)
    {
        MxEvent mxEvent = new()
        {
            Family = family,
            SessionId = "session-1",
            ServerHandle = 1,
            ItemHandle = itemHandle,
        };

        switch (family)
        {
            case MxEventFamily.OnWriteComplete:
                mxEvent.OnWriteComplete = new OnWriteCompleteEvent();
                break;

            default:
                mxEvent.OnDataChange = new OnDataChangeEvent();
                break;
        }

        return mxEvent;
    }
}
