// Port of Go tests from golang/nats-server/server/jetstream_test.go
// TestJetStreamPubAck, TestJetStreamPublishDeDupe, TestJetStreamPublishExpect

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

namespace NATS.Server.JetStream.Tests.JetStream;

public class PublishAckParityTests
{
    // Go ref: TestJetStreamPubAck (jetstream_test.go:354)
    // Verifies that each published message returns a PubAck with the correct stream
    // name and a monotonically incrementing sequence number.
    [Fact]
    public async Task PubAck_stream_name_and_incrementing_seq_are_returned()
    {
        await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("PUBACK", "foo");

        for (var i = 1UL; i <= 5UL; i++)
        {
            var ack = await fixture.PublishAndGetAckAsync("foo", "HELLO");
            ack.Stream.ShouldBe("PUBACK");
            ack.Seq.ShouldBe(i);
            ack.ErrorCode.ShouldBeNull();
        }
    }

    // Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2657) — first block
    // When the same Nats-Msg-Id is published twice within the duplicate window the
    // server returns the original sequence and does not store a second message.
    [Fact]
    public async Task Duplicate_msgid_within_window_returns_same_sequence()
    {
        var streamManager = new StreamManager();
        streamManager.CreateOrUpdate(new StreamConfig
        {
            Name = "DEDUPE",
            Subjects = ["foo.*"],
            DuplicateWindowMs = 2_000,
        }).Error.ShouldBeNull();

        var publisher = new JetStreamPublisher(streamManager);

        // First publish — should store at seq 1
        publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
            new PublishOptions { MsgId = "AA" }, out var first).ShouldBeTrue();
        first.ErrorCode.ShouldBeNull();
        first.Seq.ShouldBe(1UL);

        // Second publish — same MsgId within window, should return the original seq
        publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
            new PublishOptions { MsgId = "AA" }, out var second).ShouldBeTrue();
        second.Seq.ShouldBe(first.Seq);

        // Stream should still contain only one message
        var state = await streamManager.GetStateAsync("DEDUPE", default);
        state.Messages.ShouldBe(1UL);
    }

    // Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2728) — window-expiry block
    // After the duplicate window has elapsed the same MsgId is treated as a new publish
    // and gets a new, higher sequence number.
    [Fact]
    public async Task Duplicate_msgid_after_window_expiry_creates_new_message()
    {
        var streamManager = new StreamManager();
        streamManager.CreateOrUpdate(new StreamConfig
        {
            Name = "DEDUPE2",
            Subjects = ["bar.*"],
            DuplicateWindowMs = 30,
        }).Error.ShouldBeNull();

        var publisher = new JetStreamPublisher(streamManager);

        publisher.TryCaptureWithOptions("bar.1", "first"u8.ToArray(),
            new PublishOptions { MsgId = "M1" }, out var first).ShouldBeTrue();
        first.ErrorCode.ShouldBeNull();

        // Wait for the duplicate window to expire
        await Task.Delay(60);

        // Same MsgId after window — should be treated as a new message
        publisher.TryCaptureWithOptions("bar.1", "after-window"u8.ToArray(),
            new PublishOptions { MsgId = "M1" }, out var third).ShouldBeTrue();
        third.ErrorCode.ShouldBeNull();
        third.Seq.ShouldBeGreaterThan(first.Seq);

        // Both messages should now be stored
        var state = await streamManager.GetStateAsync("DEDUPE2", default);
        state.Messages.ShouldBe(2UL);
    }

    // Go ref: TestJetStreamPublishDeDupe (jetstream_test.go:2716) — four-distinct-ids block
    // Multiple distinct MsgIds within the window are all stored as separate messages.
    [Fact]
    public async Task Distinct_msgids_within_window_each_stored_as_separate_message()
    {
        var streamManager = new StreamManager();
        streamManager.CreateOrUpdate(new StreamConfig
        {
            Name = "DEDUPED",
            Subjects = ["foo.*"],
            DuplicateWindowMs = 2_000,
        }).Error.ShouldBeNull();

        var publisher = new JetStreamPublisher(streamManager);
        var ids = new[] { "AA", "BB", "CC", "ZZ" };

        for (var i = 0; i < ids.Length; i++)
        {
            publisher.TryCaptureWithOptions($"foo.{i + 1}", "Hello DeDupe!"u8.ToArray(),
                new PublishOptions { MsgId = ids[i] }, out var ack).ShouldBeTrue();
            ack.ErrorCode.ShouldBeNull();
            ack.Seq.ShouldBe((ulong)(i + 1));
        }

        var state = await streamManager.GetStateAsync("DEDUPED", default);
        state.Messages.ShouldBe(4UL);

        // Re-sending the same MsgIds must NOT increase the message count
        foreach (var id in ids)
        {
            publisher.TryCaptureWithOptions("foo.1", "Hello DeDupe!"u8.ToArray(),
                new PublishOptions { MsgId = id }, out _).ShouldBeTrue();
        }

        state = await streamManager.GetStateAsync("DEDUPED", default);
        state.Messages.ShouldBe(4UL);
    }

    // Go ref: TestJetStreamPublishExpect (jetstream_test.go:2817) — expected-last-seq block
    // Publishing with an ExpectedLastSeq that does not match the current last sequence
    // of the stream must return error code 10071.
    [Fact]
    public async Task Expected_last_seq_mismatch_returns_error_code_10071()
    {
        await using var fixture = await JetStreamApiFixture.StartWithStreamAsync("EXPECT", "foo.*");

        // Publish one message so the stream has last seq = 1
        var first = await fixture.PublishAndGetAckAsync("foo.bar", "HELLO");
        first.Seq.ShouldBe(1UL);
        first.ErrorCode.ShouldBeNull();

        // Expect last seq = 10 — this must fail because actual is 1
        var bad = await fixture.PublishWithExpectedLastSeqAsync("foo.bar", "HELLO", expectedLastSeq: 10);
        bad.ErrorCode.ShouldBe(10071);
    }
}
