// Go: consumer.go — Pull consumer timeout enforcement and compiled filter tests.
// ExpiresMs support per consumer.go pull request handling.
// CompiledFilter optimizes multi-subject filter matching for consumers.
using System.Text;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Consumers;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Storage;

namespace NATS.Server.Tests.JetStream.Consumers;

public class PullConsumerTimeoutTests
{
    private static StreamHandle MakeStream(MemStore store)
        => new(new StreamConfig { Name = "TEST", Subjects = ["test.>"] }, store);

    private static ConsumerHandle MakeConsumer(ConsumerConfig? config = null)
        => new("TEST", config ?? new ConsumerConfig { DurableName = "C1" });

    // -------------------------------------------------------------------------
    // Test 1 — ExpiresMs returns partial batch when timeout fires
    //
    // Go reference: consumer.go — pull fetch with expires returns whatever
    // messages are available when the timeout fires, even if batch is not full.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task FetchAsync_ExpiresMs_ReturnsPartialBatch()
    {
        var store = new MemStore();
        var stream = MakeStream(store);

        // Store only 2 messages, but request a batch of 10
        await store.AppendAsync("test.a", Encoding.UTF8.GetBytes("msg1"), CancellationToken.None);
        await store.AppendAsync("test.b", Encoding.UTF8.GetBytes("msg2"), CancellationToken.None);

        var consumer = MakeConsumer();
        var engine = new PullConsumerEngine();

        var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
        {
            Batch = 10,
            ExpiresMs = 100,
        }, CancellationToken.None);

        // Should get the 2 available messages (partial batch)
        result.Messages.Count.ShouldBe(2);
        result.Messages[0].Subject.ShouldBe("test.a");
        result.Messages[1].Subject.ShouldBe("test.b");
    }

    // -------------------------------------------------------------------------
    // Test 2 — ExpiresMs sets TimedOut = true on partial result
    //
    // Go reference: consumer.go — when a pull request expires and the batch
    // is not fully filled, the response indicates a timeout occurred.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task FetchAsync_ExpiresMs_ReturnsTimedOutTrue()
    {
        var store = new MemStore();
        var stream = MakeStream(store);

        // Store no messages — the fetch should time out with empty results
        var consumer = MakeConsumer();
        var engine = new PullConsumerEngine();

        var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
        {
            Batch = 5,
            ExpiresMs = 50,
        }, CancellationToken.None);

        result.TimedOut.ShouldBeTrue();
        result.Messages.Count.ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // Test 3 — No ExpiresMs waits for full batch (returns what's available)
    //
    // Go reference: consumer.go — without expires, the fetch returns available
    // messages up to batch size without a timeout constraint.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task FetchAsync_NoExpires_WaitsForFullBatch()
    {
        var store = new MemStore();
        var stream = MakeStream(store);

        await store.AppendAsync("test.a", Encoding.UTF8.GetBytes("msg1"), CancellationToken.None);
        await store.AppendAsync("test.b", Encoding.UTF8.GetBytes("msg2"), CancellationToken.None);
        await store.AppendAsync("test.c", Encoding.UTF8.GetBytes("msg3"), CancellationToken.None);

        var consumer = MakeConsumer();
        var engine = new PullConsumerEngine();

        var result = await engine.FetchAsync(stream, consumer, new PullFetchRequest
        {
            Batch = 3,
            ExpiresMs = 0, // No timeout
        }, CancellationToken.None);

        result.Messages.Count.ShouldBe(3);
        result.TimedOut.ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Test 4 — CompiledFilter with no filters matches everything
    //
    // Go reference: consumer.go — a consumer with no filter subjects receives
    // all messages from the stream.
    // -------------------------------------------------------------------------
    [Fact]
    public void CompiledFilter_NoFilters_MatchesEverything()
    {
        var filter = new CompiledFilter([]);

        filter.Matches("test.a").ShouldBeTrue();
        filter.Matches("foo.bar.baz").ShouldBeTrue();
        filter.Matches("anything").ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // Test 5 — CompiledFilter with single exact filter matches only that subject
    //
    // Go reference: consumer.go — single filter_subject matches via MatchLiteral.
    // -------------------------------------------------------------------------
    [Fact]
    public void CompiledFilter_SingleFilter_MatchesExact()
    {
        var filter = new CompiledFilter(["test.specific"]);

        filter.Matches("test.specific").ShouldBeTrue();
        filter.Matches("test.other").ShouldBeFalse();
        filter.Matches("test").ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Test 6 — CompiledFilter with single wildcard filter
    //
    // Go reference: consumer.go — wildcard filter_subject uses MatchLiteral
    // which supports * (single token) and > (multi-token) wildcards.
    // -------------------------------------------------------------------------
    [Fact]
    public void CompiledFilter_SingleWildcard_MatchesPattern()
    {
        var starFilter = new CompiledFilter(["test.*"]);
        starFilter.Matches("test.a").ShouldBeTrue();
        starFilter.Matches("test.b").ShouldBeTrue();
        starFilter.Matches("test.a.b").ShouldBeFalse();
        starFilter.Matches("other.a").ShouldBeFalse();

        var fwcFilter = new CompiledFilter(["test.>"]);
        fwcFilter.Matches("test.a").ShouldBeTrue();
        fwcFilter.Matches("test.a.b").ShouldBeTrue();
        fwcFilter.Matches("test.a.b.c").ShouldBeTrue();
        fwcFilter.Matches("other.a").ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Test 7 — CompiledFilter with multiple filters matches any
    //
    // Go reference: consumer.go — filter_subjects (plural) matches if ANY of
    // the patterns match. Uses HashSet for exact subjects + MatchLiteral for
    // wildcard patterns.
    // -------------------------------------------------------------------------
    [Fact]
    public void CompiledFilter_MultipleFilters_MatchesAny()
    {
        var filter = new CompiledFilter(["orders.us", "orders.eu", "events.>"]);

        // Exact matches
        filter.Matches("orders.us").ShouldBeTrue();
        filter.Matches("orders.eu").ShouldBeTrue();

        // Wildcard match
        filter.Matches("events.created").ShouldBeTrue();
        filter.Matches("events.updated.v2").ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // Test 8 — CompiledFilter with multiple filters rejects non-matching
    //
    // Go reference: consumer.go — subjects that match none of the filter
    // patterns are excluded from delivery.
    // -------------------------------------------------------------------------
    [Fact]
    public void CompiledFilter_MultipleFilters_RejectsNonMatching()
    {
        var filter = new CompiledFilter(["orders.us", "orders.eu", "events.>"]);

        filter.Matches("orders.jp").ShouldBeFalse();
        filter.Matches("billing.us").ShouldBeFalse();
        filter.Matches("events").ShouldBeFalse(); // ">" requires at least one token after
        filter.Matches("random.subject").ShouldBeFalse();
    }
}
