// Go: consumer.go:2550 (processAckMsg, processNak, processTerm, processAckProgress)
using NATS.Server.JetStream.Consumers;

namespace NATS.Server.Tests.JetStream.Consumers;

public class AckProcessorNakTests
{
    // Test 1: ProcessAck with empty payload acks the sequence
    [Fact]
    public void ProcessAck_empty_payload_acks_sequence()
    {
        // Go: consumer.go — empty ack payload treated as "+ACK"
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);

        ack.ProcessAck(1, ReadOnlySpan<byte>.Empty);

        ack.PendingCount.ShouldBe(0);
        ack.AckFloor.ShouldBe((ulong)1);
    }

    // Test 2: ProcessAck with -NAK schedules redelivery
    [Fact]
    public async Task ProcessAck_nak_payload_schedules_redelivery()
    {
        // Go: consumer.go — "-NAK" triggers rescheduled redelivery
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);

        ack.ProcessAck(1, "-NAK"u8);

        // Should still be pending (redelivery scheduled)
        ack.PendingCount.ShouldBe(1);

        // Should expire quickly (using ackWait fallback of 5000ms — verify it is still pending now)
        ack.TryGetExpired(out _, out _).ShouldBeFalse();

        await Task.CompletedTask;
    }

    // Test 3: ProcessAck with -NAK {delay} uses custom delay
    [Fact]
    public async Task ProcessAck_nak_with_delay_uses_custom_delay()
    {
        // Go: consumer.go — "-NAK {delay}" parses optional explicit delay in milliseconds
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);

        ack.ProcessAck(1, "-NAK 1"u8);

        // Sequence still pending
        ack.PendingCount.ShouldBe(1);

        // With a 1ms delay, should expire quickly
        await Task.Delay(10);
        ack.TryGetExpired(out var seq, out _).ShouldBeTrue();
        seq.ShouldBe((ulong)1);
    }

    // Test 4: ProcessAck with +TERM removes from pending
    [Fact]
    public void ProcessAck_term_removes_from_pending()
    {
        // Go: consumer.go — "+TERM" permanently terminates delivery; sequence never redelivered
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);

        ack.ProcessAck(1, "+TERM"u8);

        ack.PendingCount.ShouldBe(0);
        ack.HasPending.ShouldBeFalse();
    }

    // Test 5: ProcessAck with +WPI resets deadline without incrementing delivery count
    [Fact]
    public async Task ProcessAck_wpi_resets_deadline_without_incrementing_deliveries()
    {
        // Go: consumer.go — "+WPI" resets ack deadline; delivery count must not change
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 10);

        // Wait for the deadline to approach, then reset it via progress
        await Task.Delay(5);
        ack.ProcessAck(1, "+WPI"u8);

        // Deadline was just reset — should not be expired yet
        ack.TryGetExpired(out _, out var deliveries).ShouldBeFalse();

        // Deliveries count must remain at 1 (not incremented by WPI)
        deliveries.ShouldBe(0);

        // Sequence still pending
        ack.PendingCount.ShouldBe(1);
    }

    // Test 6: Backoff array applies correct delay per redelivery attempt
    [Fact]
    public async Task ProcessNak_backoff_array_applies_delay_by_delivery_count()
    {
        // Go: consumer.go — backoff array indexes by (deliveries - 1)
        var ack = new AckProcessor(backoffMs: [1, 50, 5000]);
        ack.Register(1, ackWaitMs: 5000);

        // First NAK — delivery count is 1 → backoff[0] = 1ms
        ack.ProcessNak(1);

        await Task.Delay(10);
        ack.TryGetExpired(out _, out _).ShouldBeTrue();

        // Now delivery count is 2 → backoff[1] = 50ms
        ack.ProcessNak(1);
        ack.TryGetExpired(out _, out _).ShouldBeFalse();
    }

    // Test 7: Backoff array clamps at last entry for high delivery counts
    [Fact]
    public async Task ProcessNak_backoff_clamps_at_last_entry_for_high_delivery_count()
    {
        // Go: consumer.go — backoff index clamped to backoff.Length-1 when deliveries exceed array size
        var ack = new AckProcessor(backoffMs: [1, 2]);
        ack.Register(1, ackWaitMs: 5000);

        // Drive deliveries up: NAK twice to advance delivery count past array length
        ack.ProcessNak(1); // deliveries becomes 2 (index 1 = 2ms)
        await Task.Delay(10);
        ack.TryGetExpired(out _, out _).ShouldBeTrue();

        ack.ProcessNak(1); // deliveries becomes 3 (index clamps to 1 = 2ms)
        await Task.Delay(10);
        ack.TryGetExpired(out var seq, out _).ShouldBeTrue();
        seq.ShouldBe((ulong)1);
    }

    // Test 8: AckSequence advances AckFloor when contiguous
    [Fact]
    public void AckSequence_advances_ackfloor_for_contiguous_sequences()
    {
        // Go: consumer.go — acking contiguous sequences from floor advances AckFloor monotonically
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);
        ack.Register(2, ackWaitMs: 5000);
        ack.Register(3, ackWaitMs: 5000);

        ack.AckSequence(1);
        ack.AckFloor.ShouldBe((ulong)1);

        ack.AckSequence(2);
        ack.AckFloor.ShouldBe((ulong)2);
    }

    // Test 9: ProcessTerm increments TerminatedCount
    [Fact]
    public void ProcessTerm_increments_terminated_count()
    {
        // Go: consumer.go — terminated sequences tracked separately from acked sequences
        var ack = new AckProcessor();
        ack.Register(1, ackWaitMs: 5000);
        ack.Register(2, ackWaitMs: 5000);

        ack.TerminatedCount.ShouldBe(0);

        ack.ProcessTerm(1);
        ack.TerminatedCount.ShouldBe(1);

        ack.ProcessTerm(2);
        ack.TerminatedCount.ShouldBe(2);
    }

    // Test 10: NAK after TERM is ignored (sequence already terminated)
    [Fact]
    public void ProcessNak_after_term_is_ignored()
    {
        // Go: consumer.go — once terminated, a sequence cannot be rescheduled via NAK
        var ack = new AckProcessor(backoffMs: [1]);
        ack.Register(1, ackWaitMs: 5000);

        ack.ProcessTerm(1);
        ack.PendingCount.ShouldBe(0);

        // Attempting to NAK a terminated sequence has no effect
        ack.ProcessNak(1);
        ack.PendingCount.ShouldBe(0);
        ack.TerminatedCount.ShouldBe(1);
    }
}
