using NATS.Server.TestUtilities;

// Ported from golang/nats-server/server/jetstream_test.go
// Reference Go tests: TestJetStreamAddStream, TestJetStreamAddStreamSameConfigOK,
// TestJetStreamUpdateStream, TestJetStreamStreamPurge, TestJetStreamDeleteMsg

namespace NATS.Server.JetStream.Tests;

public class StreamLifecycleTests
{
    // Go ref: TestJetStreamAddStream (line 178)
    // After addStream the stream exists with zero messages and the correct config.
    // Verifies the CREATE API response and a subsequent INFO lookup both reflect
    // the initial empty state with the right config.
    [Fact]
    public async Task Stream_create_returns_config_and_zero_message_state()
    {
        // Go ref: TestJetStreamAddStream — after addStream the stream exists with
        // zero messages and the correct config.  Here we verify the CREATE API
        // response shape and a subsequent INFO lookup both reflect the initial state.
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EVENTS", "events.*");

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

        info.Error.ShouldBeNull();
        info.StreamInfo.ShouldNotBeNull();
        info.StreamInfo.Config.Name.ShouldBe("EVENTS");
        info.StreamInfo.Config.Subjects.ShouldContain("events.*");
        info.StreamInfo.State.Messages.ShouldBe((ulong)0);
    }

    // Go ref: TestJetStreamAddStreamSameConfigOK (line 701)
    // Verifies that creating a stream with the same config twice is idempotent —
    // the Go test calls acc.addStream twice with the identical mconfig and expects
    // no error on the second call.
    [Fact]
    public async Task Stream_create_with_same_config_is_idempotent()
    {
        // StartWithStreamAsync creates the stream once internally.
        // Call CREATE again with the identical config on the same fixture instance.
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");

        // Second call with identical config must also succeed (no error).
        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 ref: TestJetStreamUpdateStream (line 6409)
    // Verifies that updating a stream's subjects succeeds and that the updated
    // config is reflected in a subsequent INFO call.  The Go test updates MaxMsgs
    // and verifies mset.config().MaxMsgs matches the updated value.
    [Fact]
    public async Task Stream_update_replaces_subjects_and_max_msgs()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");

        // Publish a few messages before the update so we can verify state is preserved.
        _ = await fx.PublishAndGetAckAsync("orders.created", "msg1");
        _ = await fx.PublishAndGetAckAsync("orders.created", "msg2");

        var stateBefore = await fx.GetStreamStateAsync("ORDERS");
        stateBefore.Messages.ShouldBe((ulong)2);

        // Update: change subjects and raise max_msgs limit.
        var update = await fx.RequestLocalAsync(
            "$JS.API.STREAM.UPDATE.ORDERS",
            "{\"name\":\"ORDERS\",\"subjects\":[\"orders.v2.*\"],\"max_msgs\":100}");

        update.Error.ShouldBeNull();
        update.StreamInfo.ShouldNotBeNull();
        update.StreamInfo.Config.Subjects.ShouldContain("orders.v2.*");
        update.StreamInfo.Config.MaxMsgs.ShouldBe(100);

        // INFO reflects updated config.
        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.ORDERS", "{}");
        info.Error.ShouldBeNull();
        info.StreamInfo!.Config.Subjects.ShouldContain("orders.v2.*");
    }

    // Go ref: TestJetStreamStreamPurge (line 4182)
    // Verifies that purging a stream removes all messages and resets the state,
    // matching the Go assertion: state.Msgs == 0 after mset.purge(nil), and that
    // publishing a new message afterwards records Msgs == 1.
    [Fact]
    public async Task Stream_purge_clears_all_messages_and_resets_state()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DC", "dc.*");

        // Publish 5 messages.
        for (var i = 0; i < 5; i++)
            _ = await fx.PublishAndGetAckAsync("dc.msg", $"payload-{i}");

        var beforePurge = await fx.GetStreamStateAsync("DC");
        beforePurge.Messages.ShouldBe((ulong)5);

        // Purge via the API.
        var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.DC", "{}");
        purge.Success.ShouldBeTrue();
        purge.Error.ShouldBeNull();

        var afterPurge = await fx.GetStreamStateAsync("DC");
        afterPurge.Messages.ShouldBe((ulong)0);

        // Publishing a new message after purge should be seq 1 relative perspective
        // (the store starts fresh) — state.Messages rises to 1.
        var ack = await fx.PublishAndGetAckAsync("dc.msg", "after-purge");
        ack.Stream.ShouldBe("DC");

        var afterPublish = await fx.GetStreamStateAsync("DC");
        afterPublish.Messages.ShouldBe((ulong)1);
    }

    // Go ref: TestJetStreamUpdateStream (line 6409) — deletion side,
    //         TestJetStreamAddStream (line 229) — mset.delete() check.
    // Verifies that deleting a stream succeeds and that a subsequent INFO returns
    // a not-found error, matching the Go behaviour where deleted streams are no
    // longer accessible via the API.
    [Fact]
    public async Task Stream_delete_removes_stream_and_info_returns_not_found()
    {
        await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");

        _ = await fx.PublishAndGetAckAsync("orders.placed", "order-1");

        var stateBefore = await fx.GetStreamStateAsync("ORDERS");
        stateBefore.Messages.ShouldBe((ulong)1);

        var delete = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.ORDERS", "{}");
        delete.Success.ShouldBeTrue();
        delete.Error.ShouldBeNull();

        // Subsequent INFO must return an error (stream no longer exists).
        var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.ORDERS", "{}");
        info.Error.ShouldNotBeNull();
        info.StreamInfo.ShouldBeNull();
    }
}
