package retry

import (
	"context"
	"errors"
	"fmt"
	"testing"
	"time"

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

func TestDo(t *testing.T) {
	cases := []struct {
		name        string
		maxAttempts int
		behavior    func(call int) error
		wantErr     bool
		wantCalls   int
	}{
		{"SucceedsFirstTry", 3, func(int) error { return nil }, false, 1},
		{"RetriesOnTransient", 3, func(c int) error {
			if c < 3 {
				return fmt.Errorf("blip: %w", ErrTransient)
			}
			return nil
		}, false, 3},
		{"StopsOnPermanent", 5, func(int) error {
			return fmt.Errorf("bad: %w", ErrPermanent)
		}, true, 1},
		{"UnclassifiedIsPermanent", 5, func(int) error {
			return errors.New("anything")
		}, true, 1},
		{"ExhaustsAttempts", 3, func(int) error {
			return fmt.Errorf("still bad: %w", ErrTransient)
		}, true, 3},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			calls := 0
			err := Do(context.Background(), Policy{MaxAttempts: tc.maxAttempts}, func() error {
				calls++
				return tc.behavior(calls)
			})
			if tc.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tc.wantCalls, calls)
		})
	}
}

func TestDoClampsMaxAttemptsBelowOne(t *testing.T) {
	calls := 0
	err := Do(context.Background(), Policy{MaxAttempts: 0}, func() error {
		calls++
		return nil
	})
	assert.NoError(t, err)
	assert.Equal(t, 1, calls)
}

func TestDoRespectsContextCancellation(t *testing.T) {
	ctx, cancel := context.WithCancel(t.Context())
	attempted := make(chan struct{})
	result := make(chan error, 1)
	policy := Policy{
		MaxAttempts: 5,
		Backoff:     func(int) time.Duration { return 5 * time.Second },
	}
	go func() {
		result <- Do(ctx, policy, func() error {
			close(attempted)
			return fmt.Errorf("transient: %w", ErrTransient)
		})
	}()

	<-attempted
	cancel()
	err := <-result
	assert.ErrorIs(t, err, context.Canceled)
}

type fakeErr struct{ transient bool }

func (e *fakeErr) Error() string { return "fake" }
func (e *fakeErr) Is(target error) bool {
	return target == ErrTransient && e.transient
}

func TestDoUsesIsMethod(t *testing.T) {
	calls := 0
	err := Do(context.Background(), Policy{MaxAttempts: 3}, func() error {
		calls++
		if calls < 2 {
			return &fakeErr{transient: true}
		}
		return nil
	})
	assert.NoError(t, err)
	assert.Equal(t, 2, calls)
}

func TestExponentialBackoff(t *testing.T) {
	b := ExponentialBackoff(time.Second, 30*time.Second)
	f := func(attempt int, want time.Duration) {
		t.Helper()
		assert.Equal(t, want, b(attempt), "attempt %d", attempt)
	}
	f(1, time.Second)
	f(2, 2*time.Second)
	f(3, 4*time.Second)
	f(4, 8*time.Second)
	f(10, 30*time.Second) // capped
	f(0, time.Second)     // clamp to attempt=1
}
