// Ported from golang/nats-server/server/jetstream_test.go
// Stream CRUD operations: create, update, delete, purge, info, validation

using NATS.Server.JetStream;
using NATS.Server.JetStream.Api;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Validation;
using NATS.Server.TestUtilities;

namespace NATS.Server.JetStream.Tests.JetStream;

public class JetStreamStreamCrudTests
{
    // Go: TestJetStreamAddStream server/jetstream_test.go:178
    [Fact]
    public async Task Create_stream_returns_config_and_empty_state()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.ORDERS", "{}");
        info.Error.ShouldBeNull();
        info.StreamInfo.ShouldNotBeNull();
        info.StreamInfo!.Config.Name.ShouldBe("ORDERS");
        info.StreamInfo.Config.Subjects.ShouldContain("orders.*");
        info.StreamInfo.State.Messages.ShouldBe(0UL);
    }

    // Go: TestJetStreamAddStreamDiscardNew server/jetstream_test.go:122
    // Verifies discard new policy with max_bytes rejects new messages when stream is full.
    [Fact]
    public async Task Create_stream_with_discard_new_policy()
    {
        // Each message size = subject + payload + 16 overhead.
        // Two small messages: ("dn.one"=6 + "1"=1 + 16=23) + ("dn.two"=6 + "2"=1 + 16=23) = 46 bytes.
        // MaxBytes=50 allows both small messages but rejects the large third one.
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "DN",
            Subjects = ["dn.>"],
            MaxBytes = 50,
            Discard = DiscardPolicy.New,
        });

        var ack1 = await fx.PublishAndGetAckAsync("dn.one", "1");
        ack1.ErrorCode.ShouldBeNull();
        var ack2 = await fx.PublishAndGetAckAsync("dn.two", "2");
        ack2.ErrorCode.ShouldBeNull();

        // Oversized publish should be rejected due to discard new + max_bytes
        var ack3 = await fx.PublishAndGetAckAsync("dn.three", "this-is-a-large-payload-that-exceeds-bytes");
        ack3.ErrorCode.ShouldNotBeNull();
    }

    // Go: TestJetStreamAddStreamMaxMsgSize server/jetstream_test.go:484
    [Fact]
    public async Task Create_stream_with_max_msg_size_rejects_oversized()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "SIZED",
            Subjects = ["sized.>"],
            MaxMsgSize = 10,
        });

        var small = await fx.PublishAndGetAckAsync("sized.ok", "tiny");
        small.ErrorCode.ShouldBeNull();

        var big = await fx.PublishAndGetAckAsync("sized.big", "this-is-definitely-larger-than-ten-bytes");
        big.ErrorCode.ShouldNotBeNull();
    }

    // Go: TestJetStreamAddStreamCanonicalNames server/jetstream_test.go:537
    [Fact]
    public async Task Create_stream_name_is_preserved_in_info()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MyStream", "my.>");

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

    // Go: TestJetStreamAddStreamSameConfigOK server/jetstream_test.go:701
    [Fact]
    public async Task Create_stream_with_same_config_is_idempotent()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");

        var second = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.ORDERS",
            """{"name":"ORDERS","subjects":["orders.*"]}""");
        second.Error.ShouldBeNull();
        second.StreamInfo.ShouldNotBeNull();
        second.StreamInfo!.Config.Name.ShouldBe("ORDERS");
    }

    // Go: TestJetStreamUpdateStream server/jetstream_test.go:6409
    [Fact]
    public async Task Update_stream_changes_subjects_and_limits()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
        _ = await fx.PublishAndGetAckAsync("orders.x", "1");

        var update = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.ORDERS",
            """{"name":"ORDERS","subjects":["orders.v2.*"],"max_msgs":50}""");
        update.Error.ShouldBeNull();
        update.StreamInfo!.Config.Subjects.ShouldContain("orders.v2.*");
        update.StreamInfo.Config.MaxMsgs.ShouldBe(50);
    }

    // Go: TestJetStreamStreamPurge server/jetstream_test.go:4182
    [Fact]
    public async Task Purge_stream_removes_all_messages()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("P", "p.*");
        for (var i = 0; i < 5; i++)
            _ = await fx.PublishAndGetAckAsync("p.msg", $"payload-{i}");

        var before = await fx.GetStreamStateAsync("P");
        before.Messages.ShouldBe(5UL);

        var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.P", "{}");
        purge.Success.ShouldBeTrue();

        var after = await fx.GetStreamStateAsync("P");
        after.Messages.ShouldBe(0UL);
    }

    // Go: TestJetStreamDeleteMsg server/jetstream_test.go:6464
    [Fact]
    public async Task Delete_individual_message_by_sequence()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DEL", "del.>");
        var ack1 = await fx.PublishAndGetAckAsync("del.a", "1");
        _ = await fx.PublishAndGetAckAsync("del.b", "2");

        var del = await fx.RequestLocalAsync(
            "$JS.API.STREAM.MSG.DELETE.DEL",
            $$"""{ "seq": {{ack1.Seq}} }""");
        del.Success.ShouldBeTrue();

        var state = await fx.GetStreamStateAsync("DEL");
        state.Messages.ShouldBe(1UL);
    }

    // Go: TestJetStreamAddStream — delete removes stream
    [Fact]
    public async Task Delete_stream_makes_it_inaccessible()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GONE", "gone.>");
        _ = await fx.PublishAndGetAckAsync("gone.x", "data");

        var del = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.GONE", "{}");
        del.Success.ShouldBeTrue();

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.GONE", "{}");
        info.Error.ShouldNotBeNull();
    }

    // Go: TestJetStreamStreamPurge — publish after purge works
    [Fact]
    public async Task Publish_after_purge_adds_new_message()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PP", "pp.>");
        _ = await fx.PublishAndGetAckAsync("pp.x", "before");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PP", "{}");

        var ack = await fx.PublishAndGetAckAsync("pp.x", "after");
        ack.ErrorCode.ShouldBeNull();

        var state = await fx.GetStreamStateAsync("PP");
        state.Messages.ShouldBe(1UL);
    }

    // Go: TestJetStreamBasicNilConfig server/jetstream_test.go:56
    [Fact]
    public void Stream_config_requires_name()
    {
        var sm = new StreamManager();
        var resp = sm.CreateOrUpdate(new StreamConfig { Name = "" });
        resp.Error.ShouldNotBeNull();
        resp.Error!.Code.ShouldBe(400);
    }

    // Go: TestJetStreamAddStreamBadSubjects server/jetstream_test.go:587
    [Fact]
    public void Validation_rejects_empty_name_and_subjects()
    {
        var config = new StreamConfig { Name = "", Subjects = [] };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeFalse();
    }

    // Go: TestJetStreamAddStreamBadSubjects — valid name required
    [Fact]
    public void Validation_accepts_valid_stream_config()
    {
        var config = new StreamConfig { Name = "OK", Subjects = ["ok.>"] };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeTrue();
    }

    // Go: TestJetStreamMaxConsumers server/jetstream_test.go:619
    [Fact]
    public void Validation_workqueue_requires_max_consumers()
    {
        var config = new StreamConfig
        {
            Name = "WQ",
            Subjects = ["wq.>"],
            Retention = RetentionPolicy.WorkQueue,
            MaxConsumers = 0,
        };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeFalse();
    }

    // Go: TestJetStreamInvalidConfigValues server/jetstream_test.go
    [Fact]
    public void Validation_rejects_negative_max_msg_size()
    {
        var config = new StreamConfig
        {
            Name = "NEG",
            Subjects = ["neg.>"],
            MaxMsgSize = -1,
        };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeFalse();
    }

    // Go: TestJetStreamInvalidConfigValues
    [Fact]
    public void Validation_rejects_negative_max_msgs_per()
    {
        var config = new StreamConfig
        {
            Name = "NEG2",
            Subjects = ["neg2.>"],
            MaxMsgsPer = -1,
        };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeFalse();
    }

    // Go: TestJetStreamInvalidConfigValues
    [Fact]
    public void Validation_rejects_negative_max_age_ms()
    {
        var config = new StreamConfig
        {
            Name = "NEG3",
            Subjects = ["neg3.>"],
            MaxAgeMs = -1,
        };
        var result = JetStreamConfigValidator.Validate(config);
        result.IsValid.ShouldBeFalse();
    }

    // Go: TestJetStreamStreamPurge — sealed stream cannot be purged
    [Fact]
    public async Task Sealed_stream_rejects_purge()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "SEAL",
            Subjects = ["seal.>"],
            Sealed = true,
        });

        var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.SEAL", "{}");
        purge.Success.ShouldBeFalse();
    }

    // Go: TestJetStreamDeleteMsg — deny_delete prevents removal
    [Fact]
    public async Task Deny_delete_prevents_message_removal()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "NODELETE",
            Subjects = ["nodelete.>"],
            DenyDelete = true,
        });

        var ack = await fx.PublishAndGetAckAsync("nodelete.x", "data");
        ack.ErrorCode.ShouldBeNull();

        var del = await fx.RequestLocalAsync(
            "$JS.API.STREAM.MSG.DELETE.NODELETE",
            $$"""{ "seq": {{ack.Seq}} }""");
        del.Success.ShouldBeFalse();
    }

    // Go: TestJetStreamDeleteMsg — deny_purge prevents purge
    [Fact]
    public async Task Deny_purge_prevents_stream_purge()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "NOPURGE",
            Subjects = ["nopurge.>"],
            DenyPurge = true,
        });

        _ = await fx.PublishAndGetAckAsync("nopurge.x", "data");

        var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.NOPURGE", "{}");
        purge.Success.ShouldBeFalse();
    }

    // Go: TestJetStreamStreamStorageTrackingAndLimits server/jetstream_test.go:4931
    [Fact]
    public async Task Stream_with_max_msgs_limit_enforces_count()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LIMITED", "limited.>", maxMsgs: 3);

        for (var i = 0; i < 5; i++)
            _ = await fx.PublishAndGetAckAsync("limited.x", $"msg-{i}");

        var state = await fx.GetStreamStateAsync("LIMITED");
        state.Messages.ShouldBeLessThanOrEqualTo(3UL);
    }

    // Go: TestJetStreamMaxBytesIgnored server/jetstream_test.go:15778
    // Go test uses 10MB limit with 1MB messages, expects 9 messages retained.
    // We use smaller sizes but same principle: MaxBytes caps stored bytes,
    // accounting for per-message overhead (subject + 16 bytes).
    [Fact]
    public async Task Stream_with_max_bytes_discard_old_evicts_oldest()
    {
        // Each stored message = subject("bytes.x"=7) + payload(20) + overhead(16) = 43 bytes.
        // MaxBytes=200 holds ~4 messages (4*43=172). Publishing 20 should evict old ones.
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "BYTES",
            Subjects = ["bytes.>"],
            MaxBytes = 200,
            Discard = DiscardPolicy.Old,
        });

        for (var i = 0; i < 20; i++)
            _ = await fx.PublishAndGetAckAsync("bytes.x", $"payload-{i:D10}");

        var state = await fx.GetStreamStateAsync("BYTES");
        ((long)state.Bytes).ShouldBeLessThanOrEqualTo(200L);
        state.Messages.ShouldBeLessThan(20UL);
    }

    // Go: TestJetStreamMaxMsgsPerSubject server/jetstream_test.go
    [Fact]
    public async Task Max_msgs_per_subject_enforces_limit()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "MPS",
            Subjects = ["mps.>"],
            MaxMsgsPer = 2,
        });

        _ = await fx.PublishAndGetAckAsync("mps.a", "1");
        _ = await fx.PublishAndGetAckAsync("mps.a", "2");
        _ = await fx.PublishAndGetAckAsync("mps.a", "3");
        _ = await fx.PublishAndGetAckAsync("mps.b", "4");

        var state = await fx.GetStreamStateAsync("MPS");
        // mps.a should have 2 kept, mps.b has 1 = 3 total
        state.Messages.ShouldBeLessThanOrEqualTo(3UL);
    }

    // Go: TestJetStreamStreamFileTrackingAndLimits server/jetstream_test.go:4982
    [Fact]
    public async Task Stream_with_file_storage_type()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "FSTORE",
            Subjects = ["fstore.>"],
            Storage = StorageType.File,
        });

        var ack = await fx.PublishAndGetAckAsync("fstore.x", "data");
        ack.ErrorCode.ShouldBeNull();

        var backendType = await fx.GetStreamBackendTypeAsync("FSTORE");
        backendType.ShouldBe("file");
    }

    // Go: TestJetStreamStreamFileTrackingAndLimits — memory store
    [Fact]
    public async Task Stream_with_memory_storage_type()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "MSTORE",
            Subjects = ["mstore.>"],
            Storage = StorageType.Memory,
        });

        var ack = await fx.PublishAndGetAckAsync("mstore.x", "data");
        ack.ErrorCode.ShouldBeNull();

        var backendType = await fx.GetStreamBackendTypeAsync("MSTORE");
        backendType.ShouldBe("memory");
    }

    // Go: TestJetStreamStreamLimitUpdate server/jetstream_test.go:4905
    [Fact]
    public async Task Update_stream_max_msgs_trims_existing_messages()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("UPD", "upd.>");

        for (var i = 0; i < 10; i++)
            _ = await fx.PublishAndGetAckAsync("upd.x", $"msg-{i}");

        var before = await fx.GetStreamStateAsync("UPD");
        before.Messages.ShouldBe(10UL);

        // Update to max_msgs=3
        var update = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.UPD",
            """{"name":"UPD","subjects":["upd.>"],"max_msgs":3}""");
        update.Error.ShouldBeNull();

        // Publish one more to trigger enforcement
        _ = await fx.PublishAndGetAckAsync("upd.x", "trigger");

        var after = await fx.GetStreamStateAsync("UPD");
        after.Messages.ShouldBeLessThanOrEqualTo(3UL);
    }

    // Go: TestJetStreamAllowDirectAfterUpdate server/jetstream_test.go
    [Fact]
    public async Task Allow_direct_can_be_set_via_update()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "DIR",
            Subjects = ["dir.>"],
            AllowDirect = false,
        });

        var update = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.DIR",
            """{"name":"DIR","subjects":["dir.>"],"allow_direct":true}""");
        update.Error.ShouldBeNull();
        update.StreamInfo!.Config.AllowDirect.ShouldBeTrue();
    }

    // Go: TestJetStreamStreamConfigClone server/jetstream_test.go
    [Fact]
    public async Task Stream_config_is_independent_after_creation()
    {
        var config = new StreamConfig
        {
            Name = "CLONE",
            Subjects = ["clone.>"],
            MaxMsgs = 100,
        };
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(config);

        // Mutate the original config
        config.MaxMsgs = 999;

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.CLONE", "{}");
        info.StreamInfo!.Config.MaxMsgs.ShouldBe(100);
    }

    // Go: TestJetStreamStreamPurgeWithConsumer server/jetstream_test.go:4215
    [Fact]
    public async Task Purge_with_active_consumer_resets_delivery()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PC", "pc.>");
        _ = await fx.CreateConsumerAsync("PC", "C1", "pc.>");

        for (var i = 0; i < 5; i++)
            _ = await fx.PublishAndGetAckAsync("pc.x", $"msg-{i}");

        var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PC", "{}");
        purge.Success.ShouldBeTrue();

        var state = await fx.GetStreamStateAsync("PC");
        state.Messages.ShouldBe(0UL);
    }

    // Go: TestJetStreamGetLastMsgBySubject server/jetstream_test.go
    [Fact]
    public async Task Get_message_by_sequence_returns_correct_data()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GM", "gm.>");
        var ack = await fx.PublishAndGetAckAsync("gm.first", "hello");
        _ = await fx.PublishAndGetAckAsync("gm.second", "world");

        var msg = await fx.RequestLocalAsync(
            "$JS.API.STREAM.MSG.GET.GM",
            $$"""{ "seq": {{ack.Seq}} }""");
        msg.StreamMessage.ShouldNotBeNull();
        msg.StreamMessage!.Payload.ShouldBe("hello");
        msg.StreamMessage.Subject.ShouldBe("gm.first");
    }

    // Go: TestJetStreamStateTimestamps server/jetstream_test.go:758
    [Fact]
    public async Task Stream_state_tracks_first_and_last_sequence()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TS", "ts.>");
        _ = await fx.PublishAndGetAckAsync("ts.a", "1");
        _ = await fx.PublishAndGetAckAsync("ts.b", "2");
        _ = await fx.PublishAndGetAckAsync("ts.c", "3");

        var state = await fx.GetStreamStateAsync("TS");
        state.Messages.ShouldBe(3UL);
        state.FirstSeq.ShouldBe(1UL);
        state.LastSeq.ShouldBe(3UL);
    }

    // Go: TestJetStreamAddStreamDiscardNew — discard new + max bytes
    [Fact]
    public async Task Discard_new_with_max_bytes_rejects_when_full()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "DNB",
            Subjects = ["dnb.>"],
            MaxBytes = 50,
            Discard = DiscardPolicy.New,
        });

        // Fill up
        for (var i = 0; i < 10; i++)
        {
            _ = await fx.PublishAndGetAckAsync("dnb.x", $"msg-{i:D20}");
        }

        // Eventually one should be rejected
        var state = await fx.GetStreamStateAsync("DNB");
        ((long)state.Bytes).ShouldBeLessThanOrEqualTo(50L + 50);
    }

    // Go: TestJetStreamStreamRetentionUpdatesConsumers server/jetstream_test.go
    [Fact]
    public async Task Stream_info_after_multiple_publishes()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("INF", "inf.>");

        for (var i = 0; i < 10; i++)
            _ = await fx.PublishAndGetAckAsync("inf.x", $"data-{i}");

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.INF", "{}");
        info.Error.ShouldBeNull();
        info.StreamInfo!.State.Messages.ShouldBe(10UL);
        info.StreamInfo.State.FirstSeq.ShouldBe(1UL);
        info.StreamInfo.State.LastSeq.ShouldBe(10UL);
    }

    // Go: TestJetStreamDeleteMsg — sequence 0 returns error
    [Fact]
    public async Task Delete_message_with_zero_sequence_returns_error()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DZ", "dz.>");
        _ = await fx.PublishAndGetAckAsync("dz.x", "data");

        var del = await fx.RequestLocalAsync("$JS.API.STREAM.MSG.DELETE.DZ", """{"seq":0}""");
        del.Error.ShouldNotBeNull();
    }

    // Go: TestJetStreamDeleteMsg — non-existent stream
    [Fact]
    public async Task Delete_message_from_non_existent_stream()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EXISTS", "exists.>");

        var del = await fx.RequestLocalAsync("$JS.API.STREAM.MSG.DELETE.NOTEXIST", """{"seq":1}""");
        del.Success.ShouldBeFalse();
    }

    // Go: TestJetStreamRestoreBadStream server/jetstream_test.go
    [Fact]
    public async Task Info_for_non_existent_stream_returns_error()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("X", "x.>");

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.DOESNOTEXIST", "{}");
        info.Error.ShouldNotBeNull();
    }

    // Go: TestJetStreamStreamPurge — multiple purges are idempotent
    [Fact]
    public async Task Multiple_purges_are_idempotent()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MP", "mp.>");
        for (var i = 0; i < 3; i++)
            _ = await fx.PublishAndGetAckAsync("mp.x", $"msg-{i}");

        _ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.MP", "{}");
        var second = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.MP", "{}");
        second.Success.ShouldBeTrue();

        var state = await fx.GetStreamStateAsync("MP");
        state.Messages.ShouldBe(0UL);
    }

    // Go: TestJetStreamAddStream — retention policy Limits
    [Fact]
    public async Task Create_stream_with_limits_retention()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "LIM",
            Subjects = ["lim.>"],
            Retention = RetentionPolicy.Limits,
        });

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.LIM", "{}");
        info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.Limits);
    }

    // Go: TestJetStreamInterestRetentionStream server/jetstream_test.go:4336
    [Fact]
    public async Task Create_stream_with_interest_retention()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "INT",
            Subjects = ["int.>"],
            Retention = RetentionPolicy.Interest,
        });

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.INT", "{}");
        info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.Interest);
    }

    // Go: TestJetStreamBasicWorkQueue server/jetstream_test.go:937
    [Fact]
    public async Task Create_stream_with_workqueue_retention()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "WQ",
            Subjects = ["wq.>"],
            Retention = RetentionPolicy.WorkQueue,
            MaxConsumers = 1,
        });

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.WQ", "{}");
        info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.WorkQueue);
    }

    // Go: TestJetStreamSnapshotsAPI server/jetstream_test.go:3328
    [Fact]
    public async Task Snapshot_and_restore_roundtrip()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SNAP", "snap.>");
        _ = await fx.PublishAndGetAckAsync("snap.a", "data1");
        _ = await fx.PublishAndGetAckAsync("snap.b", "data2");

        var snap = await fx.RequestLocalAsync("$JS.API.STREAM.SNAPSHOT.SNAP", "{}");
        snap.Error.ShouldBeNull();
        snap.Snapshot.ShouldNotBeNull();
        snap.Snapshot!.Payload.ShouldNotBeNullOrWhiteSpace();

        // Restore into the same stream
        var restore = await fx.RequestLocalAsync(
            "$JS.API.STREAM.RESTORE.SNAP",
            snap.Snapshot.Payload);
        restore.Success.ShouldBeTrue();
    }

    // Go: TestJetStreamAddStreamOverlapWithJSAPISubjects server/jetstream_test.go:666
    [Fact]
    public async Task Create_multiple_streams_with_non_overlapping_subjects()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("S1", "s1.>");
        var s2 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
        s2.Error.ShouldBeNull();

        var ack1 = await fx.PublishAndGetAckAsync("s1.x", "data1");
        ack1.Stream.ShouldBe("S1");
        var ack2 = await fx.PublishAndGetAckAsync("s2.x", "data2");
        ack2.Stream.ShouldBe("S2");
    }

    // Go: TestJetStreamStreamPurge — verify bytes reset after purge
    [Fact]
    public async Task Purge_resets_byte_count()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PB", "pb.>");
        for (var i = 0; i < 5; i++)
            _ = await fx.PublishAndGetAckAsync("pb.x", "some-data");

        var before = await fx.GetStreamStateAsync("PB");
        before.Bytes.ShouldBeGreaterThan(0UL);

        _ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PB", "{}");

        var after = await fx.GetStreamStateAsync("PB");
        after.Bytes.ShouldBe(0UL);
    }

    // Go: TestJetStreamDefaultMaxMsgsPer server/jetstream_test.go
    [Fact]
    public async Task Stream_defaults_replicas_to_one()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DEF", "def.>");

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.DEF", "{}");
        info.StreamInfo!.Config.Replicas.ShouldBe(1);
    }

    // Go: TestJetStreamSuppressAllowDirect server/jetstream_test.go
    [Fact]
    public async Task Allow_direct_defaults_to_false()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AD", "ad.>");

        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.AD", "{}");
        info.StreamInfo!.Config.AllowDirect.ShouldBeFalse();
    }
}
