package push

import (
	"testing"
	"time"

	pushv1 "galaxy/backend/proto/push/v1"

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

func mkEvent(cursor uint64, label string) *pushv1.PushEvent {
	return &pushv1.PushEvent{
		Cursor: formatCursor(cursor),
		Kind: &pushv1.PushEvent_ClientEvent{
			ClientEvent: &pushv1.ClientEvent{
				Kind:    label,
				Payload: []byte(label),
			},
		},
	}
}

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

	now := time.Unix(1_700_000_000, 0)
	r := newRing(8, time.Minute)
	for i := uint64(1); i <= 5; i++ {
		r.append(i, mkEvent(i, "e"), now)
	}

	got, stale := r.since(2, now)
	require.False(t, stale)
	require.Len(t, got, 3)
	assert.Equal(t, formatCursor(3), got[0].Cursor)
	assert.Equal(t, formatCursor(4), got[1].Cursor)
	assert.Equal(t, formatCursor(5), got[2].Cursor)
}

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

	now := time.Unix(1_700_000_000, 0)
	r := newRing(8, time.Minute)
	for i := uint64(1); i <= 3; i++ {
		r.append(i, mkEvent(i, "e"), now)
	}

	got, stale := r.since(3, now)
	require.False(t, stale)
	assert.Empty(t, got)

	got, stale = r.since(99, now)
	require.False(t, stale)
	assert.Empty(t, got)
}

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

	now := time.Unix(1_700_000_000, 0)
	r := newRing(3, time.Minute)
	for i := uint64(1); i <= 5; i++ {
		r.append(i, mkEvent(i, "e"), now)
	}
	// Capacity=3 means cursors 1 and 2 were evicted.
	require.Equal(t, 3, r.len())

	got, stale := r.since(1, now)
	assert.True(t, stale)
	assert.Empty(t, got)

	got, stale = r.since(2, now)
	assert.False(t, stale)
	require.Len(t, got, 3)
	assert.Equal(t, formatCursor(3), got[0].Cursor)
}

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

	t0 := time.Unix(1_700_000_000, 0)
	r := newRing(8, 10*time.Second)
	r.append(1, mkEvent(1, "e"), t0)
	r.append(2, mkEvent(2, "e"), t0.Add(2*time.Second))
	r.append(3, mkEvent(3, "e"), t0.Add(15*time.Second))

	// At t0+13s the first two entries are past their 10s TTL but the
	// third (added at t0+15s) is still within the freshness window.
	got, stale := r.since(0, t0.Add(13*time.Second))
	assert.True(t, stale)
	assert.Empty(t, got)
	assert.Equal(t, 1, r.len())
}

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

	r := newRing(4, time.Minute)
	got, stale := r.since(42, time.Now())
	assert.False(t, stale)
	assert.Empty(t, got)
}
