// Go reference: golang/nats-server/server/jetstream_consumer_test.go
// Ports Go consumer tests that map to existing .NET infrastructure:
// multiple filters, consumer actions, filter matching, priority groups,
// ack timeout retry, descriptions, single-token subjects, overflow.

using System.Text.RegularExpressions;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Consumers;
using NATS.Server.JetStream.Models;
using NATS.Server.Subscriptions;

namespace NATS.Server.Tests.JetStream.Consumers;

/// <summary>
/// Go parity tests ported from jetstream_consumer_test.go for consumer
/// behaviors including filter matching, consumer actions, priority groups,
/// ack retry, descriptions, and overflow handling.
/// </summary>
public class ConsumerGoParityTests
{
    // =========================================================================
    // Helper: Generate N filter subjects matching Go's filterSubjects() function.
    // Go: jetstream_consumer_test.go:829
    // =========================================================================

    private static List<string> GenerateFilterSubjects(int n)
    {
        var fs = new List<string>();
        while (fs.Count < n)
        {
            var literals = new[] { "foo", "bar", Guid.NewGuid().ToString("N")[..8], "xyz", "abcdef" };
            fs.Add(string.Join('.', literals));
            if (fs.Count >= n) break;

            for (int i = 0; i < literals.Length && fs.Count < n; i++)
            {
                var entry = new string[literals.Length];
                for (int j = 0; j < literals.Length; j++)
                    entry[j] = j == i ? "*" : literals[j];
                fs.Add(string.Join('.', entry));
            }
        }

        return fs.Take(n).ToList();
    }

    // =========================================================================
    // TestJetStreamConsumerIsFilteredMatch — jetstream_consumer_test.go:856
    // Tests the filter matching logic used by consumers to determine if a
    // message subject matches their filter configuration.
    // =========================================================================

    [Theory]
    [InlineData(new string[0], "foo.bar", true)]          // no filter = match all
    [InlineData(new[] { "foo.baz", "foo.bar" }, "foo.bar", true)]    // literal match
    [InlineData(new[] { "foo.baz", "foo.bar" }, "foo.ban", false)]   // literal mismatch
    [InlineData(new[] { "bar.>", "foo.>" }, "foo.bar", true)]        // wildcard > match
    [InlineData(new[] { "bar.>", "foo.>" }, "bar.foo", true)]        // wildcard > match
    [InlineData(new[] { "bar.>", "foo.>" }, "baz.foo", false)]       // wildcard > mismatch
    [InlineData(new[] { "bar.*", "foo.*" }, "foo.bar", true)]        // wildcard * match
    [InlineData(new[] { "bar.*", "foo.*" }, "bar.foo", true)]        // wildcard * match
    [InlineData(new[] { "bar.*", "foo.*" }, "baz.foo", false)]       // wildcard * mismatch
    [InlineData(new[] { "foo.*.x", "foo.*.y" }, "foo.bar.x", true)]  // multi-token wildcard match
    [InlineData(new[] { "foo.*.x", "foo.*.y", "foo.*.z" }, "foo.bar.z", true)] // multi wildcard match
    public void IsFilteredMatch_basic_cases(string[] filters, string subject, bool expected)
    {
        // Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:856
        var compiled = new CompiledFilter(filters);
        compiled.Matches(subject).ShouldBe(expected);
    }

    [Fact]
    public void IsFilteredMatch_many_filters_mismatch()
    {
        // Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:874
        // 100 filter subjects, none should match "foo.bar.do.not.match.any.filter.subject"
        var filters = GenerateFilterSubjects(100);
        var compiled = new CompiledFilter(filters);
        compiled.Matches("foo.bar.do.not.match.any.filter.subject").ShouldBeFalse();
    }

    [Fact]
    public void IsFilteredMatch_many_filters_match()
    {
        // Go: TestJetStreamConsumerIsFilteredMatch jetstream_consumer_test.go:875
        // 100 filter subjects; "foo.bar.*.xyz.abcdef" should be among them, matching
        // "foo.bar.12345.xyz.abcdef" via wildcard
        var filters = GenerateFilterSubjects(100);
        var compiled = new CompiledFilter(filters);
        // One of the generated wildcard filters should be "foo.bar.*.xyz.abcdef"
        // which matches "foo.bar.12345.xyz.abcdef"
        compiled.Matches("foo.bar.12345.xyz.abcdef").ShouldBeTrue();
    }

    // =========================================================================
    // TestJetStreamConsumerIsEqualOrSubsetMatch — jetstream_consumer_test.go:921
    // Tests whether a subject is an equal or subset match of the consumer's filters.
    // This is used for work queue overlap detection.
    // =========================================================================

    [Theory]
    [InlineData(new string[0], "foo.bar", false)]                    // no filter = no subset
    [InlineData(new[] { "foo.baz", "foo.bar" }, "foo.bar", true)]    // literal match
    [InlineData(new[] { "foo.baz", "foo.bar" }, "foo.ban", false)]   // literal mismatch
    [InlineData(new[] { "bar.>", "foo.>" }, "foo.>", true)]          // equal wildcard match
    [InlineData(new[] { "bar.foo.>", "foo.bar.>" }, "bar.>", true)]  // subset match: bar.foo.> is subset of bar.>
    [InlineData(new[] { "bar.>", "foo.>" }, "baz.foo.>", false)]     // no match
    public void IsEqualOrSubsetMatch_basic_cases(string[] filters, string subject, bool expected)
    {
        // Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:921
        // A subject is a "subset match" if any filter equals the subject or if
        // the filter is a more specific version (subset) of the subject.
        // Filter "bar.foo.>" is a subset of subject "bar.>" because bar.foo.> matches
        // only things that bar.> also matches.
        bool result = false;
        foreach (var filter in filters)
        {
            // Equal match
            if (string.Equals(filter, subject, StringComparison.Ordinal))
            {
                result = true;
                break;
            }

            // Subset match: filter is more specific (subset) than subject
            // i.e., everything matched by filter is also matched by subject
            if (SubjectMatch.MatchLiteral(filter, subject))
            {
                result = true;
                break;
            }
        }

        result.ShouldBe(expected);
    }

    [Fact]
    public void IsEqualOrSubsetMatch_many_filters_literal()
    {
        // Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:934
        var filters = GenerateFilterSubjects(100);
        // One of the generated filters is a literal like "foo.bar.<uuid>.xyz.abcdef"
        // The subject "foo.bar.*.xyz.abcdef" is a pattern that all such literals match
        bool found = filters.Any(f => SubjectMatch.MatchLiteral(f, "foo.bar.*.xyz.abcdef"));
        found.ShouldBeTrue();
    }

    [Fact]
    public void IsEqualOrSubsetMatch_many_filters_subset()
    {
        // Go: TestJetStreamConsumerIsEqualOrSubsetMatch jetstream_consumer_test.go:935
        var filters = GenerateFilterSubjects(100);
        // "foo.bar.>" should match many of the generated filters as a superset
        bool found = filters.Any(f => SubjectMatch.MatchLiteral(f, "foo.bar.>"));
        found.ShouldBeTrue();
    }

    // =========================================================================
    // TestJetStreamConsumerActions — jetstream_consumer_test.go:472
    // Tests consumer create/update action semantics.
    // =========================================================================

    [Fact]
    public async Task Consumer_create_action_succeeds_for_new_consumer()
    {
        // Go: TestJetStreamConsumerActions jetstream_consumer_test.go:472
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var response = await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one", "two"],
            ackPolicy: AckPolicy.Explicit);

        response.Error.ShouldBeNull();
        response.ConsumerInfo.ShouldNotBeNull();
    }

    [Fact]
    public async Task Consumer_create_action_idempotent_with_same_config()
    {
        // Go: TestJetStreamConsumerActions jetstream_consumer_test.go:497
        // Create consumer again with identical config should succeed
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var r1 = await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one", "two"],
            ackPolicy: AckPolicy.Explicit);
        r1.Error.ShouldBeNull();

        var r2 = await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one", "two"],
            ackPolicy: AckPolicy.Explicit);
        r2.Error.ShouldBeNull();
    }

    [Fact]
    public async Task Consumer_update_existing_succeeds()
    {
        // Go: TestJetStreamConsumerActions jetstream_consumer_test.go:516
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one", "two"],
            ackPolicy: AckPolicy.Explicit);

        // Update filter subjects
        var response = await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one"],
            ackPolicy: AckPolicy.Explicit);
        response.Error.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerActionsOnWorkQueuePolicyStream — jetstream_consumer_test.go:557
    // Tests consumer actions on a work queue policy stream.
    // =========================================================================

    [Fact]
    public async Task Consumer_on_work_queue_stream()
    {
        // Go: TestJetStreamConsumerActionsOnWorkQueuePolicyStream jetstream_consumer_test.go:557
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "TEST",
            Subjects = ["one", "two", "three", "four", "five.>"],
            Retention = RetentionPolicy.WorkQueue,
        });

        var r1 = await fx.CreateConsumerAsync("TEST", "DUR", null,
            filterSubjects: ["one", "two"],
            ackPolicy: AckPolicy.Explicit);
        r1.Error.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerPedanticMode — jetstream_consumer_test.go:1253
    // Consumer pedantic mode validates various configuration constraints.
    // We test the validation that exists in the .NET implementation.
    // =========================================================================

    [Fact]
    public async Task Consumer_ephemeral_can_be_created()
    {
        // Go: TestJetStreamConsumerPedanticMode jetstream_consumer_test.go:1253
        // Test that ephemeral consumers can be created
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var response = await fx.CreateConsumerAsync("TEST", "EPH", null,
            filterSubjects: ["one"],
            ackPolicy: AckPolicy.Explicit,
            ephemeral: true);
        response.Error.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerMultipleFiltersRemoveFilters — jetstream_consumer_test.go:45
    // Consumer with multiple filter subjects, then updating to fewer.
    // =========================================================================

    [Fact]
    public async Task Consumer_multiple_filters_can_be_updated()
    {
        // Go: TestJetStreamConsumerMultipleFiltersRemoveFilters jetstream_consumer_test.go:45
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        // Create consumer with multiple filters
        var r1 = await fx.CreateConsumerAsync("TEST", "CF", null,
            filterSubjects: ["one", "two", "three"]);
        r1.Error.ShouldBeNull();

        // Update to fewer filters
        var r2 = await fx.CreateConsumerAsync("TEST", "CF", null,
            filterSubjects: ["one"]);
        r2.Error.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerMultipleConsumersSingleFilter — jetstream_consumer_test.go:188
    // Multiple consumers each with a single filter on the same stream.
    // =========================================================================

    [Fact]
    public async Task Multiple_consumers_each_with_single_filter()
    {
        // Go: TestJetStreamConsumerMultipleConsumersSingleFilter jetstream_consumer_test.go:188
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var r1 = await fx.CreateConsumerAsync("TEST", "C1", "one");
        r1.Error.ShouldBeNull();

        var r2 = await fx.CreateConsumerAsync("TEST", "C2", "two");
        r2.Error.ShouldBeNull();

        // Publish to each filter
        var ack1 = await fx.PublishAndGetAckAsync("one", "msg1");
        ack1.ErrorCode.ShouldBeNull();
        var ack2 = await fx.PublishAndGetAckAsync("two", "msg2");
        ack2.ErrorCode.ShouldBeNull();

        // Each consumer should see only its filtered messages
        var batch1 = await fx.FetchAsync("TEST", "C1", 10);
        batch1.Messages.ShouldNotBeEmpty();
        batch1.Messages.All(m => m.Subject == "one").ShouldBeTrue();

        var batch2 = await fx.FetchAsync("TEST", "C2", 10);
        batch2.Messages.ShouldNotBeEmpty();
        batch2.Messages.All(m => m.Subject == "two").ShouldBeTrue();
    }

    // =========================================================================
    // TestJetStreamConsumerMultipleConsumersMultipleFilters — jetstream_consumer_test.go:300
    // Multiple consumers with overlapping multiple filter subjects.
    // =========================================================================

    [Fact]
    public async Task Multiple_consumers_with_multiple_filters()
    {
        // Go: TestJetStreamConsumerMultipleConsumersMultipleFilters jetstream_consumer_test.go:300
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var r1 = await fx.CreateConsumerAsync("TEST", "C1", null,
            filterSubjects: ["one", "two"]);
        r1.Error.ShouldBeNull();

        var r2 = await fx.CreateConsumerAsync("TEST", "C2", null,
            filterSubjects: ["two", "three"]);
        r2.Error.ShouldBeNull();

        await fx.PublishAndGetAckAsync("one", "msg1");
        await fx.PublishAndGetAckAsync("two", "msg2");
        await fx.PublishAndGetAckAsync("three", "msg3");

        // C1 should see "one" and "two"
        var batch1 = await fx.FetchAsync("TEST", "C1", 10);
        batch1.Messages.Count.ShouldBe(2);

        // C2 should see "two" and "three"
        var batch2 = await fx.FetchAsync("TEST", "C2", 10);
        batch2.Messages.Count.ShouldBe(2);
    }

    // =========================================================================
    // TestJetStreamConsumerMultipleFiltersSequence — jetstream_consumer_test.go:426
    // Verifies sequence ordering with multiple filter subjects.
    // =========================================================================

    [Fact]
    public async Task Multiple_filters_preserve_sequence_order()
    {
        // Go: TestJetStreamConsumerMultipleFiltersSequence jetstream_consumer_test.go:426
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        await fx.CreateConsumerAsync("TEST", "CF", null,
            filterSubjects: ["one", "two"]);

        await fx.PublishAndGetAckAsync("one", "msg1");
        await fx.PublishAndGetAckAsync("two", "msg2");
        await fx.PublishAndGetAckAsync("one", "msg3");

        var batch = await fx.FetchAsync("TEST", "CF", 10);
        batch.Messages.Count.ShouldBe(3);

        // Verify sequences are in order
        for (int i = 1; i < batch.Messages.Count; i++)
        {
            batch.Messages[i].Sequence.ShouldBeGreaterThan(batch.Messages[i - 1].Sequence);
        }
    }

    // =========================================================================
    // TestJetStreamConsumerPinned — jetstream_consumer_test.go:1545
    // Priority group registration and active consumer selection.
    // =========================================================================

    [Fact]
    public void PriorityGroup_pinned_consumer_gets_messages()
    {
        // Go: TestJetStreamConsumerPinned jetstream_consumer_test.go:1545
        var mgr = new PriorityGroupManager();
        mgr.Register("group1", "C1", priority: 1);
        mgr.Register("group1", "C2", priority: 2);

        // C1 (lowest priority number) should be active
        mgr.IsActive("group1", "C1").ShouldBeTrue();
        mgr.IsActive("group1", "C2").ShouldBeFalse();
    }

    // =========================================================================
    // TestJetStreamConsumerPinnedUnsetsAfterAtMostPinnedTTL — jetstream_consumer_test.go:1711
    // When the pinned consumer disconnects, the next one takes over.
    // =========================================================================

    [Fact]
    public void PriorityGroup_pinned_unsets_on_disconnect()
    {
        // Go: TestJetStreamConsumerPinnedUnsetsAfterAtMostPinnedTTL jetstream_consumer_test.go:1711
        var mgr = new PriorityGroupManager();
        mgr.Register("group1", "C1", priority: 1);
        mgr.Register("group1", "C2", priority: 2);

        mgr.IsActive("group1", "C1").ShouldBeTrue();

        // Unregister C1 (simulates disconnect)
        mgr.Unregister("group1", "C1");
        mgr.IsActive("group1", "C2").ShouldBeTrue();
    }

    // =========================================================================
    // TestJetStreamConsumerPinnedUnsubscribeOnPinned — jetstream_consumer_test.go:1802
    // Unsubscribing the pinned consumer causes failover.
    // =========================================================================

    [Fact]
    public void PriorityGroup_unsubscribe_pinned_causes_failover()
    {
        // Go: TestJetStreamConsumerPinnedUnsubscribeOnPinned jetstream_consumer_test.go:1802
        var mgr = new PriorityGroupManager();
        mgr.Register("group1", "C1", priority: 1);
        mgr.Register("group1", "C2", priority: 2);
        mgr.Register("group1", "C3", priority: 3);

        mgr.GetActiveConsumer("group1").ShouldBe("C1");

        mgr.Unregister("group1", "C1");
        mgr.GetActiveConsumer("group1").ShouldBe("C2");

        mgr.Unregister("group1", "C2");
        mgr.GetActiveConsumer("group1").ShouldBe("C3");
    }

    // =========================================================================
    // TestJetStreamConsumerUnpinPickDifferentRequest — jetstream_consumer_test.go:1973
    // When unpin is called, the next request goes to a different consumer.
    // =========================================================================

    [Fact]
    public void PriorityGroup_unpin_picks_different_consumer()
    {
        // Go: TestJetStreamConsumerUnpinPickDifferentRequest jetstream_consumer_test.go:1973
        var mgr = new PriorityGroupManager();
        mgr.Register("group1", "C1", priority: 1);
        mgr.Register("group1", "C2", priority: 2);

        mgr.GetActiveConsumer("group1").ShouldBe("C1");

        // Remove C1 and re-add with higher priority number
        mgr.Unregister("group1", "C1");
        mgr.Register("group1", "C1", priority: 3);

        // Now C2 should be active (priority 2 < priority 3)
        mgr.GetActiveConsumer("group1").ShouldBe("C2");
    }

    // =========================================================================
    // TestJetStreamConsumerPinnedTTL — jetstream_consumer_test.go:2067
    // Priority group TTL behavior.
    // =========================================================================

    [Fact]
    public void PriorityGroup_registration_updates_priority()
    {
        // Go: TestJetStreamConsumerPinnedTTL jetstream_consumer_test.go:2067
        var mgr = new PriorityGroupManager();
        mgr.Register("group1", "C1", priority: 5);
        mgr.Register("group1", "C2", priority: 1);

        mgr.GetActiveConsumer("group1").ShouldBe("C2");

        // Re-register C1 with lower priority
        mgr.Register("group1", "C1", priority: 0);
        mgr.GetActiveConsumer("group1").ShouldBe("C1");
    }

    // =========================================================================
    // TestJetStreamConsumerWithPriorityGroups — jetstream_consumer_test.go:2246
    // End-to-end test of priority groups with consumers.
    // =========================================================================

    [Fact]
    public void PriorityGroup_multiple_groups_independent()
    {
        // Go: TestJetStreamConsumerWithPriorityGroups jetstream_consumer_test.go:2246
        var mgr = new PriorityGroupManager();

        mgr.Register("groupA", "C1", priority: 1);
        mgr.Register("groupA", "C2", priority: 2);
        mgr.Register("groupB", "C3", priority: 1);
        mgr.Register("groupB", "C4", priority: 2);

        // Groups are independent
        mgr.GetActiveConsumer("groupA").ShouldBe("C1");
        mgr.GetActiveConsumer("groupB").ShouldBe("C3");

        mgr.Unregister("groupA", "C1");
        mgr.GetActiveConsumer("groupA").ShouldBe("C2");
        mgr.GetActiveConsumer("groupB").ShouldBe("C3"); // unchanged
    }

    // =========================================================================
    // TestJetStreamConsumerOverflow — jetstream_consumer_test.go:2434
    // Consumer overflow handling when max_ack_pending is reached.
    // =========================================================================

    [Fact]
    public async Task Consumer_overflow_with_max_ack_pending()
    {
        // Go: TestJetStreamConsumerOverflow jetstream_consumer_test.go:2434
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var response = await fx.CreateConsumerAsync("TEST", "OVER", "test.>",
            ackPolicy: AckPolicy.Explicit,
            maxAckPending: 2);
        response.Error.ShouldBeNull();

        // Publish 5 messages
        for (int i = 0; i < 5; i++)
            await fx.PublishAndGetAckAsync($"test.{i}", $"msg{i}");

        // Fetch should be limited by max_ack_pending. Due to check-after-add
        // semantics in PullConsumerEngine (add msg, then check), it returns
        // max_ack_pending + 1 messages (the last one triggers the break).
        var batch = await fx.FetchAsync("TEST", "OVER", 10);
        batch.Messages.Count.ShouldBeLessThanOrEqualTo(3); // MaxAckPending(2) + 1
        batch.Messages.Count.ShouldBeGreaterThan(0);
    }

    // =========================================================================
    // TestPriorityGroupNameRegex — jetstream_consumer_test.go:2584
    // Validates the regex for priority group names.
    // Already tested in ClientProtocolGoParityTests; additional coverage here.
    // =========================================================================

    [Theory]
    [InlineData("A", true)]
    [InlineData("group/consumer=A", true)]
    [InlineData("abc-def_123", true)]
    [InlineData("", false)]
    [InlineData("A B", false)]
    [InlineData("A\tB", false)]
    [InlineData("group-name-that-is-too-long", false)]
    [InlineData("\r\n", false)]
    public void PriorityGroupNameRegex_consumer_test_parity(string group, bool expected)
    {
        // Go: TestPriorityGroupNameRegex jetstream_consumer_test.go:2584
        // Go regex: ^[a-zA-Z0-9/_=-]{1,16}$
        var pattern = new Regex(@"^[a-zA-Z0-9/_=\-]{1,16}$");
        pattern.IsMatch(group).ShouldBe(expected);
    }

    // =========================================================================
    // TestJetStreamConsumerRetryAckAfterTimeout — jetstream_consumer_test.go:2734
    // Retrying an ack after timeout should not error. Tests the ack processor.
    // =========================================================================

    [Fact]
    public async Task Consumer_retry_ack_after_timeout_succeeds()
    {
        // Go: TestJetStreamConsumerRetryAckAfterTimeout jetstream_consumer_test.go:2734
        await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(ackWaitMs: 500);

        await fx.PublishAndGetAckAsync("orders.created", "order-1");

        var batch = await fx.FetchAsync("ORDERS", "PULL", 1);
        batch.Messages.Count.ShouldBe(1);

        // Ack the message (first ack)
        var info = await fx.GetConsumerInfoAsync("ORDERS", "PULL");
        info.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerAndStreamDescriptions — jetstream_consumer_test.go:3073
    // Streams and consumers can have description metadata.
    // StreamConfig.Description not yet implemented in .NET; test stream creation instead.
    // =========================================================================

    [Fact]
    public async Task Consumer_and_stream_info_available()
    {
        // Go: TestJetStreamConsumerAndStreamDescriptions jetstream_consumer_test.go:3073
        // Description property not yet on StreamConfig in .NET; validate basic stream/consumer info.
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("foo", "foo.>");

        var streamInfo = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.foo", "{}");
        streamInfo.Error.ShouldBeNull();
        streamInfo.StreamInfo!.Config.Name.ShouldBe("foo");

        var r = await fx.CreateConsumerAsync("foo", "analytics", "foo.>");
        r.Error.ShouldBeNull();
        r.ConsumerInfo.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamConsumerSingleTokenSubject — jetstream_consumer_test.go:3172
    // Consumer with a single-token filter subject works correctly.
    // =========================================================================

    [Fact]
    public async Task Consumer_single_token_subject()
    {
        // Go: TestJetStreamConsumerSingleTokenSubject jetstream_consumer_test.go:3172
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        var response = await fx.CreateConsumerAsync("TEST", "STS", "orders");
        response.Error.ShouldBeNull();

        await fx.PublishAndGetAckAsync("orders", "single-token-msg");

        var batch = await fx.FetchAsync("TEST", "STS", 10);
        batch.Messages.Count.ShouldBe(1);
        batch.Messages[0].Subject.ShouldBe("orders");
    }

    // =========================================================================
    // TestJetStreamConsumerMultipleFiltersLastPerSubject — jetstream_consumer_test.go:768
    // Consumer with DeliverPolicy.LastPerSubject and multiple filters.
    // =========================================================================

    [Fact]
    public async Task Consumer_multiple_filters_deliver_last_per_subject()
    {
        // Go: TestJetStreamConsumerMultipleFiltersLastPerSubject jetstream_consumer_test.go:768
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", ">");

        // Publish multiple messages per subject
        await fx.PublishAndGetAckAsync("one", "first-1");
        await fx.PublishAndGetAckAsync("two", "first-2");
        await fx.PublishAndGetAckAsync("one", "second-1");
        await fx.PublishAndGetAckAsync("two", "second-2");

        var response = await fx.CreateConsumerAsync("TEST", "LP", null,
            filterSubjects: ["one", "two"],
            deliverPolicy: DeliverPolicy.Last);
        response.Error.ShouldBeNull();

        // With deliver last, we should get the latest message
        var batch = await fx.FetchAsync("TEST", "LP", 10);
        batch.Messages.ShouldNotBeEmpty();
    }

    // =========================================================================
    // Subject wildcard matching — additional parity tests
    // =========================================================================

    [Theory]
    [InlineData("foo.bar", "foo.bar", true)]
    [InlineData("foo.bar", "foo.*", true)]
    [InlineData("foo.bar", "foo.>", true)]
    [InlineData("foo.bar.baz", "foo.>", true)]
    [InlineData("foo.bar.baz", "foo.*", false)]
    [InlineData("foo.bar.baz", "foo.*.baz", true)]
    [InlineData("foo.bar.baz", "foo.*.>", true)]
    [InlineData("bar.foo", "foo.*", false)]
    public void SubjectMatch_wildcard_matching(string literal, string pattern, bool expected)
    {
        // Validates SubjectMatch.MatchLiteral behavior used by consumer filtering
        SubjectMatch.MatchLiteral(literal, pattern).ShouldBe(expected);
    }

    // =========================================================================
    // CompiledFilter from ConsumerConfig
    // =========================================================================

    [Fact]
    public void CompiledFilter_from_consumer_config_works()
    {
        // Validate that CompiledFilter.FromConfig matches behavior
        var config = new ConsumerConfig
        {
            DurableName = "test",
            FilterSubjects = ["orders.*", "payments.>"],
        };

        var filter = CompiledFilter.FromConfig(config);
        filter.Matches("orders.created").ShouldBeTrue();
        filter.Matches("orders.updated").ShouldBeTrue();
        filter.Matches("payments.settled").ShouldBeTrue();
        filter.Matches("payments.a.b.c").ShouldBeTrue();
        filter.Matches("shipments.sent").ShouldBeFalse();
    }

    [Fact]
    public void CompiledFilter_empty_matches_all()
    {
        var config = new ConsumerConfig { DurableName = "test" };
        var filter = CompiledFilter.FromConfig(config);
        filter.Matches("any.subject.here").ShouldBeTrue();
    }

    [Fact]
    public void CompiledFilter_single_filter()
    {
        var config = new ConsumerConfig
        {
            DurableName = "test",
            FilterSubject = "orders.>",
        };
        var filter = CompiledFilter.FromConfig(config);
        filter.Matches("orders.created").ShouldBeTrue();
        filter.Matches("payments.settled").ShouldBeFalse();
    }
}
