// Go reference: server/monitor.go — closedClients ring buffer, ClosedState tracking.
// These tests verify the fixed-size ring buffer used to track recently closed connections
// for the /connz?state=closed monitoring endpoint.

using NATS.Server.Monitoring;

namespace NATS.Server.Monitoring.Tests.Monitoring;

public class ClosedConnectionRingBufferTests
{
    // -----------------------------------------------------------------------
    // Helpers
    // -----------------------------------------------------------------------

    private static ClosedClient MakeEntry(ulong cid, string reason = "normal") => new()
    {
        Cid = cid,
        Ip = "127.0.0.1",
        Port = 4222,
        Start = DateTime.UtcNow.AddSeconds(-10),
        Stop = DateTime.UtcNow,
        Reason = reason,
    };

    // -----------------------------------------------------------------------
    // 1. Add_IncreasesCount
    // -----------------------------------------------------------------------

    /// <summary>
    /// Adding entries should increase Count up to capacity.
    /// </summary>
    [Fact]
    public void Add_IncreasesCount()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);

        buf.Count.ShouldBe(0);

        buf.Add(MakeEntry(1));
        buf.Count.ShouldBe(1);

        buf.Add(MakeEntry(2));
        buf.Count.ShouldBe(2);

        buf.Add(MakeEntry(3));
        buf.Count.ShouldBe(3);
    }

    // -----------------------------------------------------------------------
    // 2. Add_RingOverwrite_CapacityNotExceeded
    // -----------------------------------------------------------------------

    /// <summary>
    /// When capacity is exceeded the count stays at capacity (oldest entry is overwritten).
    /// </summary>
    [Fact]
    public void Add_RingOverwrite_CapacityNotExceeded()
    {
        const int capacity = 5;
        var buf = new ClosedConnectionRingBuffer(capacity);

        for (var i = 1; i <= capacity + 3; i++)
            buf.Add(MakeEntry((ulong)i));

        buf.Count.ShouldBe(capacity);
    }

    // -----------------------------------------------------------------------
    // 3. GetAll_ReturnsNewestFirst
    // -----------------------------------------------------------------------

    /// <summary>
    /// GetAll should return entries ordered newest-first.
    /// </summary>
    [Fact]
    public void GetAll_ReturnsNewestFirst()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);

        buf.Add(MakeEntry(1));
        buf.Add(MakeEntry(2));
        buf.Add(MakeEntry(3));

        var all = buf.GetAll();

        all.Count.ShouldBe(3);
        all[0].Cid.ShouldBe(3UL); // newest
        all[1].Cid.ShouldBe(2UL);
        all[2].Cid.ShouldBe(1UL); // oldest
    }

    // -----------------------------------------------------------------------
    // 4. GetAll_EmptyBuffer_ReturnsEmpty
    // -----------------------------------------------------------------------

    /// <summary>
    /// GetAll on an empty buffer should return an empty list.
    /// </summary>
    [Fact]
    public void GetAll_EmptyBuffer_ReturnsEmpty()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);

        var all = buf.GetAll();

        all.ShouldBeEmpty();
    }

    // -----------------------------------------------------------------------
    // 5. GetRecent_ReturnsRequestedCount
    // -----------------------------------------------------------------------

    /// <summary>
    /// GetRecent(n) where n &lt;= Count should return exactly n entries.
    /// </summary>
    [Fact]
    public void GetRecent_ReturnsRequestedCount()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);
        for (var i = 1; i <= 8; i++)
            buf.Add(MakeEntry((ulong)i));

        var recent = buf.GetRecent(3);

        recent.Count.ShouldBe(3);
        recent[0].Cid.ShouldBe(8UL); // newest
        recent[1].Cid.ShouldBe(7UL);
        recent[2].Cid.ShouldBe(6UL);
    }

    // -----------------------------------------------------------------------
    // 6. GetRecent_LessThanAvailable_ReturnsAll
    // -----------------------------------------------------------------------

    /// <summary>
    /// GetRecent(n) where n &gt; Count should return all available entries.
    /// </summary>
    [Fact]
    public void GetRecent_LessThanAvailable_ReturnsAll()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);

        buf.Add(MakeEntry(1));
        buf.Add(MakeEntry(2));

        var recent = buf.GetRecent(100);

        recent.Count.ShouldBe(2);
    }

    // -----------------------------------------------------------------------
    // 7. TotalClosed_TracksAllAdditions
    // -----------------------------------------------------------------------

    /// <summary>
    /// TotalClosed should increment for every Add, even after the ring wraps around.
    /// </summary>
    [Fact]
    public void TotalClosed_TracksAllAdditions()
    {
        const int capacity = 4;
        var buf = new ClosedConnectionRingBuffer(capacity);

        buf.TotalClosed.ShouldBe(0L);

        for (var i = 1; i <= 10; i++)
            buf.Add(MakeEntry((ulong)i));

        buf.TotalClosed.ShouldBe(10L);
        buf.Count.ShouldBe(capacity); // buffer is full but total reflects all 10
    }

    // -----------------------------------------------------------------------
    // 8. Clear_ResetsCountAndBuffer
    // -----------------------------------------------------------------------

    /// <summary>
    /// Clear should reset Count to zero and GetAll should return an empty list.
    /// TotalClosed is intentionally not reset because it is a running lifetime counter.
    /// </summary>
    [Fact]
    public void Clear_ResetsCountAndBuffer()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 10);

        buf.Add(MakeEntry(1));
        buf.Add(MakeEntry(2));
        buf.Add(MakeEntry(3));
        buf.TotalClosed.ShouldBe(3L);

        buf.Clear();

        buf.Count.ShouldBe(0);
        buf.GetAll().ShouldBeEmpty();
        // TotalClosed is a lifetime counter; it is not reset by Clear.
        buf.TotalClosed.ShouldBe(3L);
    }

    // -----------------------------------------------------------------------
    // 9. Capacity_ReturnsConfiguredSize
    // -----------------------------------------------------------------------

    /// <summary>
    /// Capacity should reflect the value passed to the constructor.
    /// </summary>
    [Fact]
    public void Capacity_ReturnsConfiguredSize()
    {
        var buf = new ClosedConnectionRingBuffer(capacity: 42);
        buf.Capacity.ShouldBe(42);
    }

    // -----------------------------------------------------------------------
    // 10. Add_WrapsCorrectly
    // -----------------------------------------------------------------------

    /// <summary>
    /// After adding capacity+1 items the oldest entry (cid=1) should no longer be present,
    /// and the buffer should contain the most recent 'capacity' items.
    /// </summary>
    [Fact]
    public void Add_WrapsCorrectly()
    {
        const int capacity = 5;
        var buf = new ClosedConnectionRingBuffer(capacity);

        for (var i = 1; i <= capacity + 1; i++)
            buf.Add(MakeEntry((ulong)i));

        var all = buf.GetAll();

        all.Count.ShouldBe(capacity);

        // cid=1 (the oldest) should have been overwritten
        all.Any(e => e.Cid == 1UL).ShouldBeFalse();

        // The newest entry (cid=capacity+1) should be first
        all[0].Cid.ShouldBe((ulong)(capacity + 1));
    }
}
