package run

import (
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func timePtr(value time.Time) *time.Time { return &value }

func TestRecoveryAttemptDelay(t *testing.T) {
	cases := []struct {
		k    int
		want time.Duration
	}{
		{0, 0},
		{1, time.Minute},
		{2, 3 * time.Minute},
		{3, 7 * time.Minute},
		{4, 15 * time.Minute},
		{5, 31 * time.Minute},
	}
	for _, tc := range cases {
		require.Equal(t, tc.want, recoveryAttemptDelay(tc.k), "attempt delay for k=%d", tc.k)
	}
}

func TestNextRecoveryStepScheduleArithmetic(t *testing.T) {
	base := time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC)

	t.Run("first non-working observation anchors the schedule without sending", func(t *testing.T) {
		step := nextRecoveryStep(base, nil, 0, nil, false)
		require.False(t, step.send)
		require.False(t, step.working)
		require.NotNil(t, step.firstNonWorkingAt)
		require.Equal(t, base, *step.firstNonWorkingAt)
		require.Equal(t, 0, step.attemptCount)
		require.NotNil(t, step.nextRecoveryAt)
		require.Equal(t, base.Add(time.Minute), *step.nextRecoveryAt)
	})

	t.Run("working returns state unchanged and never starts timing", func(t *testing.T) {
		step := nextRecoveryStep(base, nil, 0, nil, true)
		require.True(t, step.working)
		require.False(t, step.send)
		require.Nil(t, step.firstNonWorkingAt)
		require.Nil(t, step.nextRecoveryAt)
		require.Equal(t, 0, step.attemptCount)
	})

	t.Run("sends at cumulative 1/3/7/15/31 minutes", func(t *testing.T) {
		anchor := base
		// The first non-working observation anchors the schedule and arms the
		// first send at +1 without firing.
		observed := nextRecoveryStep(anchor, nil, 0, nil, false)
		require.False(t, observed.send)
		require.Equal(t, anchor.Add(time.Minute), *observed.nextRecoveryAt)
		next := observed.nextRecoveryAt
		attempts := 0
		for _, wantOffset := range []time.Duration{
			time.Minute, 3 * time.Minute, 7 * time.Minute, 15 * time.Minute, 31 * time.Minute,
		} {
			// Just before the scheduled slot nothing fires and nothing advances.
			before := anchor.Add(wantOffset - time.Second)
			step := nextRecoveryStep(before, &anchor, attempts, next, false)
			require.False(t, step.send, "no send before the scheduled slot at %s", before)
			require.Equal(t, attempts, step.attemptCount, "attempt counter must not advance before a send")

			// At the scheduled slot the send fires exactly once.
			at := anchor.Add(wantOffset)
			step = nextRecoveryStep(at, &anchor, attempts, next, false)
			require.True(t, step.send, "send expected at %s", at)
			require.Equal(t, attempts+1, step.attemptIndex)
			require.Equal(t, attempts+1, step.attemptCount)
			next = step.nextRecoveryAt
			attempts++
		}
		// After the 5th send the cap is reached: no further auto-wake.
		after := anchor.Add(63 * time.Minute)
		step := nextRecoveryStep(after, &anchor, attempts, next, false)
		require.False(t, step.send)
		require.Equal(t, maxRecoveryAttempts, step.attemptCount)
	})

	t.Run("working flap does not reset the counter or rewind the schedule", func(t *testing.T) {
		anchor := base
		// Establish the anchor, then send attempts 1 and 2 at +1 and +3.
		observed := nextRecoveryStep(anchor, nil, 0, nil, false)
		first := nextRecoveryStep(anchor.Add(time.Minute), observed.firstNonWorkingAt, 0, observed.nextRecoveryAt, false)
		require.True(t, first.send)
		require.Equal(t, 1, first.attemptCount)
		require.Equal(t, anchor.Add(3*time.Minute), *first.nextRecoveryAt)
		second := nextRecoveryStep(anchor.Add(3*time.Minute), &anchor, 1, first.nextRecoveryAt, false)
		require.True(t, second.send)
		require.Equal(t, 2, second.attemptCount)
		require.Equal(t, anchor.Add(7*time.Minute), *second.nextRecoveryAt)

		// The responder returns working: state unchanged, no reset, no rewind.
		working := nextRecoveryStep(anchor.Add(4*time.Minute), &anchor, 2, second.nextRecoveryAt, true)
		require.False(t, working.send)
		require.Equal(t, 2, working.attemptCount, "counter must not reset on re-working")
		require.Equal(t, anchor, *working.firstNonWorkingAt, "anchor must never move")
		require.Equal(t, anchor.Add(7*time.Minute), *working.nextRecoveryAt, "schedule must not rewind")

		// Non-working again before the next slot: still nothing.
		early := nextRecoveryStep(anchor.Add(5*time.Minute), &anchor, 2, second.nextRecoveryAt, false)
		require.False(t, early.send)
		require.Equal(t, 2, early.attemptCount)

		// At the 3rd slot (+7) the 3rd send fires with the SAME anchor.
		third := nextRecoveryStep(anchor.Add(7*time.Minute), &anchor, 2, second.nextRecoveryAt, false)
		require.True(t, third.send)
		require.Equal(t, 3, third.attemptIndex)
		require.Equal(t, anchor, *third.firstNonWorkingAt)
	})

	t.Run("a missed slot while working fires on the first non-working tick after it", func(t *testing.T) {
		anchor := base
		// First non-working observation at +1 arms attempt 1 at +2.
		observed := nextRecoveryStep(anchor.Add(time.Minute), nil, 0, nil, false)
		require.Equal(t, anchor.Add(2*time.Minute), *observed.nextRecoveryAt)
		// The effect returns working at +1.5, past the +2 slot deadline... actually
		// the slot is at +2; the effect works through +2 and +2.5.
		working := nextRecoveryStep(anchor.Add(2*time.Minute), observed.firstNonWorkingAt, 0, observed.nextRecoveryAt, true)
		require.False(t, working.send)
		// It returns non-working at +2.5: the overdue slot fires immediately.
		overdue := nextRecoveryStep(anchor.Add(150*time.Second), observed.firstNonWorkingAt, 0, observed.nextRecoveryAt, false)
		require.True(t, overdue.send)
		require.Equal(t, 1, overdue.attemptIndex)
	})

	t.Run("nil nextRecoveryAt with an anchor is a defensive no-op", func(t *testing.T) {
		anchor := base
		step := nextRecoveryStep(base.Add(10*time.Minute), &anchor, 2, nil, false)
		require.False(t, step.send)
		require.Equal(t, 2, step.attemptCount)
	})
}
