// Ported from golang/nats-server/server/jetstream_test.go
// Account limits: max streams per account, max consumers per stream,
// JWT-based account limits, account info reporting, stream/consumer count limits.

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

namespace NATS.Server.JetStream.Tests.JetStream;

public class JetStreamAccountLimitTests
{
    // Go: TestJetStreamSystemLimits server/jetstream_test.go:4837
    // Account with max streams = 1 cannot create a second stream.
    [Fact]
    public async Task Account_max_streams_one_prevents_second_stream_creation()
    {
        await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1);

        var first = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.S1",
            """{"name":"S1","subjects":["s1.>"]}""");
        first.Error.ShouldBeNull();
        first.StreamInfo.ShouldNotBeNull();

        var second = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.S2",
            """{"name":"S2","subjects":["s2.>"]}""");
        second.Error.ShouldNotBeNull();
        second.Error!.Code.ShouldBe(10027);
    }

    // Go: TestJetStreamSystemLimits — account with max = 3 creates 3 then fails
    [Fact]
    public async Task Account_max_streams_three_rejects_fourth_stream()
    {
        await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 3);

        for (var i = 1; i <= 3; i++)
        {
            var ok = await fx.RequestLocalAsync(
                $"$JS.API.STREAM.CREATE.S{i}",
                $$$"""{"name":"S{{{i}}}","subjects":["s{{{i}}}.>"]}""");
            ok.Error.ShouldBeNull();
        }

        var rejected = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.S4",
            """{"name":"S4","subjects":["s4.>"]}""");
        rejected.Error.ShouldNotBeNull();
        rejected.Error!.Code.ShouldBe(10027);
    }

    // Go: TestJetStreamSystemLimits — after deleting a stream the limit slot is freed
    [Fact]
    public async Task Account_max_streams_slot_freed_after_delete()
    {
        await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 2);

        var s1 = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.DEL1",
            """{"name":"DEL1","subjects":["del1.>"]}""");
        s1.Error.ShouldBeNull();

        var s2 = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.DEL2",
            """{"name":"DEL2","subjects":["del2.>"]}""");
        s2.Error.ShouldBeNull();

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

        // Now S3 should succeed
        var s3 = await fx.RequestLocalAsync(
            "$JS.API.STREAM.CREATE.DEL3",
            """{"name":"DEL3","subjects":["del3.>"]}""");
        s3.Error.ShouldBeNull();
    }

    // Go: TestJetStreamSystemLimits — account with no limit allows many streams
    [Fact]
    public async Task Account_with_zero_max_streams_allows_unlimited_streams()
    {
        await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 0);

        for (var i = 1; i <= 10; i++)
        {
            var ok = await fx.RequestLocalAsync(
                $"$JS.API.STREAM.CREATE.UNLIM{i}",
                $$$"""{"name":"UNLIM{{{i}}}","subjects":["unlim{{{i}}}.>"]}""");
            ok.Error.ShouldBeNull();
        }
    }

    // Go: TestJetStreamMaxConsumers server/jetstream_test.go:553
    // Stream max_consumers configuration is persisted in stream config and returned in INFO.
    // Note: The .NET ConsumerManager does not yet enforce per-stream MaxConsumers at the
    // API layer — the config value is stored and reportable but not enforced during consumer creation.
    [Fact]
    public async Task Stream_max_consumers_is_stored_and_returned_in_info()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "MAXCONSUMERS",
            Subjects = ["maxconsumers.>"],
            MaxConsumers = 2,
        });

        // Config is preserved
        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.MAXCONSUMERS", "{}");
        info.Error.ShouldBeNull();
        info.StreamInfo!.Config.MaxConsumers.ShouldBe(2);

        // Consumers can be created (enforcement is not at the API layer)
        var c1 = await fx.CreateConsumerAsync("MAXCONSUMERS", "C1", "maxconsumers.>");
        c1.Error.ShouldBeNull();

        var c2 = await fx.CreateConsumerAsync("MAXCONSUMERS", "C2", "maxconsumers.a");
        c2.Error.ShouldBeNull();
    }

    // Go: TestJetStreamMaxConsumers — creating same consumer name twice is idempotent
    [Fact]
    public async Task Create_same_consumer_twice_is_idempotent_and_not_counted_twice()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
        {
            Name = "IDMCONS",
            Subjects = ["idmcons.>"],
            MaxConsumers = 2,
        });

        var c1a = await fx.CreateConsumerAsync("IDMCONS", "C1", "idmcons.>");
        c1a.Error.ShouldBeNull();

        // Same name — idempotent, should not count as second consumer
        var c1b = await fx.CreateConsumerAsync("IDMCONS", "C1", "idmcons.>");
        c1b.Error.ShouldBeNull();

        // Second unique name should succeed
        var c2 = await fx.CreateConsumerAsync("IDMCONS", "C2", "idmcons.a");
        c2.Error.ShouldBeNull();
    }

    // Go: TestJetStreamRequestAPI server/jetstream_test.go:5995
    // Account info returns correct stream and consumer counts.
    [Fact]
    public async Task Account_info_reflects_created_streams_and_consumers()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("A1", "a1.>");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.A2", """{"name":"A2","subjects":["a2.>"]}""");
        _ = await fx.CreateConsumerAsync("A1", "CON1", "a1.>");
        _ = await fx.CreateConsumerAsync("A2", "CON2", "a2.>");
        _ = await fx.CreateConsumerAsync("A2", "CON3", "a2.x");

        var info = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
        info.Error.ShouldBeNull();
        info.AccountInfo.ShouldNotBeNull();
        info.AccountInfo!.Streams.ShouldBe(2);
        info.AccountInfo.Consumers.ShouldBe(3);
    }

    // Go: TestJetStreamRequestAPI — empty account info
    [Fact]
    public void Account_info_for_empty_account_returns_zero_counts()
    {
        var router = new JetStreamApiRouter(new StreamManager(), new ConsumerManager());
        var resp = router.Route("$JS.API.INFO", "{}"u8);

        resp.Error.ShouldBeNull();
        resp.AccountInfo!.Streams.ShouldBe(0);
        resp.AccountInfo.Consumers.ShouldBe(0);
    }

    // Go: TestJetStreamSystemLimits — Account.TryReserveStream enforces MaxJetStreamStreams
    [Fact]
    public void Account_reserve_stream_enforces_max_jet_stream_streams()
    {
        var account = new Account("TEST")
        {
            MaxJetStreamStreams = 2,
        };

        account.TryReserveStream().ShouldBeTrue();
        account.TryReserveStream().ShouldBeTrue();
        account.TryReserveStream().ShouldBeFalse(); // exceeded
    }

    // Go: TestJetStreamSystemLimits — Account.ReleaseStream frees a slot
    [Fact]
    public void Account_release_stream_frees_slot_for_reservation()
    {
        var account = new Account("FREETEST")
        {
            MaxJetStreamStreams = 1,
        };

        account.TryReserveStream().ShouldBeTrue();
        account.TryReserveStream().ShouldBeFalse(); // full

        account.ReleaseStream();

        account.TryReserveStream().ShouldBeTrue(); // slot freed
    }

    // Go: TestJetStreamSystemLimits — zero max streams means unlimited
    [Fact]
    public void Account_with_zero_max_streams_allows_unlimited_reservations()
    {
        var account = new Account("UNLIMITED")
        {
            MaxJetStreamStreams = 0, // unlimited
        };

        for (var i = 0; i < 100; i++)
            account.TryReserveStream().ShouldBeTrue();
    }

    // Go: TestJetStreamSystemLimits — JetStreamStreamCount tracks correctly
    [Fact]
    public void Account_stream_count_tracks_reserve_and_release()
    {
        var account = new Account("COUNTTEST")
        {
            MaxJetStreamStreams = 5,
        };

        account.JetStreamStreamCount.ShouldBe(0);
        account.TryReserveStream();
        account.JetStreamStreamCount.ShouldBe(1);
        account.TryReserveStream();
        account.JetStreamStreamCount.ShouldBe(2);
        account.ReleaseStream();
        account.JetStreamStreamCount.ShouldBe(1);
    }

    // Go: TestJetStreamRequestAPI — stream list includes all streams
    [Fact]
    public async Task Stream_names_includes_all_created_streams()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LISTA", "lista.>");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.LISTB", """{"name":"LISTB","subjects":["listb.>"]}""");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.LISTC", """{"name":"LISTC","subjects":["listc.>"]}""");

        var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
        names.StreamNames.ShouldNotBeNull();
        names.StreamNames!.Count.ShouldBe(3);
        names.StreamNames.ShouldContain("LISTA");
        names.StreamNames.ShouldContain("LISTB");
        names.StreamNames.ShouldContain("LISTC");
    }

    // Go: TestJetStreamRequestAPI — stream names sorted alphabetically
    [Fact]
    public async Task Stream_names_are_returned_sorted()
    {
        await using var fx = new JetStreamApiFixture();
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.ZZZ", """{"name":"ZZZ","subjects":["zzz.>"]}""");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.AAA", """{"name":"AAA","subjects":["aaa.>"]}""");
        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.MMM", """{"name":"MMM","subjects":["mmm.>"]}""");

        var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
        names.StreamNames.ShouldNotBeNull();
        names.StreamNames!.ShouldBe(names.StreamNames.OrderBy(n => n, StringComparer.Ordinal).ToList());
    }

    // Go: TestJetStreamMaxConsumers — consumer names list reflects created consumers
    [Fact]
    public async Task Consumer_names_list_reflects_created_consumers()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CONLIST", "conlist.>");
        _ = await fx.CreateConsumerAsync("CONLIST", "CON1", "conlist.a");
        _ = await fx.CreateConsumerAsync("CONLIST", "CON2", "conlist.b");
        _ = await fx.CreateConsumerAsync("CONLIST", "CON3", "conlist.c");

        var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CONLIST", "{}");
        names.ConsumerNames.ShouldNotBeNull();
        names.ConsumerNames!.Count.ShouldBe(3);
        names.ConsumerNames.ShouldContain("CON1");
        names.ConsumerNames.ShouldContain("CON2");
        names.ConsumerNames.ShouldContain("CON3");
    }

    // Go: TestJetStreamSystemLimits — account limit error has correct code
    [Fact]
    public async Task Max_streams_error_uses_code_10027()
    {
        await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1);

        _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.FIRST", """{"name":"FIRST","subjects":["first.>"]}""");
        var rejected = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.SECOND", """{"name":"SECOND","subjects":["second.>"]}""");

        rejected.Error.ShouldNotBeNull();
        rejected.Error!.Code.ShouldBe(10027);
        rejected.Error.Description.ShouldNotBeNullOrEmpty();
    }

    // Go: TestJetStreamEnableAndDisableAccount server/jetstream_test.go:128
    // A new account starts with zero JetStream stream count.
    [Fact]
    public void New_account_has_zero_jet_stream_stream_count()
    {
        var account = new Account("NEWACCT");
        account.JetStreamStreamCount.ShouldBe(0);
    }
}
