package backoff

import (
	"fmt"
	"math"
	"testing"
	"time"
)

// If given New with 0's and no jitter, ensure that certain invariants are met:
//
//   - the default max duration and interval should be used
//   - noJitter should be true
//   - the RNG should not be initialised
//   - the first duration should be equal to the default interval
func TestDefaults(t *testing.T) {
	b := NewWithoutJitter(0, 0)

	if b.maxDuration != DefaultMaxDuration {
		t.Fatalf("expected new backoff to use the default max duration (%s), but have %s", DefaultMaxDuration, b.maxDuration)
	}

	if b.interval != DefaultInterval {
		t.Fatalf("exepcted new backoff to use the default interval (%s), but have %s", DefaultInterval, b.interval)
	}

	if b.noJitter != true {
		t.Fatal("backoff should have been initialised without jitter")
	}

	dur := b.Duration()
	if dur != DefaultInterval {
		t.Fatalf("expected first duration to be %s, have %s", DefaultInterval, dur)
	}
}

// Given a zero-value initialised Backoff, it should be transparently
// setup.
func TestSetup(t *testing.T) {
	b := new(Backoff)
	dur := b.Duration()
	if dur < 0 || dur > (5*time.Minute) {
		t.Fatalf("want duration between 0 and 5 minutes, have %s", dur)
	}
}

// Ensure that tries incremenets as expected.
func TestTries(t *testing.T) {
	b := NewWithoutJitter(5, 1)

	for i := uint64(0); i < 3; i++ {
		if b.n != i {
			t.Fatalf("want tries=%d, have tries=%d", i, b.n)
		}

		pow := 1 << i
		expected := time.Duration(pow)
		dur := b.Duration()
		if dur != expected {
			t.Fatalf("want duration=%d, have duration=%d at i=%d", expected, dur, i)
		}
	}

	for i := uint(3); i < 5; i++ {
		dur := b.Duration()
		if dur != 5 {
			t.Fatalf("want duration=5, have %d at i=%d", dur, i)
		}
	}
}

// Ensure that a call to Reset will actually reset the Backoff.
func TestReset(t *testing.T) {
	const iter = 10
	b := New(1000, 1)
	for i := 0; i < iter; i++ {
		_ = b.Duration()
	}

	if b.n != iter {
		t.Fatalf("expected tries=%d, have tries=%d", iter, b.n)
	}

	b.Reset()
	if b.n != 0 {
		t.Fatalf("expected tries=0 after reset, have tries=%d", b.n)
	}
}

const decay = 5 * time.Millisecond
const max = 10 * time.Millisecond
const interval = time.Millisecond

func TestDecay(t *testing.T) {
	const iter = 10

	b := NewWithoutJitter(max, 1)
	b.SetDecay(decay)

	var backoff time.Duration
	for i := 0; i < iter; i++ {
		backoff = b.Duration()
	}

	if b.n != iter {
		t.Fatalf("expected tries=%d, have tries=%d", iter, b.n)
	}

	// Don't decay below backoff
	b.lastTry = time.Now().Add(-backoff + 1)
	backoff = b.Duration()
	if b.n != iter+1 {
		t.Fatalf("expected tries=%d, have tries=%d", iter+1, b.n)
	}

	// Reset after backoff + decay
	b.lastTry = time.Now().Add(-backoff - decay)
	b.Duration()
	if b.n != 1 {
		t.Fatalf("expected tries=%d, have tries=%d", 1, b.n)
	}
}

// Ensure that decay works even if the retry counter is saturated.
func TestDecaySaturation(t *testing.T) {
	b := NewWithoutJitter(1<<2, 1)
	b.SetDecay(decay)

	var duration time.Duration
	for i := 0; i <= 2; i++ {
		duration = b.Duration()
	}

	if duration != 1<<2 {
		t.Fatalf("expected duration=%v, have duration=%v", 1<<2, duration)
	}

	b.lastTry = time.Now().Add(-duration - decay)
	b.n = math.MaxUint64

	duration = b.Duration()
	if duration != 1 {
		t.Errorf("expected duration=%v, have duration=%v", 1, duration)
	}
}

func ExampleBackoff_SetDecay() {
	b := NewWithoutJitter(max, interval)
	b.SetDecay(decay)

	// try 0
	fmt.Println(b.Duration())

	// try 1
	fmt.Println(b.Duration())

	// try 2
	duration := b.Duration()
	fmt.Println(duration)

	// try 3, below decay
	time.Sleep(duration)
	duration = b.Duration()
	fmt.Println(duration)

	// try 4, resets
	time.Sleep(duration + decay)
	fmt.Println(b.Duration())

	// Output: 1ms
	// 2ms
	// 4ms
	// 8ms
	// 1ms
}
