// Go reference: jetstream_api.go:1200-1350 — stream purge supports options: subject filter,
// sequence cutoff, and keep-last-N. Combinations like filter+keep allow keeping the last N
// messages per matching subject.

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

namespace NATS.Server.JetStream.Tests.JetStream.Api;

public class StreamPurgeOptionsTests
{
    private static JetStreamApiRouter CreateRouterWithStream(string streamName, string subjectPattern, out StreamManager streamManager)
    {
        streamManager = new StreamManager();
        var consumerManager = new ConsumerManager();
        var router = new JetStreamApiRouter(streamManager, consumerManager);

        var payload = Encoding.UTF8.GetBytes($$$"""{"name":"{{{streamName}}}","subjects":["{{{subjectPattern}}}"]}""");
        var result = router.Route($"$JS.API.STREAM.CREATE.{streamName}", payload);
        result.Error.ShouldBeNull();

        return router;
    }

    private static async Task PublishAsync(StreamManager streamManager, string subject, string payload)
    {
        var stream = streamManager.FindBySubject(subject);
        stream.ShouldNotBeNull();
        await stream.Store.AppendAsync(subject, Encoding.UTF8.GetBytes(payload), default);
    }

    /// <summary>
    /// Purge with no options removes all messages and returns the count.
    /// Go reference: jetstream_api.go — basic purge with empty request body.
    /// </summary>
    [Fact]
    public async Task Purge_NoOptions_RemovesAll()
    {
        var router = CreateRouterWithStream("TEST", "test.>", out var sm);

        await PublishAsync(sm, "test.a", "1");
        await PublishAsync(sm, "test.b", "2");
        await PublishAsync(sm, "test.c", "3");

        var result = router.Route("$JS.API.STREAM.PURGE.TEST", Encoding.UTF8.GetBytes("{}"));
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(3UL);

        var state = await sm.GetStateAsync("TEST", default);
        state.Messages.ShouldBe(0UL);
    }

    /// <summary>
    /// Purge with a subject filter removes only messages matching the pattern.
    /// Go reference: jetstream_api.go:1200-1350 — filter option.
    /// </summary>
    [Fact]
    public async Task Purge_WithSubjectFilter_RemovesOnlyMatching()
    {
        var router = CreateRouterWithStream("TEST", ">", out var sm);

        await PublishAsync(sm, "orders.a", "1");
        await PublishAsync(sm, "orders.b", "2");
        await PublishAsync(sm, "logs.x", "3");
        await PublishAsync(sm, "orders.c", "4");

        var payload = Encoding.UTF8.GetBytes("""{"filter":"orders.*"}""");
        var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(3UL);

        var state = await sm.GetStateAsync("TEST", default);
        state.Messages.ShouldBe(1UL);
    }

    /// <summary>
    /// Purge with seq option removes all messages with sequence strictly less than the given value.
    /// Go reference: jetstream_api.go:1200-1350 — seq option.
    /// </summary>
    [Fact]
    public async Task Purge_WithSeq_RemovesBelowSequence()
    {
        var router = CreateRouterWithStream("TEST", "test.>", out var sm);

        await PublishAsync(sm, "test.a", "1"); // seq 1
        await PublishAsync(sm, "test.b", "2"); // seq 2
        await PublishAsync(sm, "test.c", "3"); // seq 3
        await PublishAsync(sm, "test.d", "4"); // seq 4
        await PublishAsync(sm, "test.e", "5"); // seq 5

        // Remove all messages with seq < 4 (i.e., sequences 1, 2, 3).
        var payload = Encoding.UTF8.GetBytes("""{"seq":4}""");
        var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(3UL);

        var state = await sm.GetStateAsync("TEST", default);
        state.Messages.ShouldBe(2UL);
    }

    /// <summary>
    /// Purge with keep option retains the last N messages globally.
    /// Go reference: jetstream_api.go:1200-1350 — keep option.
    /// </summary>
    [Fact]
    public async Task Purge_WithKeep_KeepsLastN()
    {
        var router = CreateRouterWithStream("TEST", "test.>", out var sm);

        await PublishAsync(sm, "test.a", "1"); // seq 1
        await PublishAsync(sm, "test.b", "2"); // seq 2
        await PublishAsync(sm, "test.c", "3"); // seq 3
        await PublishAsync(sm, "test.d", "4"); // seq 4
        await PublishAsync(sm, "test.e", "5"); // seq 5

        // Keep the last 2 messages (seq 4, 5); purge 1, 2, 3.
        var payload = Encoding.UTF8.GetBytes("""{"keep":2}""");
        var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(3UL);

        var state = await sm.GetStateAsync("TEST", default);
        state.Messages.ShouldBe(2UL);
    }

    /// <summary>
    /// Purge with both filter and keep retains the last N messages per matching subject.
    /// Go reference: jetstream_api.go:1200-1350 — filter+keep combination.
    /// </summary>
    [Fact]
    public async Task Purge_FilterAndKeep_KeepsLastNPerFilter()
    {
        var router = CreateRouterWithStream("TEST", ">", out var sm);

        // Publish multiple messages on two subjects.
        await PublishAsync(sm, "orders.a", "o1"); // seq 1
        await PublishAsync(sm, "orders.a", "o2"); // seq 2
        await PublishAsync(sm, "orders.a", "o3"); // seq 3
        await PublishAsync(sm, "logs.x", "l1");   // seq 4 — not matching filter
        await PublishAsync(sm, "orders.b", "ob1"); // seq 5
        await PublishAsync(sm, "orders.b", "ob2"); // seq 6

        // Keep last 1 per matching subject "orders.*".
        // orders.a has 3 msgs -> keep seq 3, purge seq 1, 2
        // orders.b has 2 msgs -> keep seq 6, purge seq 5
        // logs.x is unaffected (does not match filter)
        var payload = Encoding.UTF8.GetBytes("""{"filter":"orders.*","keep":1}""");
        var result = router.Route("$JS.API.STREAM.PURGE.TEST", payload);
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(3UL);

        var state = await sm.GetStateAsync("TEST", default);
        // Remaining: orders.a seq 3, logs.x seq 4, orders.b seq 6 = 3 messages
        state.Messages.ShouldBe(3UL);
    }

    /// <summary>
    /// Purge on a non-existent stream returns a 404 not-found error.
    /// Go reference: jetstream_api.go — stream not found.
    /// </summary>
    [Fact]
    public void Purge_InvalidStream_ReturnsNotFound()
    {
        var streamManager = new StreamManager();
        var consumerManager = new ConsumerManager();
        var router = new JetStreamApiRouter(streamManager, consumerManager);

        var result = router.Route("$JS.API.STREAM.PURGE.NONEXISTENT", Encoding.UTF8.GetBytes("{}"));
        result.Error.ShouldNotBeNull();
        result.Error!.Code.ShouldBe(404);
    }

    /// <summary>
    /// Purge on an empty stream returns success with zero purged count.
    /// Go reference: jetstream_api.go — purge on empty stream.
    /// </summary>
    [Fact]
    public void Purge_EmptyStream_ReturnsZeroPurged()
    {
        var router = CreateRouterWithStream("TEST", "test.>", out _);

        var result = router.Route("$JS.API.STREAM.PURGE.TEST", Encoding.UTF8.GetBytes("{}"));
        result.Error.ShouldBeNull();
        result.Success.ShouldBeTrue();
        result.Purged.ShouldBe(0UL);
    }
}
