// Ported from golang/nats-server/server/jetstream_test.go:
//   TestJetStreamLimitsRetention, TestJetStreamInterestStream,
//   TestJetStreamWorkQueueRetention, TestJetStreamWorkQueueAckAll
//
// These tests exercise the three JetStream retention policies through
// StreamManager.Capture, which is the same code path the Go server uses
// when routing published messages into a stream store.

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

namespace NATS.Server.JetStream.Tests.JetStream;

public class RetentionPolicyParityTests
{
    // Go ref: TestJetStreamLimitsRetention — Limits retention keeps messages up to
    // configured MaxMsgs cap, evicting oldest first. MaxMsgsPer limits per-subject depth.
    // Sequence numbers advance monotonically even as old messages are dropped.
    [Fact]
    public async Task Limits_retention_evicts_oldest_when_max_msgs_exceeded()
    {
        const int maxMsgs = 3;

        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "LIMITS",
            Subjects = ["limits.*"],
            Retention = RetentionPolicy.Limits,
            MaxMsgs = maxMsgs,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        // Publish more messages than the cap allows.
        for (var i = 1; i <= 6; i++)
            manager.Capture("limits.foo", Encoding.UTF8.GetBytes($"msg{i}"));

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

        // Only the last maxMsgs messages remain.
        state.Messages.ShouldBe((ulong)maxMsgs);
        // Sequence numbers are monotonically increasing — they do not wrap.
        state.LastSeq.ShouldBe((ulong)6);
        state.FirstSeq.ShouldBe((ulong)(6 - maxMsgs + 1));
        // The evicted messages are no longer retrievable.
        (await handle.Store.LoadAsync(1, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(2, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(3, default)).ShouldBeNull();
    }

    // Go ref: TestJetStreamLimitsRetention — MaxMsgsPer prunes per-subject depth independently
    // of the global MaxMsgs cap under Limits retention.
    [Fact]
    public async Task Limits_retention_prunes_per_subject_depth_independently()
    {
        var manager = new StreamManager();
        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "LIMITS_PER",
            Subjects = ["lper.*"],
            Retention = RetentionPolicy.Limits,
            MaxMsgsPer = 1,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();

        // Publish two messages to the same subject — only the latest survives.
        manager.Capture("lper.a", "first"u8.ToArray());
        manager.Capture("lper.a", "second"u8.ToArray());
        // Publish to a different subject — it keeps its own slot.
        manager.Capture("lper.b", "only"u8.ToArray());

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

        // One message per subject: lper.a (seq=2), lper.b (seq=3).
        state.Messages.ShouldBe((ulong)2);

        // The first lper.a message was pruned.
        (await handle.Store.LoadAsync(1, default)).ShouldBeNull();
        // The second lper.a and the lper.b message survive.
        (await handle.Store.LoadAsync(2, default)).ShouldNotBeNull();
        (await handle.Store.LoadAsync(3, default)).ShouldNotBeNull();
    }

    // Go ref: TestJetStreamInterestStream — Interest retention behaves like Limits for
    // bounded pruning (MaxMsgs, MaxMsgsPer, MaxAgeMs still apply). It does NOT use an
    // ack-floor to remove messages; pruning is driven purely by limit configuration.
    [Fact]
    public async Task Interest_retention_applies_limits_pruning_but_not_ack_floor_pruning()
    {
        var consumers = new ConsumerManager();
        var manager = new StreamManager(consumerManager: consumers);

        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "INTEREST",
            Subjects = ["interest.*"],
            Retention = RetentionPolicy.Interest,
            MaxMsgs = 5,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();
        consumers.CreateOrUpdate("INTEREST", new ConsumerConfig
        {
            DurableName = "C1",
            AckPolicy = AckPolicy.All,
        }).Error.ShouldBeNull();

        // Publish 3 messages and acknowledge through seq=2.
        manager.Capture("interest.foo", "one"u8.ToArray());
        manager.Capture("interest.foo", "two"u8.ToArray());
        manager.Capture("interest.foo", "three"u8.ToArray());
        consumers.AckAll("INTEREST", "C1", 2);

        // Trigger a retention pass via another publish.
        manager.Capture("interest.foo", "four"u8.ToArray());

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

        // Interest retention does NOT remove messages based on ack floor —
        // all 4 messages remain because MaxMsgs=5 has not been exceeded.
        state.Messages.ShouldBe((ulong)4);
    }

    // Go ref: TestJetStreamWorkQueueRetention — WorkQueue validation rejects a stream whose
    // MaxConsumers is 0 (Go: ErrJetStreamWorkQueueMaxConsumers).
    [Fact]
    public void WorkQueue_retention_validation_rejects_zero_max_consumers()
    {
        var result = JetStreamConfigValidator.Validate(new StreamConfig
        {
            Name = "WQ_INVALID",
            Subjects = ["wq.invalid"],
            Retention = RetentionPolicy.WorkQueue,
            MaxConsumers = 0,
        });

        result.IsValid.ShouldBeFalse();
        result.Message.ShouldNotBeNullOrWhiteSpace();
    }

    // Go ref: TestJetStreamWorkQueueRetention — WorkQueue retention removes messages once
    // a consumer's ack floor advances past them. Messages below the ack floor are pruned
    // on the next Capture call; messages above it remain available.
    [Fact]
    public async Task WorkQueue_retention_removes_messages_below_ack_floor_on_next_publish()
    {
        var consumers = new ConsumerManager();
        var manager = new StreamManager(consumerManager: consumers);

        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "WQ",
            Subjects = ["wq.*"],
            Retention = RetentionPolicy.WorkQueue,
            MaxConsumers = 1,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();
        consumers.CreateOrUpdate("WQ", new ConsumerConfig
        {
            DurableName = "WORKER",
            AckPolicy = AckPolicy.All,
        }).Error.ShouldBeNull();

        // Publish three messages.
        manager.Capture("wq.a", "first"u8.ToArray());
        manager.Capture("wq.a", "second"u8.ToArray());
        manager.Capture("wq.a", "third"u8.ToArray());

        // Acknowledge through seq=2 — floor advances to 2.
        consumers.AckAll("WQ", "WORKER", 2).ShouldBeTrue();

        // Next publish triggers the WorkQueue retention pass.
        manager.Capture("wq.a", "fourth"u8.ToArray());

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

        // Messages 1 and 2 were at or below the ack floor and must be removed.
        // Messages 3 and 4 are above the floor and must still be present.
        state.Messages.ShouldBe((ulong)2);
        (await handle.Store.LoadAsync(1, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(2, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(3, default)).ShouldNotBeNull();
        (await handle.Store.LoadAsync(4, default)).ShouldNotBeNull();
    }

    // Go ref: TestJetStreamWorkQueueAckAll — a full AckAll to the last sequence causes
    // all previously stored messages to be pruned on the next Capture. The stream then
    // contains only the newly published message.
    [Fact]
    public async Task WorkQueue_retention_prunes_all_messages_when_ack_floor_reaches_last_seq()
    {
        var consumers = new ConsumerManager();
        var manager = new StreamManager(consumerManager: consumers);

        manager.CreateOrUpdate(new StreamConfig
        {
            Name = "WQ_FULL",
            Subjects = ["wqf.*"],
            Retention = RetentionPolicy.WorkQueue,
            MaxConsumers = 1,
            Storage = StorageType.Memory,
        }).Error.ShouldBeNull();
        consumers.CreateOrUpdate("WQ_FULL", new ConsumerConfig
        {
            DurableName = "WORKER",
            AckPolicy = AckPolicy.All,
        }).Error.ShouldBeNull();

        manager.Capture("wqf.a", "one"u8.ToArray());
        manager.Capture("wqf.a", "two"u8.ToArray());
        manager.Capture("wqf.a", "three"u8.ToArray());

        // Acknowledge through the last sequence — floor reaches seq=3.
        consumers.AckAll("WQ_FULL", "WORKER", 3).ShouldBeTrue();

        // Trigger retention pass.
        manager.Capture("wqf.a", "four"u8.ToArray());

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

        // All three previously stored messages are pruned; only seq=4 remains.
        state.Messages.ShouldBe((ulong)1);
        state.LastSeq.ShouldBe((ulong)4);
        (await handle.Store.LoadAsync(1, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(2, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(3, default)).ShouldBeNull();
        (await handle.Store.LoadAsync(4, default)).ShouldNotBeNull();
    }
}
