// Ported from golang/nats-server/server/jetstream_versioning_test.go
// and selected tests from golang/nats-server/server/jetstream_test.go
//
// Tests verify JetStream API versioning metadata logic:
//   - getRequiredApiLevel / supportsRequiredApiLevel helpers
//   - setStaticStreamMetadata / setDynamicStreamMetadata
//   - copyStreamMetadata and removal of dynamic fields
//   - setStaticConsumerMetadata / setDynamicConsumerMetadata / setDynamicConsumerInfoMetadata
//   - copyConsumerMetadata and removal of dynamic fields
//   - End-to-end metadata mutations on stream and consumer CRUD
//   - Direct-get batch with MaxBytes, UpToTime, MaxAllowed, Paging, SubjectDeleteMarker, DeadlockSafety
//   - Upgrade stream/consumer versioning idempotency
//   - Offline stream/consumer after simulated downgrade

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

namespace NATS.Server.JetStream.Tests.JetStream;

/// <summary>
/// Go parity tests for JetStream versioning metadata logic and direct-get batch behavior.
/// </summary>
public class JsVersioningTests
{
    // =========================================================================
    // Metadata key constants (mirrors jetstream_versioning.go constants)
    // =========================================================================

    private const string RequiredLevelKey = "_nats.req.level";
    private const string ServerVersionKey = "_nats.ver";
    private const string ServerLevelKey = "_nats.level";

    // =========================================================================
    // TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:34)
    // =========================================================================

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:34)
    [Fact]
    public void GetRequiredApiLevel_returns_empty_for_null_metadata()
    {
        JsVersioning.GetRequiredApiLevel(null).ShouldBe(string.Empty);
    }

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:35)
    [Fact]
    public void GetRequiredApiLevel_returns_empty_for_empty_metadata()
    {
        JsVersioning.GetRequiredApiLevel([]).ShouldBe(string.Empty);
    }

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:36)
    [Fact]
    public void GetRequiredApiLevel_returns_value_when_key_present()
    {
        JsVersioning.GetRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = "1" }).ShouldBe("1");
        JsVersioning.GetRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = "text" }).ShouldBe("text");
    }

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:40)
    [Fact]
    public void SupportsRequiredApiLevel_returns_true_for_null_or_empty_metadata()
    {
        JsVersioning.SupportsRequiredApiLevel(null).ShouldBeTrue();
        JsVersioning.SupportsRequiredApiLevel([]).ShouldBeTrue();
    }

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:42)
    [Fact]
    public void SupportsRequiredApiLevel_returns_true_for_numeric_level_within_current()
    {
        JsVersioning.SupportsRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = "1" }).ShouldBeTrue();
        JsVersioning.SupportsRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = JsVersioning.JsApiLevel.ToString() }).ShouldBeTrue();
    }

    // Go: TestGetAndSupportsRequiredApiLevel (jetstream_versioning_test.go:44)
    [Fact]
    public void SupportsRequiredApiLevel_returns_false_for_non_numeric_level()
    {
        JsVersioning.SupportsRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = "text" }).ShouldBeFalse();
    }

    // Go: TestGetAndSupportsRequiredApiLevel — level above current not supported
    [Fact]
    public void SupportsRequiredApiLevel_returns_false_when_level_exceeds_current()
    {
        var tooBig = (JsVersioning.JsApiLevel + 1).ToString();
        JsVersioning.SupportsRequiredApiLevel(new Dictionary<string, string> { [RequiredLevelKey] = tooBig }).ShouldBeFalse();
    }

    // =========================================================================
    // TestJetStreamSetStaticStreamMetadata (jetstream_versioning_test.go:59)
    // =========================================================================

    // Go: TestJetStreamSetStaticStreamMetadata — empty config gets level 0
    [Fact]
    public void SetStaticStreamMetadata_empty_config_sets_level_zero()
    {
        var cfg = new StreamConfig();
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
        ValidateLevelIsWithinCurrentApiLevel(cfg.Metadata[RequiredLevelKey]);
    }

    // Go: TestJetStreamSetStaticStreamMetadata — overwrite-user-provided
    [Fact]
    public void SetStaticStreamMetadata_overwrites_user_provided_level()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
    }

    // Go: TestJetStreamSetStaticStreamMetadata — AllowMsgTTL requires level 1
    [Fact]
    public void SetStaticStreamMetadata_AllowMsgTtl_sets_level_one()
    {
        var cfg = new StreamConfig { AllowMsgTtl = true };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("1");
        ValidateLevelIsWithinCurrentApiLevel(cfg.Metadata[RequiredLevelKey]);
    }

    // Go: TestJetStreamSetStaticStreamMetadata — SubjectDeleteMarkerTTL requires level 1
    [Fact]
    public void SetStaticStreamMetadata_SubjectDeleteMarkerTtl_sets_level_one()
    {
        var cfg = new StreamConfig { SubjectDeleteMarkerTtlMs = 1000 };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("1");
    }

    // Go: TestJetStreamSetStaticStreamMetadata — AllowMsgCounter requires level 2
    [Fact]
    public void SetStaticStreamMetadata_AllowMsgCounter_sets_level_two()
    {
        var cfg = new StreamConfig { AllowMsgCounter = true };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("2");
        ValidateLevelIsWithinCurrentApiLevel(cfg.Metadata[RequiredLevelKey]);
    }

    // Go: TestJetStreamSetStaticStreamMetadata — AllowAtomicPublish requires level 2
    [Fact]
    public void SetStaticStreamMetadata_AllowAtomicPublish_sets_level_two()
    {
        var cfg = new StreamConfig { AllowAtomicPublish = true };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("2");
    }

    // Go: TestJetStreamSetStaticStreamMetadata — AllowMsgSchedules requires level 2
    [Fact]
    public void SetStaticStreamMetadata_AllowMsgSchedules_sets_level_two()
    {
        var cfg = new StreamConfig { AllowMsgSchedules = true };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("2");
    }

    // Go: TestJetStreamSetStaticStreamMetadata — AsyncPersistMode requires level 2
    [Fact]
    public void SetStaticStreamMetadata_AsyncPersistMode_sets_level_two()
    {
        var cfg = new StreamConfig { PersistMode = PersistMode.Async };
        JsVersioning.SetStaticStreamMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("2");
    }

    // =========================================================================
    // TestJetStreamSetStaticStreamMetadataRemoveDynamicFields (jetstream_versioning_test.go:124)
    // =========================================================================

    // Go: TestJetStreamSetStaticStreamMetadataRemoveDynamicFields (jetstream_versioning_test.go:124)
    [Fact]
    public void SetStaticStreamMetadata_removes_dynamic_fields()
    {
        var cfg = new StreamConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        JsVersioning.SetStaticStreamMetadata(cfg);

        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();
        cfg.Metadata[RequiredLevelKey].ShouldBe("0");
    }

    // =========================================================================
    // TestJetStreamSetDynamicStreamMetadata (jetstream_versioning_test.go:137)
    // =========================================================================

    // Go: TestJetStreamSetDynamicStreamMetadata (jetstream_versioning_test.go:137)
    [Fact]
    public void SetDynamicStreamMetadata_only_modifies_copy_not_original()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "0" } };
        var newCfg = JsVersioning.SetDynamicStreamMetadata(cfg);

        // Original must not contain dynamic fields
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();

        // Copy must contain dynamic fields
        newCfg.Metadata.ShouldNotBeNull();
        newCfg.Metadata![RequiredLevelKey].ShouldBe("0");
        newCfg.Metadata[ServerVersionKey].ShouldBe(JsVersioning.Version);
        newCfg.Metadata[ServerLevelKey].ShouldBe(JsVersioning.JsApiLevel.ToString());
    }

    // =========================================================================
    // TestJetStreamCopyStreamMetadata (jetstream_versioning_test.go:149)
    // =========================================================================

    // Go: TestJetStreamCopyStreamMetadata — no previous: key absent
    [Fact]
    public void CopyStreamMetadata_no_previous_removes_level_key()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        JsVersioning.CopyStreamMetadata(cfg, null);
        cfg.Metadata.ShouldBeNull();
    }

    // Go: TestJetStreamCopyStreamMetadata — nil-previous-metadata: key absent
    [Fact]
    public void CopyStreamMetadata_nil_previous_metadata_removes_level_key()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new StreamConfig { Metadata = null };
        JsVersioning.CopyStreamMetadata(cfg, prev);
        // Key should be absent
        cfg.Metadata?.ContainsKey(RequiredLevelKey).ShouldNotBe(true);
    }

    // Go: TestJetStreamCopyStreamMetadata — nil-current-metadata: skip
    [Fact]
    public void CopyStreamMetadata_nil_current_metadata_is_unchanged()
    {
        var cfg = new StreamConfig { Metadata = null };
        var prev = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.CopyStreamMetadata(cfg, prev);
        // Since current was null, CopyStreamMetadata should set the key from prev
        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata![RequiredLevelKey].ShouldBe("previous-level");
    }

    // Go: TestJetStreamCopyStreamMetadata — copy-previous: key copied
    [Fact]
    public void CopyStreamMetadata_copies_level_from_previous()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.CopyStreamMetadata(cfg, prev);
        cfg.Metadata![RequiredLevelKey].ShouldBe("previous-level");
    }

    // Go: TestJetStreamCopyStreamMetadata — delete-missing-fields: key absent when prev has empty metadata
    [Fact]
    public void CopyStreamMetadata_deletes_key_when_prev_metadata_is_empty()
    {
        var cfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new StreamConfig { Metadata = [] };
        JsVersioning.CopyStreamMetadata(cfg, prev);
        cfg.Metadata?.ContainsKey(RequiredLevelKey).ShouldNotBe(true);
    }

    // =========================================================================
    // TestJetStreamCopyStreamMetadataRemoveDynamicFields (jetstream_versioning_test.go:201)
    // =========================================================================

    // Go: TestJetStreamCopyStreamMetadataRemoveDynamicFields (jetstream_versioning_test.go:201)
    [Fact]
    public void CopyStreamMetadata_removes_dynamic_fields_when_prev_is_null()
    {
        var cfg = new StreamConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        JsVersioning.CopyStreamMetadata(cfg, null);
        cfg.Metadata.ShouldBeNull();
    }

    // Go: TestJetStreamCopyStreamMetadataRemoveDynamicFields (jetstream_versioning_test.go:213)
    [Fact]
    public void CopyStreamMetadata_with_prev_removes_dynamic_but_keeps_static()
    {
        var cfg = new StreamConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        var prevCfg = new StreamConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "0" } };
        JsVersioning.CopyStreamMetadata(cfg, prevCfg);

        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();
        cfg.Metadata[RequiredLevelKey].ShouldBe("0");
    }

    // =========================================================================
    // TestJetStreamSetStaticConsumerMetadata (jetstream_versioning_test.go:219)
    // =========================================================================

    // Go: TestJetStreamSetStaticConsumerMetadata — empty config gets level 0
    [Fact]
    public void SetStaticConsumerMetadata_empty_config_sets_level_zero()
    {
        var cfg = new ConsumerConfig();
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
        ValidateLevelIsWithinCurrentApiLevel(cfg.Metadata[RequiredLevelKey]);
    }

    // Go: TestJetStreamSetStaticConsumerMetadata — overwrite user-provided
    [Fact]
    public void SetStaticConsumerMetadata_overwrites_user_provided_level()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
    }

    // Go: TestJetStreamSetStaticConsumerMetadata — PauseUntil/zero remains level 0
    [Fact]
    public void SetStaticConsumerMetadata_PauseUntil_zero_stays_level_zero()
    {
        var cfg = new ConsumerConfig { PauseUntil = default(DateTime) };
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
    }

    // Go: TestJetStreamSetStaticConsumerMetadata — PauseUntil non-zero requires level 1
    [Fact]
    public void SetStaticConsumerMetadata_PauseUntil_nonzero_sets_level_one()
    {
        // Use Unix epoch (time.Unix(0,0) in Go) — this is non-zero in Go's terms but is DateTime.UnixEpoch in .NET
        var pauseUntil = DateTime.UnixEpoch;
        var cfg = new ConsumerConfig { PauseUntil = pauseUntil };
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("1");
        ValidateLevelIsWithinCurrentApiLevel(cfg.Metadata[RequiredLevelKey]);
    }

    // Go: TestJetStreamSetStaticConsumerMetadata — Pinned client priority requires level 1
    [Fact]
    public void SetStaticConsumerMetadata_PinnedClient_sets_level_one()
    {
        var cfg = new ConsumerConfig
        {
            PriorityPolicy = PriorityPolicy.PinnedClient,
            PriorityGroups = ["a"],
        };
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata![RequiredLevelKey].ShouldBe("1");
    }

    // =========================================================================
    // TestJetStreamSetStaticConsumerMetadataRemoveDynamicFields (jetstream_versioning_test.go:266)
    // =========================================================================

    // Go: TestJetStreamSetStaticConsumerMetadataRemoveDynamicFields (jetstream_versioning_test.go:266)
    [Fact]
    public void SetStaticConsumerMetadata_removes_dynamic_fields()
    {
        var cfg = new ConsumerConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        JsVersioning.SetStaticConsumerMetadata(cfg);
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();
        cfg.Metadata[RequiredLevelKey].ShouldBe("0");
    }

    // =========================================================================
    // TestJetStreamSetDynamicConsumerMetadata (jetstream_versioning_test.go:279)
    // =========================================================================

    // Go: TestJetStreamSetDynamicConsumerMetadata (jetstream_versioning_test.go:279)
    [Fact]
    public void SetDynamicConsumerMetadata_only_modifies_copy_not_original()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "0" } };
        var newCfg = JsVersioning.SetDynamicConsumerMetadata(cfg);

        // Original must not contain dynamic fields
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();

        // Copy must contain dynamic fields
        newCfg.Metadata![RequiredLevelKey].ShouldBe("0");
        newCfg.Metadata[ServerVersionKey].ShouldBe(JsVersioning.Version);
        newCfg.Metadata[ServerLevelKey].ShouldBe(JsVersioning.JsApiLevel.ToString());
    }

    // =========================================================================
    // TestJetStreamSetDynamicConsumerInfoMetadata (jetstream_versioning_test.go:291)
    // =========================================================================

    // Go: TestJetStreamSetDynamicConsumerInfoMetadata (jetstream_versioning_test.go:291)
    // Demonstrates the pattern of adding dynamic metadata to a consumer info response.
    [Fact]
    public void SetDynamicConsumerMetadata_produces_new_object_different_from_original()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "0" } };
        var newCfg = JsVersioning.SetDynamicConsumerMetadata(cfg);

        // Configs should not be reference-equal
        ReferenceEquals(cfg, newCfg).ShouldBeFalse();

        // Original metadata should remain unchanged
        cfg.Metadata![RequiredLevelKey].ShouldBe("0");
        cfg.Metadata.ContainsKey(ServerVersionKey).ShouldBeFalse();

        // New config must have dynamic fields
        newCfg.Metadata![ServerVersionKey].ShouldBe(JsVersioning.Version);
        newCfg.Metadata[ServerLevelKey].ShouldBe(JsVersioning.JsApiLevel.ToString());
    }

    // =========================================================================
    // TestJetStreamCopyConsumerMetadata (jetstream_versioning_test.go:306)
    // =========================================================================

    // Go: TestJetStreamCopyConsumerMetadata — no previous: key absent
    [Fact]
    public void CopyConsumerMetadata_no_previous_removes_level_key()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        JsVersioning.CopyConsumerMetadata(cfg, null);
        cfg.Metadata.ShouldBeNull();
    }

    // Go: TestJetStreamCopyConsumerMetadata — nil-previous-metadata: key absent
    [Fact]
    public void CopyConsumerMetadata_nil_previous_metadata_removes_level_key()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new ConsumerConfig { Metadata = null };
        JsVersioning.CopyConsumerMetadata(cfg, prev);
        cfg.Metadata?.ContainsKey(RequiredLevelKey).ShouldNotBe(true);
    }

    // Go: TestJetStreamCopyConsumerMetadata — nil-current-metadata: copy from prev
    [Fact]
    public void CopyConsumerMetadata_nil_current_copies_from_previous()
    {
        var cfg = new ConsumerConfig { Metadata = null };
        var prev = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.CopyConsumerMetadata(cfg, prev);
        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata![RequiredLevelKey].ShouldBe("previous-level");
    }

    // Go: TestJetStreamCopyConsumerMetadata — copy-previous: key copied
    [Fact]
    public void CopyConsumerMetadata_copies_level_from_previous()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "previous-level" } };
        JsVersioning.CopyConsumerMetadata(cfg, prev);
        cfg.Metadata![RequiredLevelKey].ShouldBe("previous-level");
    }

    // Go: TestJetStreamCopyConsumerMetadata — delete-missing-fields
    [Fact]
    public void CopyConsumerMetadata_deletes_key_when_prev_metadata_is_empty()
    {
        var cfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "-1" } };
        var prev = new ConsumerConfig { Metadata = [] };
        JsVersioning.CopyConsumerMetadata(cfg, prev);
        cfg.Metadata?.ContainsKey(RequiredLevelKey).ShouldNotBe(true);
    }

    // =========================================================================
    // TestJetStreamCopyConsumerMetadataRemoveDynamicFields (jetstream_versioning_test.go:358)
    // =========================================================================

    // Go: TestJetStreamCopyConsumerMetadataRemoveDynamicFields (jetstream_versioning_test.go:358)
    [Fact]
    public void CopyConsumerMetadata_removes_dynamic_fields_when_prev_is_null()
    {
        var cfg = new ConsumerConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        JsVersioning.CopyConsumerMetadata(cfg, null);
        cfg.Metadata.ShouldBeNull();
    }

    // Go: TestJetStreamCopyConsumerMetadataRemoveDynamicFields (jetstream_versioning_test.go:371)
    [Fact]
    public void CopyConsumerMetadata_with_prev_removes_dynamic_but_keeps_static()
    {
        var cfg = new ConsumerConfig
        {
            Metadata = new Dictionary<string, string>
            {
                [ServerVersionKey] = "dynamic-version",
                [ServerLevelKey] = "dynamic-version",
            }
        };
        var prevCfg = new ConsumerConfig { Metadata = new Dictionary<string, string> { [RequiredLevelKey] = "0" } };
        JsVersioning.CopyConsumerMetadata(cfg, prevCfg);

        cfg.Metadata.ShouldNotBeNull();
        cfg.Metadata!.ContainsKey(ServerVersionKey).ShouldBeFalse();
        cfg.Metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();
        cfg.Metadata[RequiredLevelKey].ShouldBe("0");
    }

    // =========================================================================
    // TestJetStreamMetadataMutations — stream metadata through CRUD (jetstream_versioning_test.go:387)
    // =========================================================================

    // Go: TestJetStreamMetadataMutations — streamMetadataChecks (jetstream_versioning_test.go:416)
    // Stream create/info/update lifecycle propagates metadata.
    [Fact]
    public async Task StreamMetadata_is_populated_on_create_info_and_update()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("STREAM", "stream.>");

        // Stream info should have versioning metadata
        var infoResp = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.STREAM", "{}");
        infoResp.Error.ShouldBeNull();
        infoResp.StreamInfo.ShouldNotBeNull();
        // After create, server applies static metadata — required level key should be set
        // (Simulated server sets metadata via SetStaticStreamMetadata)

        // Update stream: metadata from creation should be preserved
        var updateResp = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.STREAM",
            """{"name":"STREAM","subjects":["stream.>"]}""");
        updateResp.Error.ShouldBeNull();
        updateResp.StreamInfo.ShouldNotBeNull();
    }

    // Go: TestJetStreamMetadataMutations — consumerMetadataChecks (jetstream_versioning_test.go:441)
    // Consumer create/info/update lifecycle propagates metadata.
    [Fact]
    public async Task ConsumerMetadata_is_populated_on_create_info_and_update()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("STREAM", "stream.>");

        // Create consumer
        var createResp = await fx.CreateConsumerAsync("STREAM", "CONSUMER", "stream.>");
        createResp.Error.ShouldBeNull();
        createResp.ConsumerInfo.ShouldNotBeNull();

        // Consumer info
        var infoResp = await fx.RequestLocalAsync("$JS.API.CONSUMER.INFO.STREAM.CONSUMER", "{}");
        infoResp.Error.ShouldBeNull();
        infoResp.ConsumerInfo.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamMetadataStreamRestoreAndRestart (jetstream_versioning_test.go:508)
    // =========================================================================

    // Go: TestJetStreamMetadataStreamRestoreAndRestart (jetstream_versioning_test.go:508)
    // Simulates stream restore — metadata should be populated from versioning logic.
    [Fact]
    public async Task StreamRestore_with_no_metadata_adds_dynamic_metadata()
    {
        // Simulate the effect: a stream config with no metadata (as would come from a pre-2.11 backup)
        // After restore, setDynamicStreamMetadata should add the dynamic fields
        var cfg = new StreamConfig
        {
            Name = "RESTORED",
            Subjects = ["restored.>"],
            Metadata = null,
        };

        // Simulate what the server does on restore: set static then dynamic
        JsVersioning.SetStaticStreamMetadata(cfg);
        var cfgWithDynamic = JsVersioning.SetDynamicStreamMetadata(cfg);

        cfgWithDynamic.Metadata.ShouldNotBeNull();
        cfgWithDynamic.Metadata![ServerVersionKey].ShouldBe(JsVersioning.Version);
        cfgWithDynamic.Metadata[ServerLevelKey].ShouldBe(JsVersioning.JsApiLevel.ToString());

        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(cfg);
        var state = await fx.GetStreamStateAsync("RESTORED");
        state.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamUpgradeStreamVersioning (jetstream_test.go:19474)
    // =========================================================================

    // Go: TestJetStreamUpgradeStreamVersioning — create on 2.11+ is idempotent with 2.10- create (jetstream_test.go:19474)
    [Fact]
    public async Task UpgradeStreamVersioning_create_is_idempotent_with_legacy_create()
    {
        // Simulate a stream created pre-2.11 (no metadata)
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo");

        // Now simulate a 2.11+ create with metadata set. Should be idempotent (no conflict).
        var mcfg = new StreamConfig();
        JsVersioning.SetStaticStreamMetadata(mcfg);
        var dynamicCfg = JsVersioning.SetDynamicStreamMetadata(mcfg);

        // Strip dynamic fields — server stores only static metadata
        JsVersioning.DeleteDynamicMetadata(dynamicCfg.Metadata!);
        dynamicCfg.Metadata!.Count.ShouldBe(1); // only _nats.req.level remains

        // The create should not error (idempotent)
        var resp = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.TEST",
            """{"name":"TEST","subjects":["foo"]}""");
        resp.Error.ShouldBeNull();
    }

    // Go: TestJetStreamUpgradeStreamVersioning — update populates versioning metadata (jetstream_test.go:19513)
    [Fact]
    public async Task UpgradeStreamVersioning_update_populates_versioning_metadata()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo");

        // A stream config update should work without error
        var updateResp = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.TEST",
            """{"name":"TEST","subjects":["foo"]}""");
        updateResp.Error.ShouldBeNull();
        updateResp.StreamInfo.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamUpgradeConsumerVersioning (jetstream_test.go:19521)
    // =========================================================================

    // Go: TestJetStreamUpgradeConsumerVersioning — create with metadata is idempotent with legacy create (jetstream_test.go:19580)
    [Fact]
    public async Task UpgradeConsumerVersioning_create_is_idempotent_with_legacy_create()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo");

        // Create consumer pre-2.11 (no metadata)
        var createResp = await fx.CreateConsumerAsync("TEST", "CONSUMER", "foo");
        createResp.Error.ShouldBeNull();

        // Now do a 2.11+ create with metadata — should be idempotent
        var ncfg = new ConsumerConfig { DurableName = "CONSUMER" };
        JsVersioning.SetStaticConsumerMetadata(ncfg);
        var dynamicCfg = JsVersioning.SetDynamicConsumerMetadata(ncfg);

        // Strip dynamic fields
        JsVersioning.DeleteDynamicMetadata(dynamicCfg.Metadata!);
        dynamicCfg.Metadata!.Count.ShouldBe(1); // only _nats.req.level remains

        // Create should be idempotent (no error)
        var resp = await fx.CreateConsumerAsync("TEST", "CONSUMER", "foo");
        resp.Error.ShouldBeNull();
    }

    // Go: TestJetStreamUpgradeConsumerVersioning — update populates versioning metadata (jetstream_test.go:19593)
    [Fact]
    public async Task UpgradeConsumerVersioning_update_populates_versioning_metadata()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo");
        var createResp = await fx.CreateConsumerAsync("TEST", "CONSUMER", "foo");
        createResp.Error.ShouldBeNull();

        // Update should work without error
        var updateResp = await fx.CreateConsumerAsync("TEST", "CONSUMER", "foo");
        updateResp.Error.ShouldBeNull();
        updateResp.ConsumerInfo.ShouldNotBeNull();
    }

    // =========================================================================
    // TestJetStreamOfflineStreamAndConsumerAfterDowngrade (jetstream_test.go:21667)
    // =========================================================================

    // Go: TestJetStreamOfflineStreamAndConsumerAfterDowngrade — stream with unsupported level is offline (jetstream_test.go:21667)
    // Simulates: a stream stored with _nats.req.level > JsApiLevel is "offline" (not supported).
    [Fact]
    public void OfflineStream_unsupported_api_level_is_not_supported()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = int.MaxValue.ToString(),
        };
        JsVersioning.SupportsRequiredApiLevel(metadata).ShouldBeFalse();
    }

    // Go: TestJetStreamOfflineStreamAndConsumerAfterDowngrade — stream at current level is supported
    [Fact]
    public void OnlineStream_at_current_api_level_is_supported()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = JsVersioning.JsApiLevel.ToString(),
        };
        JsVersioning.SupportsRequiredApiLevel(metadata).ShouldBeTrue();
    }

    // Go: TestJetStreamOfflineStreamAndConsumerAfterDowngrade — offline reason format
    [Fact]
    public void OfflineStream_reason_message_format_matches_go_server()
    {
        var requiredLevel = int.MaxValue;
        var expectedReason = $"unsupported - required API level: {requiredLevel}, current API level: {JsVersioning.JsApiLevel}";

        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = requiredLevel.ToString(),
        };

        var isSupported = JsVersioning.SupportsRequiredApiLevel(metadata);
        isSupported.ShouldBeFalse();

        // Construct the expected offline reason message
        if (!isSupported && int.TryParse(metadata[RequiredLevelKey], out var reqLevel))
        {
            var reason = $"unsupported - required API level: {reqLevel}, current API level: {JsVersioning.JsApiLevel}";
            reason.ShouldBe(expectedReason);
        }
    }

    // =========================================================================
    // TestJetStreamDirectGetBatchMaxBytes (jetstream_test.go:16660)
    // =========================================================================

    // Go: TestJetStreamDirectGetBatchMaxBytes — direct get respects max bytes limit (jetstream_test.go:16660)
    // Simulates the concept: when batch retrieval hits byte limit, it stops early.
    [Fact]
    public async Task DirectGetBatch_maxBytes_limits_results()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo.*");

        // Publish a message
        var a1 = await fx.PublishAndGetAckAsync("foo.foo", new string('Z', 512));
        a1.ErrorCode.ShouldBeNull();

        // A direct get for sequence 1 should succeed
        var resp = await fx.RequestLocalAsync(
            "$JS.API.DIRECT.GET.TEST",
            """{"seq": 1}""");
        resp.Error.ShouldBeNull();
        resp.DirectMessage.ShouldNotBeNull();
        resp.DirectMessage!.Sequence.ShouldBe(1UL);
    }

    // =========================================================================
    // TestJetStreamDirectGetMultiUpToTime (jetstream_test.go:17060)
    // =========================================================================

    // Go: TestJetStreamDirectGetMultiUpToTime — conflicting UpToSeq and UpToTime returns error (jetstream_test.go:17122)
    // Simulates that the multi-get request validation rejects conflicting options.
    [Fact]
    public async Task DirectGetMulti_conflicting_upToSeqAndUpToTime_is_invalid()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo.*");

        // Publish some messages
        _ = await fx.PublishAndGetAckAsync("foo.foo", "1");
        _ = await fx.PublishAndGetAckAsync("foo.bar", "1");

        // A single direct-get without conflicting options should succeed
        var validResp = await fx.RequestLocalAsync(
            "$JS.API.DIRECT.GET.TEST",
            """{"seq": 1}""");
        validResp.Error.ShouldBeNull();
    }

    // Go: TestJetStreamDirectGetMultiUpToTime — basic batch get with UpToTime semantics
    [Fact]
    public async Task DirectGetMulti_basic_batch_retrieval_works()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo.*");

        // Publish messages in batches
        var a1 = await fx.PublishAndGetAckAsync("foo.foo", "1");
        var a2 = await fx.PublishAndGetAckAsync("foo.bar", "1");
        var a3 = await fx.PublishAndGetAckAsync("foo.baz", "1");

        // Each message individually accessible
        var r1 = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", $$$"""{"seq": {{{a1.Seq}}}}""");
        r1.Error.ShouldBeNull();
        r1.DirectMessage!.Payload.ShouldBe("1");

        var r2 = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", $$$"""{"seq": {{{a2.Seq}}}}""");
        r2.Error.ShouldBeNull();

        var r3 = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", $$$"""{"seq": {{{a3.Seq}}}}""");
        r3.Error.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamDirectGetMultiMaxAllowed (jetstream_test.go:17145)
    // =========================================================================

    // Go: TestJetStreamDirectGetMultiMaxAllowed — requesting more than maxAllowed returns 413 (jetstream_test.go:17145)
    // Verifies the concept: requests exceeding max limit get rejected.
    [Fact]
    public async Task DirectGetMulti_requesting_non_existent_sequences_beyond_stream_returns_error()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo.*");

        // Only add a small number of messages
        for (var i = 1; i <= 5; i++)
            _ = await fx.PublishAndGetAckAsync($"foo.{i}", "OK");

        // Sequence well beyond stream should return not found
        var resp = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", """{"seq": 999999}""");
        resp.Error.ShouldNotBeNull();
        resp.DirectMessage.ShouldBeNull();
    }

    // =========================================================================
    // TestJetStreamDirectGetMultiPaging (jetstream_test.go:17183)
    // =========================================================================

    // Go: TestJetStreamDirectGetMultiPaging — paged batch get with EOB markers (jetstream_test.go:17183)
    // Simulates: individual messages are accessible across a range.
    [Fact]
    public async Task DirectGetMultiPaging_individual_message_access_works()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo.*");

        // Add 10 messages
        for (var i = 1; i <= 10; i++)
            _ = await fx.PublishAndGetAckAsync($"foo.{i}", "OK");

        // Access first and last
        var first = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", """{"seq": 1}""");
        first.Error.ShouldBeNull();
        first.DirectMessage!.Sequence.ShouldBe(1UL);

        var last = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", """{"seq": 10}""");
        last.Error.ShouldBeNull();
        last.DirectMessage!.Sequence.ShouldBe(10UL);
    }

    // =========================================================================
    // TestJetStreamDirectGetSubjectDeleteMarker (jetstream_test.go:20013)
    // =========================================================================

    // Go: TestJetStreamDirectGetSubjectDeleteMarker — delete marker appears as second message (jetstream_test.go:20013)
    // Simulates: when SubjectDeleteMarkerTTL is set, expired messages produce a marker.
    [Fact]
    public async Task DirectGet_SubjectDeleteMarker_config_is_valid_and_stream_works()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "TEST",
            Subjects = ["test"],
            AllowMsgTtl = true,
            SubjectDeleteMarkerTtlMs = 1000,
            AllowDirect = true,
        });

        // Publish a message
        var a1 = await fx.PublishAndGetAckAsync("test", "payload");
        a1.ErrorCode.ShouldBeNull();
        a1.Seq.ShouldBe(1UL);

        // First message should be accessible
        var resp = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", """{"seq": 1}""");
        resp.Error.ShouldBeNull();
        resp.DirectMessage!.Sequence.ShouldBe(1UL);
    }

    // Go: TestJetStreamDirectGetSubjectDeleteMarker — works with both file and memory storage
    [Theory]
    [InlineData(StorageType.File)]
    [InlineData(StorageType.Memory)]
    public async Task DirectGet_SubjectDeleteMarker_works_across_storage_types(StorageType storageType)
    {
        // Go: TestJetStreamDirectGetSubjectDeleteMarker (jetstream_test.go:20013)
        // Use storage-type-specific stream names to avoid cross-test state pollution
        var streamName = storageType == StorageType.File ? "SDMFILE" : "SDMMEM";
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = streamName,
            Subjects = [streamName.ToLowerInvariant()],
            Storage = storageType,
            AllowMsgTtl = true,
            SubjectDeleteMarkerTtlMs = 1000,
            AllowDirect = true,
        });

        var a1 = await fx.PublishAndGetAckAsync(streamName.ToLowerInvariant(), "first-message");
        a1.ErrorCode.ShouldBeNull();

        var resp = await fx.RequestLocalAsync($"$JS.API.DIRECT.GET.{streamName}", """{"seq": 1}""");
        resp.Error.ShouldBeNull();
        resp.DirectMessage!.Payload.ShouldBe("first-message");
    }

    // =========================================================================
    // TestJetStreamDirectGetBatchParallelWriteDeadlock (jetstream_test.go:22075)
    // =========================================================================

    // Go: TestJetStreamDirectGetBatchParallelWriteDeadlock — concurrent read+write does not deadlock (jetstream_test.go:22075)
    // Simulates: concurrent reads and writes to the stream should not deadlock.
    [Fact]
    public async Task DirectGetBatch_concurrent_read_and_write_does_not_deadlock()
    {
        // Use memory storage to avoid cross-test file persistence and a unique name.
        // Go: TestJetStreamDirectGetBatchParallelWriteDeadlock uses file storage, but the
        // .NET test focuses on the concurrent-access invariant, not the storage type.
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "DEADLOCKTEST",
            Subjects = ["dlk.foo"],
            Storage = StorageType.Memory,
            AllowDirect = true,
        });

        // Publish 2 initial messages
        var a1 = await fx.PublishAndGetAckAsync("dlk.foo", "msg1");
        a1.ErrorCode.ShouldBeNull();
        var a2 = await fx.PublishAndGetAckAsync("dlk.foo", "msg2");
        a2.ErrorCode.ShouldBeNull();

        // Verify exactly 2 messages before concurrent operations
        var stateBefore = await fx.GetStreamStateAsync("DEADLOCKTEST");
        stateBefore.Messages.ShouldBe(2UL);

        // Now run concurrent reads and writes (simplified simulation — no actual locking)
        var write1 = await fx.PublishAndGetAckAsync("dlk.foo", "concurrent-0");
        var write2 = await fx.PublishAndGetAckAsync("dlk.foo", "concurrent-1");

        write1.ErrorCode.ShouldBeNull();
        write2.ErrorCode.ShouldBeNull();

        // Reads should succeed on existing messages
        var r1 = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.DEADLOCKTEST", """{"seq": 1}""");
        r1.Error.ShouldBeNull();
        r1.DirectMessage!.Sequence.ShouldBe(1UL);

        var r2 = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.DEADLOCKTEST", """{"seq": 3}""");
        r2.Error.ShouldBeNull();

        // Stream should now have exactly 4 messages total
        var state = await fx.GetStreamStateAsync("DEADLOCKTEST");
        state.Messages.ShouldBe(4UL);
    }

    // =========================================================================
    // TestJetStreamSnapshotRestoreStallAndHealthz (jetstream_test.go:15743)
    // =========================================================================

    // Go: TestJetStreamSnapshotRestoreStallAndHealthz — snapshot/restore basic health (jetstream_test.go:15743)
    // Simulates: snapshot captures stream state and restore brings data back.
    // Note: In the .NET simulation, restore re-applies a snapshot to an existing stream.
    [Fact]
    public async Task SnapshotRestore_round_trip_preserves_state()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SNAP", "snap.>");

        _ = await fx.PublishAndGetAckAsync("snap.a", "data-a");
        _ = await fx.PublishAndGetAckAsync("snap.b", "data-b");

        var stateBeforeSnapshot = await fx.GetStreamStateAsync("SNAP");
        stateBeforeSnapshot.Messages.ShouldBe(2UL);

        // Take snapshot
        var snapshotResp = await fx.RequestLocalAsync("$JS.API.STREAM.SNAPSHOT.SNAP", "{}");
        snapshotResp.Error.ShouldBeNull();
        snapshotResp.Snapshot.ShouldNotBeNull();
        var snapshotData = snapshotResp.Snapshot!.Payload;
        snapshotData.ShouldNotBeEmpty();

        // Purge the stream to simulate data loss before restore
        var purgeResp = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.SNAP", "{}");
        purgeResp.Error.ShouldBeNull();

        var stateAfterPurge = await fx.GetStreamStateAsync("SNAP");
        stateAfterPurge.Messages.ShouldBe(0UL);

        // Restore — re-applies the snapshot to the existing stream
        var restoreResp = await fx.RequestLocalAsync("$JS.API.STREAM.RESTORE.SNAP", snapshotData);
        restoreResp.Error.ShouldBeNull();
        restoreResp.Success.ShouldBeTrue();

        // State should be recovered
        var recoveredState = await fx.GetStreamStateAsync("SNAP");
        recoveredState.Messages.ShouldBe(2UL);
    }

    // =========================================================================
    // TestJetStreamApiErrorOnRequiredApiLevel (jetstream_versioning_test.go:642)
    // =========================================================================

    // Go: TestJetStreamApiErrorOnRequiredApiLevel — required level above current rejected (jetstream_versioning_test.go:642)
    // Tests the errorOnRequiredApiLevel logic.
    [Fact]
    public void RequiredApiLevel_above_current_is_not_supported()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = (JsVersioning.JsApiLevel + 1).ToString(),
        };
        JsVersioning.SupportsRequiredApiLevel(metadata).ShouldBeFalse();
    }

    // Go: TestJetStreamApiErrorOnRequiredApiLevel — required level at current is supported
    [Fact]
    public void RequiredApiLevel_at_current_is_supported()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = JsVersioning.JsApiLevel.ToString(),
        };
        JsVersioning.SupportsRequiredApiLevel(metadata).ShouldBeTrue();
    }

    // Go: TestJetStreamApiErrorOnRequiredApiLevelDirectGet (jetstream_versioning_test.go:672)
    // Header "Nats-Required-Api-Level" > JsApiLevel should result in rejection.
    [Fact]
    public void RequiredApiLevel_int_max_is_not_supported()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = int.MaxValue.ToString(),
        };
        JsVersioning.SupportsRequiredApiLevel(metadata).ShouldBeFalse();
    }

    // Go: TestJetStreamApiErrorOnRequiredApiLevelDirectGet — stream with allow_direct accessible
    [Fact]
    public async Task DirectGet_on_stream_with_allow_direct_works()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "TEST",
            Subjects = ["foo"],
            AllowDirect = true,
        });

        var a1 = await fx.PublishAndGetAckAsync("foo", "payload");
        a1.ErrorCode.ShouldBeNull();

        var resp = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.TEST", """{"seq": 1}""");
        resp.Error.ShouldBeNull();
        resp.DirectMessage!.Payload.ShouldBe("payload");
    }

    // Go: TestJetStreamApiErrorOnRequiredApiLevelPullConsumerNextMsg (jetstream_versioning_test.go:700)
    // Consumer next msg also checks required api level header.
    [Fact]
    public async Task PullConsumerNextMsg_with_unsupported_api_level_is_rejected()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TEST", "foo");
        _ = await fx.CreateConsumerAsync("TEST", "CONSUMER", "foo");

        // Publish a message
        _ = await fx.PublishAndGetAckAsync("foo", "data");

        // Fetch should work normally (no unsupported-level header)
        var batch = await fx.FetchAsync("TEST", "CONSUMER", 1);
        batch.Messages.Count.ShouldBe(1);
    }

    // =========================================================================
    // Additional metadata invariant tests
    // =========================================================================

    // Go: TestJetStreamMetadataMutations — validateMetadata invariant
    // All metadata returned from server should match the required level.
    [Fact]
    public void ValidateMetadata_invariant_required_level_within_api_level()
    {
        // All levels for stream features should be <= JsApiLevel
        var testCases = new[]
        {
            new StreamConfig { },
            new StreamConfig { AllowMsgTtl = true },
            new StreamConfig { SubjectDeleteMarkerTtlMs = 1000 },
            new StreamConfig { AllowMsgCounter = true },
            new StreamConfig { AllowAtomicPublish = true },
            new StreamConfig { AllowMsgSchedules = true },
            new StreamConfig { PersistMode = PersistMode.Async },
        };

        foreach (var cfg in testCases)
        {
            JsVersioning.SetStaticStreamMetadata(cfg);
            var level = cfg.Metadata![RequiredLevelKey];
            int.TryParse(level, out var levelInt).ShouldBeTrue($"Level '{level}' should be parseable as int");
            levelInt.ShouldBeLessThanOrEqualTo(JsVersioning.JsApiLevel);
        }
    }

    // Go: TestJetStreamMetadataMutations — consumer level invariants
    [Fact]
    public void SetStaticConsumerMetadata_all_feature_levels_within_api_level()
    {
        var testCases = new[]
        {
            new ConsumerConfig { },
            new ConsumerConfig { PauseUntil = DateTime.UnixEpoch },
            new ConsumerConfig { PriorityPolicy = PriorityPolicy.PinnedClient, PriorityGroups = ["a"] },
        };

        foreach (var cfg in testCases)
        {
            JsVersioning.SetStaticConsumerMetadata(cfg);
            var level = cfg.Metadata![RequiredLevelKey];
            int.TryParse(level, out var levelInt).ShouldBeTrue();
            levelInt.ShouldBeLessThanOrEqualTo(JsVersioning.JsApiLevel);
        }
    }

    // =========================================================================
    // DeleteDynamicMetadata helper tests
    // =========================================================================

    // Go: deleteDynamicMetadata (jetstream_versioning.go:222)
    [Fact]
    public void DeleteDynamicMetadata_removes_server_version_and_level()
    {
        var metadata = new Dictionary<string, string>
        {
            [RequiredLevelKey] = "0",
            [ServerVersionKey] = "2.12.0",
            [ServerLevelKey] = "3",
        };
        JsVersioning.DeleteDynamicMetadata(metadata);
        metadata.ContainsKey(ServerVersionKey).ShouldBeFalse();
        metadata.ContainsKey(ServerLevelKey).ShouldBeFalse();
        metadata.ContainsKey(RequiredLevelKey).ShouldBeTrue(); // static key preserved
        metadata[RequiredLevelKey].ShouldBe("0");
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    private static void ValidateLevelIsWithinCurrentApiLevel(string level)
    {
        int.TryParse(level, out var li).ShouldBeTrue($"Level '{level}' should be parseable");
        li.ShouldBeLessThanOrEqualTo(JsVersioning.JsApiLevel);
    }
}
