package ring

import (
	"slices"
	"testing"

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

func ints(n int) []int {
	ret := make([]int, n)
	for i := range ret {
		ret[i] = i + 1
	}
	return ret
}

func TestRing(t *testing.T) {
	t.Parallel()
	const (
		dLen     = 10
		dHalfLen = dLen / 2
	)
	data := ints(dLen)
	lData := slices.Clone(data[:dHalfLen])
	rData := slices.Clone(data[dHalfLen:])

	require.NotPanics(t, func() {
		new(Ring[int]).WriteStats(nil)
	}, "WriteStats should be panic free")

	t.Run("basic enqueue and dequeue - no min, no max", func(t *testing.T) {
		t.Parallel()
		q, expected := new(Ring[int]), new(Ring[int])

		enq(t, q, data...)
		expected.len = dLen
		expected.stats.Enqueued = dLen
		expected.buf = data
		ringEq(t, expected, q)

		deq(t, q, lData...)
		expected.back = dHalfLen
		expected.len = dHalfLen
		expected.stats.Dequeued = dHalfLen
		expected.buf = append(make([]int, dHalfLen), rData...)
		ringEq(t, expected, q)

		enq(t, q, data...)
		expected.back = 0
		expected.len = dLen + dHalfLen
		expected.stats.Enqueued += dLen
		expected.buf = append(rData, data...)
		ringEq(t, expected, q)

		deqAll(t, q, append(rData, data...)...)
		expected.len = 0
		expected.stats.Dequeued += dLen + dHalfLen
		expected.buf = []int{}
		ringEq(t, expected, q)

		enq(t, q, data...)
		expected.len = dLen
		expected.stats.Enqueued += dLen
		expected.buf = data
		ringEq(t, expected, q)

		clearRing(t, q)
		expected.len = 0
		expected.stats.Dequeued += dLen
		expected.buf = []int{}
		ringEq(t, expected, q)
	})

	t.Run("enqueue, dequeue, grow and shrink - no min, yes max", func(t *testing.T) {
		t.Parallel()
		q, expected := new(Ring[int]), new(Ring[int])
		q.Max = dLen

		// basic wrap and overwrite
		enq(t, q, lData...)
		enq(t, q, data...)
		enq(t, q, data...)
		expected.back = dHalfLen
		expected.buf = append(rData, lData...)
		expected.len = dLen
		expected.stats.Enqueued = 2*dLen + dHalfLen
		expected.stats.Dropped = dLen + dHalfLen
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())

		// can't allocate past max and cannot shrink because we're at capacity
		q.Grow(3 * dLen)
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())

		q.Shrink(2 * dLen)
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())

		// remove some items and play with extra space
		deq(t, q, lData...)
		expected.back = 0
		expected.buf = rData
		expected.len -= dHalfLen
		expected.stats.Dequeued = dHalfLen
		require.Equal(t, dLen, q.Cap())
		ringEq(t, expected, q)

		q.Shrink(1)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen+1, q.Cap())

		q.Grow(2)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen+2, q.Cap())

		q.Grow(dLen)
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())
	})

	t.Run("enqueue, dequeue, grow and shrink - yes min, no max", func(t *testing.T) {
		t.Parallel()
		q, expected := new(Ring[int]), new(Ring[int])
		q.Min = dHalfLen

		// enqueueing one item should allocate Min
		enq(t, q, 1)
		expected.buf = []int{1}
		expected.len = 1
		expected.stats.Enqueued = 1
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// clearing should not migrate now
		clearRing(t, q)
		expected.buf = []int{}
		expected.len = 0
		expected.stats.Dequeued = 1
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// enqueue some data
		enq(t, q, data...)
		expected.buf = data
		expected.len = dLen
		expected.stats.Enqueued += dLen
		ringEq(t, expected, q)
		require.GreaterOrEqual(t, q.Cap(), dLen)

		// now clearing should migrate and move to a slice of Min length
		clearRing(t, q)
		expected.buf = []int{}
		expected.len = 0
		expected.stats.Dequeued += dLen
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// we shouldn't be able to shrink past Min, but it shouldn't allocate a
		// greater slice either because it's purpose is to reduce allocated
		// memory if possible
		q.Min = dLen
		q.Shrink(dHalfLen)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// dequeueing shouldn't allocate either, just in case
		require.Zero(t, q.Dequeue())
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// enqueueing one item allocates again to Min, which is now greater than
		// before
		enq(t, q, 1)
		expected.buf = []int{1}
		expected.len = 1
		expected.stats.Enqueued += 1
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())

		// we reduce Min again, then we should be able to shrink as well
		q.Min = dHalfLen
		q.Shrink(dHalfLen)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen+1, q.Cap())
		q.Shrink(1)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())
		q.Shrink(0)
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// enqueue a lot and then dequeue all, we should still see Min cap
		enq(t, q, data...)
		expected.buf = append(expected.buf, data...)
		expected.len += dLen
		expected.stats.Enqueued += dLen
		ringEq(t, expected, q)
		require.GreaterOrEqual(t, q.Cap(), dLen+1)

		deqAll(t, q, expected.buf...)
		expected.buf = []int{}
		expected.len = 0
		expected.stats.Dequeued += dLen + 1
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())
	})

	t.Run("enqueue, dequeue, grow and shrink - yes min, yes max", func(t *testing.T) {
		t.Parallel()
		q, expected := new(Ring[int]), new(Ring[int])
		q.Min, q.Max = dHalfLen, dLen

		// single enqueueing should allocate for Min
		enq(t, q, 1)
		expected.buf = []int{1}
		expected.len = 1
		expected.stats.Enqueued = 1
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())

		// enqueue a lot until we overwrite the first item
		enq(t, q, data...)
		expected.back = 1
		expected.buf = append(data[dLen-1:], data[:dLen-1]...)
		expected.len = dLen
		expected.stats.Enqueued += dLen
		expected.stats.Dropped = 1
		ringEq(t, expected, q)
		require.Equal(t, dLen, q.Cap())

		// clearing should bring us back to Min alloc
		clearRing(t, q)
		expected.back = 0
		expected.buf = expected.buf[:0]
		expected.len = 0
		expected.stats.Dequeued += dLen
		ringEq(t, expected, q)
		require.Equal(t, dHalfLen, q.Cap())
	})

	t.Run("growing and shrinking invariants - no min, no max", func(t *testing.T) {
		t.Parallel()
		q, expected := new(Ring[int]), new(Ring[int])

		// dummy grow and shrink
		q.Grow(0)
		require.Equal(t, 0, q.Cap())
		ringEq(t, expected, q)

		q.Shrink(0)
		require.Equal(t, 0, q.Cap())
		ringEq(t, expected, q)

		// add 3*dLen and leave 2*dLen
		q.Grow(3 * dLen)
		expected.buf = []int{}
		require.Equal(t, 3*dLen, q.Cap())
		ringEq(t, expected, q)

		q.Shrink(2 * dLen)
		require.Equal(t, 2*dLen, q.Cap())
		ringEq(t, expected, q)

		// add dLen items and play with cap
		enq(t, q, data...)
		expected.buf = data
		expected.len = dLen
		expected.stats.Enqueued = dLen
		require.Equal(t, 2*dLen, q.Cap())
		ringEq(t, expected, q)

		q.Grow(2 * dLen)
		require.GreaterOrEqual(t, q.Cap(), 3*dLen)
		ringEq(t, expected, q)

		q.Shrink(0)
		require.Equal(t, dLen, q.Cap())
		ringEq(t, expected, q)

		// remove all items and shrink to zero
		deqAll(t, q, data...)
		expected.buf = []int{}
		expected.len = 0
		expected.stats.Dequeued = dLen
		require.Equal(t, dLen, q.Cap())
		ringEq(t, expected, q)

		q.Shrink(0)
		expected.buf = nil
		require.Equal(t, 0, q.Cap())
		ringEq(t, expected, q)
	})
}

// enq enqueues the given items into the given Ring.
func enq[T any](t *testing.T, q *Ring[T], s ...T) {
	t.Helper()

	initLen := q.Len()
	initCap := q.Cap()
	for _, v := range s {
		require.NotPanics(t, func() {
			q.Enqueue(v)
		})
	}

	expectedLen := initLen + len(s)
	if q.Max > 0 {
		expectedMax := max(initCap, q.Max)
		expectedLen = min(expectedLen, expectedMax)
	}
	require.Equal(t, expectedLen, q.Len())
}

// deq dequeues len(expected) items from the given Ring and compares them to
// expected. Ring should have at least len(expected) items.
func deq[T any](t *testing.T, q *Ring[T], expected ...T) {
	t.Helper()

	if q.Cap() == 0 {
		require.Nil(t, q.buf)       // internal state
		require.Equal(t, 0, q.back) // internal state
		return
	}

	oldLen := q.Len()
	require.True(t, oldLen >= len(expected))
	got := make([]T, len(expected))
	for i := range got {
		var val T
		require.NotPanics(t, func() {
			prePeekLen := q.Len()
			val = q.Peek()
			require.Equal(t, prePeekLen, q.Len())
			got[i] = q.Dequeue()
		})
		require.Equal(t, val, got[i])
	}

	require.Equal(t, expected, got)
	require.Equal(t, oldLen-len(expected), q.Len())
}

// clearRing calls Clear on the given Ring and performs a set of assertions that
// should be satisfied afterwards.
func clearRing[T any](t *testing.T, q *Ring[T]) {
	t.Helper()

	var expectedBuf []T
	if clearShouldMigrate(q.Cap(), q.Min, q.Max) {
		expectedBuf = make([]T, q.Min)
	} else {
		expectedBuf = make([]T, q.Cap())
	}

	require.NotPanics(t, func() {
		q.Clear()
	})
	require.Equal(t, expectedBuf, q.buf) // internal state
	require.Equal(t, 0, q.Len())
	require.Equal(t, 0, q.back) // internal state

	// dequeueing should yield zero values
	var zero T
	for i := 0; i < 10; i++ {
		var val1, val2 T
		require.NotPanics(t, func() {
			val1 = q.Peek()
			val1 = q.Dequeue()
		})
		require.Equal(t, zero, val1)
		require.Equal(t, zero, val2)
	}
}

// deqAll depletes the given Ring and compares the dequeued items to those
// provided.
func deqAll[T any](t *testing.T, q *Ring[T], expected ...T) {
	t.Helper()

	deq[T](t, q, expected...)

	zeroS := make([]T, q.Cap())
	require.Equal(t, zeroS, q.buf) // internal state

	require.Equal(t, 0, q.Len())

	// dequeueing further should yield zero values when empty
	var zero T
	for i := 0; i < 10; i++ {
		var val1, val2 T
		require.NotPanics(t, func() {
			val1 = q.Peek()
			val2 = q.Dequeue()
		})
		require.Equal(t, zero, val1)
		require.Equal(t, zero, val2)
	}

	clearRing(t, q)
}

// ringEq tests that the given Rings are the same in many aspects. The following
// are the things that are not checked:
//   - The values of Min and Max, since the code does not programmatically
//     channge them
//   - Allocation numbers (Cap, Grown, Shrunk, Allocs)
//   - The free capacity to the right of `got`
func ringEq[T any](t *testing.T, expected, got *Ring[T]) {
	t.Helper()

	var expStats, gotStats RingStats
	require.NotPanics(t, func() {
		expected.WriteStats(&expStats)
		got.WriteStats(&gotStats)
	})

	// capacity and allocations are to be tested separately
	removeAllocStats(&expStats)
	removeAllocStats(&gotStats)

	require.Equal(t, expStats, gotStats, "expStats == gotStats")

	// internal state
	require.Equal(t, expected.back, got.back, "expected.back == got.back")
	// only check for used capacity
	require.Equal(t, expected.buf, got.buf[:min(got.back+got.len, len(got.buf))],
		"expected.buf == got.buf[:min(got.back+got.len, len(got.s))]")
}

func removeAllocStats(s *RingStats) {
	s.Cap = 0
	s.Grown = 0
	s.Shrunk = 0
	s.Allocs = 0
}

func TestMinMaxValidity(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		Min, Max               int
		minIsValid, maxIsValid bool
	}{
		{Min: 0, Max: 0, minIsValid: false, maxIsValid: false},
		{Min: 0, Max: 1, minIsValid: false, maxIsValid: true},
		{Min: 0, Max: 2, minIsValid: false, maxIsValid: true},
		{Min: 1, Max: 0, minIsValid: true, maxIsValid: false},
		{Min: 1, Max: 1, minIsValid: true, maxIsValid: true},
		{Min: 1, Max: 2, minIsValid: true, maxIsValid: true},
		{Min: 2, Max: 0, minIsValid: true, maxIsValid: false},
		{Min: 2, Max: 1, minIsValid: false, maxIsValid: false},
		{Min: 2, Max: 2, minIsValid: true, maxIsValid: true},
	}

	for i, tc := range testCases {
		gotMinIsValid := minIsValid(tc.Min, tc.Max)
		require.Equal(t, tc.minIsValid, gotMinIsValid,
			"test index %d; test data: %#v", i, tc)

		gotMaxIsValid := maxIsValid(tc.Min, tc.Max)
		require.Equal(t, tc.maxIsValid, gotMaxIsValid,
			"test index %d; test data: %#v", i, tc)
	}
}

func TestClearShouldMigrate(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		// we don't need to include Max in the test, we just disable it by
		// passing zero because Max is only needed to establish the validity of
		// Min. The validity of Min wrt Max is already covered in the test for
		// minIsValid, and once Min is valid Max has no impact on the outcome of
		// clearShouldMigrate.
		CurCap, Min int
		expected    bool
	}{
		{CurCap: 0, Min: 0, expected: false},
		{CurCap: 0, Min: 9, expected: false},
		{CurCap: 0, Min: 10, expected: false},
		{CurCap: 0, Min: 11, expected: false},
		{CurCap: 10, Min: 0, expected: false},
		{CurCap: 10, Min: 9, expected: true},
		{CurCap: 10, Min: 10, expected: false},
		{CurCap: 10, Min: 11, expected: false},
	}

	for i, tc := range testCases {
		got := clearShouldMigrate(tc.CurCap, tc.Min, 0)
		require.Equal(t, tc.expected, got,
			"test index %d; test data: %#v", i, tc)
	}
}

func TestFixAllocSize(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		CurLen, Min, Max, NewCap, expected int
	}{
		// we don't need to add test cases for odd configurations of Min and Max
		// not being valid for different reasons because that is already covered
		// in the unit tests for minIsValid and maxIsValid. It suffices to
		// provide a zero for Min or Max to disable their respective behaviour

		{CurLen: 0, Min: 0, Max: 0, NewCap: 0, expected: 0},
		{CurLen: 0, Min: 0, Max: 0, NewCap: 5, expected: 5},

		{CurLen: 0, Min: 0, Max: 10, NewCap: 0, expected: 0},
		{CurLen: 0, Min: 0, Max: 10, NewCap: 9, expected: 9},
		{CurLen: 0, Min: 0, Max: 10, NewCap: 10, expected: 10},
		{CurLen: 0, Min: 0, Max: 10, NewCap: 11, expected: 10},

		{CurLen: 0, Min: 10, Max: 0, NewCap: 0, expected: 10},
		{CurLen: 0, Min: 10, Max: 0, NewCap: 5, expected: 10},
		{CurLen: 0, Min: 10, Max: 0, NewCap: 9, expected: 10},
		{CurLen: 0, Min: 10, Max: 0, NewCap: 10, expected: 10},
		{CurLen: 0, Min: 10, Max: 0, NewCap: 11, expected: 11},

		{CurLen: 0, Min: 10, Max: 10, NewCap: 0, expected: 10},
		{CurLen: 0, Min: 10, Max: 10, NewCap: 5, expected: 10},
		{CurLen: 0, Min: 10, Max: 10, NewCap: 9, expected: 10},
		{CurLen: 0, Min: 10, Max: 10, NewCap: 10, expected: 10},
		{CurLen: 0, Min: 10, Max: 10, NewCap: 11, expected: 10},

		{CurLen: 0, Min: 10, Max: 20, NewCap: 0, expected: 10},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 5, expected: 10},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 9, expected: 10},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 10, expected: 10},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 19, expected: 19},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 20, expected: 20},
		{CurLen: 0, Min: 10, Max: 20, NewCap: 21, expected: 20},

		{CurLen: 5, Min: 0, Max: 0, NewCap: 0, expected: 5},
		{CurLen: 5, Min: 0, Max: 0, NewCap: 5, expected: 5},
		{CurLen: 5, Min: 0, Max: 0, NewCap: 10, expected: 10},

		{CurLen: 5, Min: 0, Max: 10, NewCap: 0, expected: 5},
		{CurLen: 5, Min: 0, Max: 10, NewCap: 5, expected: 5},
		{CurLen: 5, Min: 0, Max: 10, NewCap: 9, expected: 9},
		{CurLen: 5, Min: 0, Max: 10, NewCap: 10, expected: 10},
		{CurLen: 5, Min: 0, Max: 10, NewCap: 11, expected: 10},

		{CurLen: 5, Min: 10, Max: 0, NewCap: 0, expected: 10},
		{CurLen: 5, Min: 10, Max: 0, NewCap: 5, expected: 10},
		{CurLen: 5, Min: 10, Max: 0, NewCap: 9, expected: 10},
		{CurLen: 5, Min: 10, Max: 0, NewCap: 10, expected: 10},
		{CurLen: 5, Min: 10, Max: 0, NewCap: 11, expected: 11},

		{CurLen: 5, Min: 10, Max: 10, NewCap: 0, expected: 10},
		{CurLen: 5, Min: 10, Max: 10, NewCap: 5, expected: 10},
		{CurLen: 5, Min: 10, Max: 10, NewCap: 9, expected: 10},
		{CurLen: 5, Min: 10, Max: 10, NewCap: 10, expected: 10},
		{CurLen: 5, Min: 10, Max: 10, NewCap: 11, expected: 10},

		{CurLen: 5, Min: 10, Max: 20, NewCap: 0, expected: 10},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 5, expected: 10},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 9, expected: 10},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 10, expected: 10},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 19, expected: 19},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 20, expected: 20},
		{CurLen: 5, Min: 10, Max: 20, NewCap: 21, expected: 20},
	}

	for i, tc := range testCases {
		got := fixAllocSize(tc.CurLen, tc.Min, tc.Max, tc.NewCap)
		require.Equal(t, tc.expected, got,
			"test index %d; test data %#v", i, tc)
	}
}
