package avg_sliding_window

import (
	"testing"
	"time"

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

func TestSlidingWindow_AddWithTime_Single(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 5)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 5.0, sw.Sum())
	require.Equal(t, 1, int(sw.Count()))
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 5.0, sw.buckets.Values()[0].(*bucket).sum)
}

func TestSlidingWindow_AddWithTime_TwoValues_SameBucket(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 5)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 10.0, sw.Sum())
	require.Equal(t, 2, int(sw.Count()))
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 2, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 10.0, sw.buckets.Values()[0].(*bucket).sum)
}

func TestSlidingWindow_AddWithTime_ThreeValues_SameBucket(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 4)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 6)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 15.0, sw.Sum())
	require.Equal(t, 3, int(sw.Count()))
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 15.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 3, int(sw.buckets.Values()[0].(*bucket).qty))
}

func TestSlidingWindow_AddWithTime_ThreeValues_ThreeBuckets(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:01"), 4)
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 6)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 15.0, sw.Sum())
	require.Equal(t, 3, int(sw.Count()))
	require.Equal(t, 3, sw.buckets.Size())
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 4.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 5.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[2].(*bucket).qty))
	require.Equal(t, 6.0, sw.buckets.Values()[2].(*bucket).sum)
}

func TestSlidingWindow_AddWithTime_OutWindow(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:03:55"), 1000)
	sw.AddWithTime(ts("2023-04-21 15:04:01"), 4)
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 6)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 15.0, sw.Sum())
	require.Equal(t, 3, int(sw.Count()))
	require.Equal(t, 3, sw.buckets.Size())
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 4.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 5.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[2].(*bucket).qty))
	require.Equal(t, 6.0, sw.buckets.Values()[2].(*bucket).sum)
}

func TestSlidingWindow_AdvanceClock(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:01"), 4)
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 6)
	require.Equal(t, 5.0, sw.Avg())
	require.Equal(t, 15.0, sw.Sum())
	require.Equal(t, 3, int(sw.Count()))
	require.Equal(t, 3, sw.buckets.Size())

	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 4.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 5.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[2].(*bucket).qty))
	require.Equal(t, 6.0, sw.buckets.Values()[2].(*bucket).sum)

	// up until 15:04:05 we had 3 buckets
	// let's advance the clock to 15:04:11 and the first data point should be evicted
	clock.Set(ts("2023-04-21 15:04:11"))
	require.Equal(t, 5.5, sw.Avg())
	require.Equal(t, 11.0, sw.Sum())
	require.Equal(t, 2, int(sw.Count()))
	require.Equal(t, 2, sw.buckets.Size())
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 5.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 6.0, sw.buckets.Values()[1].(*bucket).sum)

	// let's advance the clock to 15:04:12 and another data point should be evicted
	clock.Set(ts("2023-04-21 15:04:12"))
	require.Equal(t, 6.0, sw.Avg())
	require.Equal(t, 6.0, sw.Sum())
	require.Equal(t, 1, int(sw.Count()))
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 6.0, sw.buckets.Values()[0].(*bucket).sum)

	// let's advance the clock to 15:04:25 and all data point should be evicted
	clock.Set(ts("2023-04-21 15:04:25"))
	require.Equal(t, 0.0, sw.Avg())
	require.Equal(t, 0.0, sw.Sum())
	require.Equal(t, 0, int(sw.Count()))
	require.Equal(t, 0, sw.buckets.Size())
}

func TestSlidingWindow_MultipleValPerBucket(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(10*time.Second),
		WithBucketSize(time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:04:01"), 4)
	sw.AddWithTime(ts("2023-04-21 15:04:01"), 12)
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 5)
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 15)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 6)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 3)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 1)
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 3)
	require.Equal(t, 6.125, sw.Avg())
	require.Equal(t, 49.0, sw.Sum())
	require.Equal(t, 8, int(sw.Count()))
	require.Equal(t, 3, sw.buckets.Size())
	require.Equal(t, 2, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 16.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 2, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 20.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 4, int(sw.buckets.Values()[2].(*bucket).qty))
	require.Equal(t, 13.0, sw.buckets.Values()[2].(*bucket).sum)

	// up until 15:04:05 we had 3 buckets
	// let's advance the clock to 15:04:11 and the first data point should be evicted
	clock.Set(ts("2023-04-21 15:04:11"))
	require.Equal(t, 5.5, sw.Avg())
	require.Equal(t, 33.0, sw.Sum())
	require.Equal(t, 6, int(sw.Count()))
	require.Equal(t, 2, sw.buckets.Size())
	require.Equal(t, 2, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 20.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 4, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 13.0, sw.buckets.Values()[1].(*bucket).sum)

	// let's advance the clock to 15:04:12 and another data point should be evicted
	clock.Set(ts("2023-04-21 15:04:12"))
	require.Equal(t, 3.25, sw.Avg())
	require.Equal(t, 13.0, sw.Sum())
	require.Equal(t, 4, int(sw.Count()))
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 4, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 13.0, sw.buckets.Values()[0].(*bucket).sum)

	// let's advance the clock to 15:04:25 and all data point should be evicted
	clock.Set(ts("2023-04-21 15:04:25"))
	require.Equal(t, 0.0, sw.Avg())
	require.Equal(t, 0, sw.buckets.Size())
}

func TestSlidingWindow_CustomBucket(t *testing.T) {
	now := ts("2023-04-21 15:04:05")
	clock := NewAdjustableClock(now)

	sw := NewSlidingWindow(
		WithWindowLength(30*time.Second),
		WithBucketSize(10*time.Second),
		WithClock(clock))
	sw.AddWithTime(ts("2023-04-21 15:03:49"), 5)  // key: 03:50, sum: 5.0
	sw.AddWithTime(ts("2023-04-21 15:04:02"), 15) // key: 04:00
	sw.AddWithTime(ts("2023-04-21 15:04:03"), 5)  // key: 04:00
	sw.AddWithTime(ts("2023-04-21 15:04:04"), 1)  // key: 04:00, sum: 21.0
	sw.AddWithTime(ts("2023-04-21 15:04:05"), 3)  // key: 04:10, sum: 3.0
	require.Equal(t, 5.8, sw.Avg())
	require.Equal(t, 29.0, sw.Sum())
	require.Equal(t, 5, int(sw.Count()))
	require.Equal(t, 3, sw.buckets.Size())
	require.Equal(t, 5.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 21.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 3, int(sw.buckets.Values()[1].(*bucket).qty))
	require.Equal(t, 3.0, sw.buckets.Values()[2].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[2].(*bucket).qty))

	// up until 15:04:05 we had 3 buckets
	// let's advance the clock to 15:04:21 and the first data point should be evicted
	clock.Set(ts("2023-04-21 15:04:21"))
	require.Equal(t, 6.0, sw.Avg())
	require.Equal(t, 24.0, sw.Sum())
	require.Equal(t, 4, int(sw.Count()))
	require.Equal(t, 2, sw.buckets.Size())
	require.Equal(t, 21.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 3, int(sw.buckets.Values()[0].(*bucket).qty))
	require.Equal(t, 3.0, sw.buckets.Values()[1].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[1].(*bucket).qty))

	// let's advance the clock to 15:04:32 and another data point should be evicted
	clock.Set(ts("2023-04-21 15:04:32"))
	require.Equal(t, 3.0, sw.Avg())
	require.Equal(t, 3.0, sw.Sum())
	require.Equal(t, 1, sw.buckets.Size())
	require.Equal(t, 1, int(sw.Count()))
	require.Equal(t, 3.0, sw.buckets.Values()[0].(*bucket).sum)
	require.Equal(t, 1, int(sw.buckets.Values()[0].(*bucket).qty))

	// let's advance the clock to 15:04:46 and all data point should be evicted
	clock.Set(ts("2023-04-21 15:04:46"))
	require.Equal(t, 0.0, sw.Avg())
	require.Equal(t, 0.0, sw.Sum())
	require.Equal(t, 0, int(sw.Count()))
	require.Equal(t, 0, sw.buckets.Size())
}

// ts is a convenient method that must parse a time.Time from a string in format `"2006-01-02 15:04:05"`
func ts(s string) time.Time {
	format := "2006-01-02 15:04:05"
	t, err := time.Parse(format, s)
	if err != nil {
		panic(err)
	}
	return t
}
