// Go: consumer.go (trackPending ~line 5540, processNak, rdq/rdc map,
//               addToRedeliverQueue, maxdeliver check)
using NATS.Server.JetStream.Consumers;

namespace NATS.Server.Tests.JetStream.Consumers;

public class RedeliveryTrackerTests
{
    // -------------------------------------------------------------------------
    // Test 1 — Backoff array clamping at last entry for high delivery counts
    //
    // Go reference: consumer.go — backoff index = min(deliveries-1, len(backoff)-1)
    // so that sequences with delivery counts past the array length use the last
    // backoff value rather than going out of bounds.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task Schedule_clamps_backoff_at_last_entry_for_high_delivery_count()
    {
        var tracker = new RedeliveryTracker([1, 5000]);

        // delivery 1 → backoff[0] = 1ms
        tracker.Schedule(seq: 1, deliveryCount: 1);
        await Task.Delay(10);
        tracker.GetDue().ShouldContain(1UL);

        tracker.Acknowledge(1);

        // delivery 3 → index clamps to 1 → backoff[1] = 5000ms
        tracker.Schedule(seq: 1, deliveryCount: 3);
        tracker.GetDue().ShouldNotContain(1UL);
    }

    // -------------------------------------------------------------------------
    // Test 2 — GetDue returns only entries whose deadline has passed
    //
    // Go reference: consumer.go — rdq items are eligible for redelivery only
    // once their scheduled deadline has elapsed.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task GetDue_returns_only_expired_entries()
    {
        var tracker = new RedeliveryTracker([1, 5000]);

        // 1ms backoff → will expire quickly
        tracker.Schedule(seq: 10, deliveryCount: 1);
        // 5000ms backoff → will not expire in test window
        tracker.Schedule(seq: 20, deliveryCount: 2);

        // Neither should be due yet immediately after scheduling
        tracker.GetDue().ShouldNotContain(10UL);

        await Task.Delay(15);

        var due = tracker.GetDue();
        due.ShouldContain(10UL);
        due.ShouldNotContain(20UL);
    }

    // -------------------------------------------------------------------------
    // Test 3 — Acknowledge removes the sequence from tracking
    //
    // Go reference: consumer.go — acking a sequence removes it from pending map
    // so it is never surfaced by GetDue again.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task Acknowledge_removes_sequence_from_tracking()
    {
        var tracker = new RedeliveryTracker([1]);

        tracker.Schedule(seq: 5, deliveryCount: 1);
        await Task.Delay(10);

        tracker.GetDue().ShouldContain(5UL);

        tracker.Acknowledge(5);

        tracker.IsTracking(5).ShouldBeFalse();
        tracker.GetDue().ShouldNotContain(5UL);
        tracker.TrackedCount.ShouldBe(0);
    }

    // -------------------------------------------------------------------------
    // Test 4 — IsMaxDeliveries returns true when threshold is reached
    //
    // Go reference: consumer.go — when rdc[sseq] >= MaxDeliver the sequence is
    // dropped from redelivery and never surfaced again.
    // -------------------------------------------------------------------------
    [Fact]
    public void IsMaxDeliveries_returns_true_when_delivery_count_meets_threshold()
    {
        var tracker = new RedeliveryTracker([100]);

        tracker.Schedule(seq: 7, deliveryCount: 3);

        tracker.IsMaxDeliveries(7, maxDeliver: 3).ShouldBeTrue();
        tracker.IsMaxDeliveries(7, maxDeliver: 4).ShouldBeFalse();
        tracker.IsMaxDeliveries(7, maxDeliver: 2).ShouldBeTrue();
    }

    // -------------------------------------------------------------------------
    // Test 5 — IsMaxDeliveries returns false when maxDeliver is 0 (unlimited)
    //
    // Go reference: consumer.go — MaxDeliver <= 0 means unlimited redeliveries.
    // -------------------------------------------------------------------------
    [Fact]
    public void IsMaxDeliveries_returns_false_when_maxDeliver_is_zero()
    {
        var tracker = new RedeliveryTracker([100]);

        tracker.Schedule(seq: 99, deliveryCount: 1000);

        tracker.IsMaxDeliveries(99, maxDeliver: 0).ShouldBeFalse();
    }

    // -------------------------------------------------------------------------
    // Test 6 — Empty backoff falls back to ackWait
    //
    // Go reference: consumer.go — when BackOff is empty the ack-wait duration is
    // used as the redelivery delay.
    // -------------------------------------------------------------------------
    [Fact]
    public async Task Schedule_with_empty_backoff_falls_back_to_ackWait()
    {
        // Empty backoff array → fall back to ackWaitMs
        var tracker = new RedeliveryTracker([]);

        tracker.Schedule(seq: 1, deliveryCount: 1, ackWaitMs: 1);
        await Task.Delay(10);

        tracker.GetDue().ShouldContain(1UL);
    }

    // -------------------------------------------------------------------------
    // Test 7 — Empty backoff with large ackWait does not expire prematurely
    // -------------------------------------------------------------------------
    [Fact]
    public void Schedule_with_empty_backoff_and_large_ackWait_does_not_expire()
    {
        var tracker = new RedeliveryTracker([]);

        tracker.Schedule(seq: 2, deliveryCount: 1, ackWaitMs: 5000);

        tracker.GetDue().ShouldNotContain(2UL);
    }

    // -------------------------------------------------------------------------
    // Test 8 — Schedule returns the deadline UTC time
    //
    // Go reference: consumer.go:5540 — trackPending stores the computed deadline.
    // -------------------------------------------------------------------------
    [Fact]
    public void Schedule_returns_deadline_in_the_future()
    {
        var tracker = new RedeliveryTracker([100]);

        var before = DateTime.UtcNow;
        var deadline = tracker.Schedule(seq: 3, deliveryCount: 1);
        var after = DateTime.UtcNow;

        deadline.ShouldBeGreaterThanOrEqualTo(before);
        // Deadline should be ahead of scheduling time by at least the backoff value
        (deadline - after).TotalMilliseconds.ShouldBeGreaterThan(0);
    }

    // -------------------------------------------------------------------------
    // Test 9 — Multiple sequences tracked independently
    // -------------------------------------------------------------------------
    [Fact]
    public async Task Multiple_sequences_are_tracked_independently()
    {
        var tracker = new RedeliveryTracker([1, 5000]);

        tracker.Schedule(seq: 1, deliveryCount: 1);   // 1ms → expires soon
        tracker.Schedule(seq: 2, deliveryCount: 2);   // 5000ms → won't expire

        tracker.TrackedCount.ShouldBe(2);

        await Task.Delay(15);

        var due = tracker.GetDue();
        due.ShouldContain(1UL);
        due.ShouldNotContain(2UL);

        tracker.Acknowledge(1);
        tracker.TrackedCount.ShouldBe(1);
    }

    // -------------------------------------------------------------------------
    // Test 10 — IsMaxDeliveries returns false for untracked sequence
    // -------------------------------------------------------------------------
    [Fact]
    public void IsMaxDeliveries_returns_false_for_untracked_sequence()
    {
        var tracker = new RedeliveryTracker([100]);

        tracker.IsMaxDeliveries(999, maxDeliver: 1).ShouldBeFalse();
    }
}
