// Ported from golang/nats-server/server/memstore_test.go:
//   TestMemStoreMsgLimit, TestMemStoreBytesLimit, TestMemStoreAgeLimit
//
// Retention limits are enforced by StreamManager (which calls MemStore.TrimToMaxMessages,
// removes oldest messages by bytes, and prunes by age). These tests exercise the full
// Limits-retention path via StreamManager.Capture, which is the code path the Go server
// exercises through its StoreMsg integration.

using System.Text;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Models;

namespace NATS.Server.JetStream.Tests.JetStream.Storage;

public class StorageRetentionTests
{
    // Go ref: TestMemStoreMsgLimit — store MaxMsgs+N messages; only MaxMsgs remain,
    // oldest are evicted, sequence window advances.
    [Fact]
    public async Task Max_msgs_limit_enforced()
    {
        const int maxMsgs = 10;
        const int overCount = 20;

        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "MSGLIMIT",
            Subjects = ["msglimit.*"],
            MaxMsgs = maxMsgs,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        for (var i = 0; i < overCount; i++)
            manager.Capture("msglimit.foo", Encoding.UTF8.GetBytes($"msg{i}"));

        manager.TryGet("MSGLIMIT", out var handle).ShouldBeTrue();
        var state = await handle.Store.GetStateAsync(default);

        state.Messages.ShouldBe((ulong)maxMsgs);
        // The last stored sequence is overCount.
        state.LastSeq.ShouldBe((ulong)overCount);
        // The first kept sequence is overCount - maxMsgs + 1.
        state.FirstSeq.ShouldBe((ulong)(overCount - maxMsgs + 1));
    }

    // Go ref: TestMemStoreBytesLimit — store messages until bytes exceed MaxBytes;
    // oldest messages are purged to keep total bytes at or below the limit.
    [Fact]
    public async Task Max_bytes_limit_enforced()
    {
        // Go: memStoreMsgSize = subject.Length + headers.Length + data.Length + 16
        // Each message = "byteslimit.foo"(14) + payload(100) + overhead(16) = 130 bytes.
        var payload = new byte[100];
        const string subject = "byteslimit.foo";
        const int msgSize = 14 + 100 + 16; // 130
        const int maxCapacity = 5;
        var maxBytes = (long)(msgSize * maxCapacity); // 650

        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "BYTESLIMIT",
            Subjects = ["byteslimit.*"],
            MaxBytes = maxBytes,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        // Store exactly maxCapacity messages — should all fit.
        for (var i = 0; i < maxCapacity; i++)
            manager.Capture(subject, payload);

        manager.TryGet("BYTESLIMIT", out var handle).ShouldBeTrue();
        var stateAtCapacity = await handle.Store.GetStateAsync(default);
        stateAtCapacity.Messages.ShouldBe((ulong)maxCapacity);
        stateAtCapacity.Bytes.ShouldBe((ulong)(msgSize * maxCapacity));

        // Store 5 more — each one should displace an old message.
        for (var i = 0; i < maxCapacity; i++)
            manager.Capture(subject, payload);

        var stateFinal = await handle.Store.GetStateAsync(default);
        stateFinal.Messages.ShouldBe((ulong)maxCapacity);
        stateFinal.Bytes.ShouldBeLessThanOrEqualTo((ulong)maxBytes);
        stateFinal.LastSeq.ShouldBe((ulong)(maxCapacity * 2));
    }

    // Go ref: TestMemStoreAgeLimit — messages older than MaxAge are pruned on the next Capture.
    // In the Go server, the memstore runs a background timer; in the .NET port, pruning happens
    // synchronously inside StreamManager.Capture via PruneExpiredMessages which compares
    // TimestampUtc against (now - MaxAge). We backdate stored messages to simulate expiry.
    [Fact]
    public async Task Max_age_limit_enforced()
    {
        // Use a 1-second MaxAge so we can reason clearly about cutoff.
        const int maxAgeMs = 1000;

        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "AGELIMIT",
            Subjects = ["agelimit.*"],
            MaxAgeMs = maxAgeMs,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        // Store 5 messages that are logically "already expired" by storing them,
        // then relying on an additional capture after sleeping past MaxAge to trigger
        // the pruning pass.
        const int initialCount = 5;
        for (var i = 0; i < initialCount; i++)
            manager.Capture("agelimit.foo", Encoding.UTF8.GetBytes($"msg{i}"));

        manager.TryGet("AGELIMIT", out var handle).ShouldBeTrue();
        var stateBefore = await handle.Store.GetStateAsync(default);
        stateBefore.Messages.ShouldBe((ulong)initialCount);

        // Wait for MaxAge to elapse so the stored messages are now older than the cutoff.
        await Task.Delay(maxAgeMs + 50);

        // A subsequent Capture triggers PruneExpiredMessages, which removes all messages
        // whose TimestampUtc < (now - MaxAge).
        manager.Capture("agelimit.foo", "trigger"u8.ToArray());

        var stateAfter = await handle.Store.GetStateAsync(default);
        // Only the freshly-appended trigger message should remain.
        stateAfter.Messages.ShouldBe((ulong)1);
        stateAfter.Bytes.ShouldBeGreaterThan((ulong)0);
    }

    // Go ref: TestMemStoreMsgLimit — verifies that sequence numbers keep incrementing even
    // after old messages are evicted; the store window moves forward rather than wrapping.
    [Fact]
    public async Task Sequence_numbers_monotonically_increase_through_eviction()
    {
        const int maxMsgs = 5;
        const int totalToStore = 15;

        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "SEQMONOT",
            Subjects = ["seqmonot.*"],
            MaxMsgs = maxMsgs,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        for (var i = 1; i <= totalToStore; i++)
            manager.Capture("seqmonot.foo", Encoding.UTF8.GetBytes($"msg{i}"));

        manager.TryGet("SEQMONOT", out var handle).ShouldBeTrue();
        var state = await handle.Store.GetStateAsync(default);

        state.Messages.ShouldBe((ulong)maxMsgs);
        state.LastSeq.ShouldBe((ulong)totalToStore);
        state.FirstSeq.ShouldBe((ulong)(totalToStore - maxMsgs + 1));

        // The first evicted sequence (1) is no longer loadable.
        (await handle.Store.LoadAsync(1, default)).ShouldBeNull();
        // The last evicted sequence is totalToStore - maxMsgs (= 10).
        (await handle.Store.LoadAsync((ulong)(totalToStore - maxMsgs), default)).ShouldBeNull();
        // The first surviving message is still present.
        (await handle.Store.LoadAsync((ulong)(totalToStore - maxMsgs + 1), default)).ShouldNotBeNull();
    }
}
