package backoff

import (
	"math"
	"math/rand/v2"
	"strings"
	"testing"
	"time"
)

func TestConfigValidate(t *testing.T) {
	var tests = []struct {
		name        string
		cfg         Config
		shouldError bool
	}{
		{
			name:        "initial zero",
			cfg:         Config{Initial: 0, Max: time.Second},
			shouldError: true,
		},
		{
			name:        "initial negative",
			cfg:         Config{Initial: -1, Max: time.Second},
			shouldError: true,
		},
		{
			name: "max less than initial",
			cfg: Config{
				Initial: time.Second,
				Max:     500 * time.Millisecond,
			},
			shouldError: true,
		},
		{
			name: "max equals maxint64",
			cfg: Config{
				Initial: time.Second,
				Max:     math.MaxInt64,
			},
			shouldError: true,
		},
		{
			name: "valid",
			cfg: Config{
				Initial: time.Second,
				Max:     time.Second,
			},
			shouldError: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := tt.cfg.Validate()
			if tt.shouldError && err == nil {
				t.Fatalf("expected error, got nil")
			}
			if !tt.shouldError && err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
		})
	}
}

func TestNewInvalidConfig(t *testing.T) {
	_, err := New(Config{Initial: 0, Max: time.Second})
	if err == nil {
		t.Fatalf("expected error, got nil")
	}
	if !strings.Contains(err.Error(), "invalid config") {
		t.Fatalf("expected invalid config error, got %v", err)
	}
}

// TestDistribution uses a fixed seed to check the logic flow and jitter.
func TestDistribution(t *testing.T) {
	// RNG with seed (1, 2)
	rng := rand.New(rand.NewPCG(1, 2))

	initial := 100 * time.Millisecond
	maxDur := 400 * time.Millisecond

	b, _ := New(Config{
		Initial: initial,
		Max:     maxDur,
		RNG:     rng,
	})

	// Generate reference numbers using the same seed to predict outcomes.
	refRng := rand.New(rand.NewPCG(1, 2))

	expectedCaps := []time.Duration{
		100 * time.Millisecond,
		200 * time.Millisecond,
		400 * time.Millisecond,
		400 * time.Millisecond, // Clamped
	}

	for i, cap := range expectedCaps {
		// Expect Next to pick rand(0, cap).
		expected := time.Duration(refRng.Int64N(int64(cap) + 1))
		got := b.Next()

		if got != expected {
			t.Errorf(
				"step %d: expected %v, got %v (cap was %v)",
				i, expected, got, cap,
			)
		}
		if got > cap {
			t.Errorf(
				"step %d: got %v greater than cap %v",
				i, got, cap,
			)
		}
	}
}

// TestOverflowProtection ensures that large Max values do not cause overflow
// during the doubling phase.
func TestOverflowProtection(t *testing.T) {
	// Use a Max that is near MaxInt64.
	max := time.Duration(math.MaxInt64 - 1)
	start := max / 4

	b, err := New(Config{
		Initial: start,
		Max:     max,
	})
	if err != nil {
		t.Fatalf("New failed: %v", err)
	}

	// 1. Current = start.
	// Next returns [0, start]. state becomes start*2 (max/2).
	_ = b.Next()

	// 2. Current = max/2.
	// Next returns [0, max/2].
	_ = b.Next()

	// 3. Current = max.
	// Next returns [0, max].
	// Logic check: current >= max. state stays max.
	val := b.Next()
	if val < 0 {
		t.Errorf("got negative duration %v, likely overflow", val)
	}

	// Verify clamp.
	// No panic implies success.
	for range 5 {
		b.Next()
	}
}

func TestDefaultRNG(t *testing.T) {
	b, err := New(Config{
		Initial: time.Millisecond,
		Max:     10 * time.Millisecond,
	})
	if err != nil {
		t.Fatalf("New failed: %v", err)
	}
	got := b.Next()
	if got < 0 || got > time.Millisecond {
		t.Errorf("out of bounds: %v", got)
	}
}

func TestReset(t *testing.T) {
	b, _ := New(Config{
		Initial: 10 * time.Millisecond,
		Max:     100 * time.Millisecond,
	})

	// Advance state
	b.Next() // 10
	b.Next() // 20
	b.Next() // 40

	b.Reset()

	// Verify that the cap is reset by checking the bounds of the next
	// call. If reset works, the next call is bounded by Initial (10ms).
	for range 10 {
		got := b.Next()
		if got > 10*time.Millisecond {
			t.Fatalf(
				"call after Reset returned %v; expected <= 10ms",
				got,
			)
		}
		b.Reset()
	}
}
