namespace ZB.MOM.WW.Audit.Tests;

public class CompositeAuditWriterTests
{
    private sealed class RecordingWriter : IAuditWriter
    {
        public int Count;
        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default) { Count++; return Task.CompletedTask; }
    }
    private sealed class ThrowingWriter : IAuditWriter
    {
        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default) => throw new InvalidOperationException("boom");
    }
    private sealed class CancellingWriter : IAuditWriter
    {
        public Task WriteAsync(AuditEvent evt, CancellationToken ct = default) => throw new OperationCanceledException();
    }

    private static AuditEvent Evt() => new() { EventId = Guid.NewGuid(), OccurredAtUtc = DateTimeOffset.UtcNow,
        Actor = "a", Action = "x", Outcome = AuditOutcome.Success };

    [Fact]
    public async Task Fans_out_to_all_writers()
    {
        var a = new RecordingWriter(); var b = new RecordingWriter();
        await new CompositeAuditWriter(new IAuditWriter[] { a, b }).WriteAsync(Evt());
        Assert.Equal(1, a.Count);
        Assert.Equal(1, b.Count);
    }

    [Fact]
    public async Task One_failing_writer_does_not_stop_the_others()
    {
        var after = new RecordingWriter();
        var sut = new CompositeAuditWriter(new IAuditWriter[] { new ThrowingWriter(), after });
        await sut.WriteAsync(Evt()); // must not throw
        Assert.Equal(1, after.Count);
    }

    [Fact]
    public async Task Cancellation_does_not_surface_to_the_caller()
    {
        // Per the IAuditWriter hard contract ("must not throw to the caller"), an
        // OperationCanceledException from an inner writer is swallowed like any other failure —
        // it must NOT abort the user-facing action that produced the event.
        var after = new RecordingWriter();
        var sut = new CompositeAuditWriter(new IAuditWriter[] { new CancellingWriter(), after });
        await sut.WriteAsync(Evt()); // must not throw
        Assert.Equal(1, after.Count); // drain continues past the cancelled writer
    }

    [Fact]
    public async Task Empty_writer_list_is_a_no_op()
    {
        var sut = new CompositeAuditWriter(Array.Empty<IAuditWriter>());
        await sut.WriteAsync(Evt()); // must not throw
    }

    [Fact]
    public async Task Null_writer_entry_is_swallowed_and_does_not_stop_the_others()
    {
        // A null inner writer faults the await; the best-effort seam swallows it (like any
        // other writer failure) and continues draining the remaining writers.
        var after = new RecordingWriter();
        var sut = new CompositeAuditWriter(new IAuditWriter?[] { null, after }!);
        await sut.WriteAsync(Evt()); // must not throw
        Assert.Equal(1, after.Count);
    }
}
