using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
using ZB.MOM.WW.Audit;
using IAuditWriter = ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services.IAuditWriter;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;

/// <summary>
/// Bundle B (M2-T4) tests for <see cref="FallbackAuditWriter"/> — composes the
/// primary <see cref="SqliteAuditWriter"/>, the drop-oldest
/// <see cref="RingBufferFallback"/>, and an
/// <see cref="IAuditWriteFailureCounter"/> health counter.
/// </summary>
public class FallbackAuditWriterTests
{
    private static AuditEvent NewEvent(string? target = null) => ScadaBridgeAuditEventFactory.Create(
        eventId: Guid.NewGuid(),
        occurredAtUtc: DateTime.UtcNow,
        channel: AuditChannel.ApiOutbound,
        kind: AuditKind.ApiCall,
        status: AuditStatus.Delivered,
        target: target);

    /// <summary>Flip-switch primary writer mock.</summary>
    private sealed class FlipSwitchPrimary : IAuditWriter
    {
        public bool FailNext { get; set; }
        public List<AuditEvent> Written { get; } = new();

        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
        {
            if (FailNext)
            {
                return Task.FromException(new InvalidOperationException("primary down"));
            }
            Written.Add(evt);
            return Task.CompletedTask;
        }
    }

    [Fact]
    public async Task WriteAsync_PrimaryThrows_EventLandsInRing_CallReturnsSuccess()
    {
        var primary = new FlipSwitchPrimary { FailNext = true };
        var ring = new RingBufferFallback(capacity: 16);
        var counter = Substitute.For<IAuditWriteFailureCounter>();

        var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);

        var evt = NewEvent("doomed");
        // Must NOT throw — audit failures are always swallowed at this layer.
        await fallback.WriteAsync(evt);

        Assert.Equal(1, ring.Count);
        counter.Received(1).Increment();
    }

    [Fact]
    public async Task WriteAsync_PrimaryRecovers_RingDrains_InFIFOOrder_OnNextWrite()
    {
        var primary = new FlipSwitchPrimary { FailNext = true };
        var ring = new RingBufferFallback(capacity: 16);
        var counter = Substitute.For<IAuditWriteFailureCounter>();

        var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);

        var failed = new[] { NewEvent("a"), NewEvent("b"), NewEvent("c") };
        foreach (var e in failed)
        {
            await fallback.WriteAsync(e);
        }

        Assert.Equal(3, ring.Count);

        // Primary recovers; the very next successful write should drain the
        // ring in FIFO order through the primary.
        primary.FailNext = false;
        var trigger = NewEvent("trigger");
        await fallback.WriteAsync(trigger);

        Assert.Equal(0, ring.Count);
        // Order: the triggering event reaches the primary first (that's the
        // signal the primary has recovered), then the backlog drains in FIFO
        // submission order behind it.
        Assert.Equal(4, primary.Written.Count);
        Assert.Equal("trigger", primary.Written[0].Target);
        Assert.Equal("a", primary.Written[1].Target);
        Assert.Equal("b", primary.Written[2].Target);
        Assert.Equal("c", primary.Written[3].Target);
    }

    [Fact]
    public async Task WriteAsync_PrimaryAlwaysSucceeds_Ring_StaysEmpty()
    {
        var primary = new FlipSwitchPrimary();
        var ring = new RingBufferFallback(capacity: 16);
        var counter = Substitute.For<IAuditWriteFailureCounter>();

        var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);

        for (int i = 0; i < 10; i++)
        {
            await fallback.WriteAsync(NewEvent());
        }

        Assert.Equal(0, ring.Count);
        Assert.Equal(10, primary.Written.Count);
        counter.DidNotReceive().Increment();
    }

    [Fact]
    public async Task WriteAsync_FailureCounter_Incremented_Per_PrimaryFailure()
    {
        var primary = new FlipSwitchPrimary { FailNext = true };
        var ring = new RingBufferFallback(capacity: 16);
        var counter = Substitute.For<IAuditWriteFailureCounter>();

        var fallback = new FallbackAuditWriter(primary, ring, counter, NullLogger<FallbackAuditWriter>.Instance);

        for (int i = 0; i < 5; i++)
        {
            await fallback.WriteAsync(NewEvent());
        }

        counter.Received(5).Increment();
    }
}
