package memtable

import (
	"fmt"
	"math/rand"
	"sync"
	"testing"

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

func TestSkipListEmpty(t *testing.T) {
	sl := NewSkipList(NewArena(1024))

	found, value, sequence := sl.Get([]byte("missing"))
	require.False(t, found)
	require.Nil(t, value)
	require.Zero(t, sequence)
}

func TestSkipListOrdered(t *testing.T) {
	sl := NewSkipList(NewArena(64 << 20))

	keys := []string{"c", "a", "e", "b", "d"}
	for i, key := range keys {
		require.NoError(t, sl.Put([]byte(key), []byte("value-"+key), uint64(i+1), false))
	}

	for i, key := range keys {
		found, value, sequence := sl.Get([]byte(key))
		require.True(t, found, "key %q", key)
		require.Equal(t, []byte("value-"+key), value)
		require.Equal(t, uint64(i+1), sequence)
	}

	it := sl.NewIterator()
	var got []string
	for it.Valid() {
		got = append(got, string(it.Key()))
		it.Next()
	}
	require.Equal(t, []string{"a", "b", "c", "d", "e"}, got)
}

func TestSkipListOverwrite(t *testing.T) {
	sl := NewSkipList(NewArena(1024))

	require.NoError(t, sl.Put([]byte("key"), []byte("old"), 1, false))
	require.NoError(t, sl.Put([]byte("key"), []byte("new"), 2, false))

	found, value, sequence := sl.Get([]byte("key"))
	require.True(t, found)
	require.Equal(t, []byte("new"), value)
	require.Equal(t, uint64(2), sequence)

	it := sl.NewIterator()
	require.True(t, it.Valid())
	require.Equal(t, []byte("key"), it.Key())
	require.Equal(t, []byte("new"), it.Value())
	it.Next()
	require.False(t, it.Valid())
}

func TestSkipListPendingEntriesAreHidden(t *testing.T) {
	sl := NewSkipList(NewArena(1024))

	require.NoError(t, sl.Put([]byte("a"), []byte("pending"), 1, true))
	require.NoError(t, sl.Put([]byte("b"), []byte("published"), 2, false))

	found, _, _ := sl.Get([]byte("a"))
	require.False(t, found)

	it := sl.NewIterator()
	require.True(t, it.Valid())
	require.Equal(t, []byte("b"), it.Key())
	it.Next()
	require.False(t, it.Valid())
}

func TestSkipListConcurrent(t *testing.T) {
	sl := NewSkipList(NewArena(64 << 20))

	const (
		writers        = 4
		readers        = 4
		keysPerWriter  = 100
		readsPerReader = 1000
	)

	var wg sync.WaitGroup
	start := make(chan struct{})
	errCh := make(chan error, writers+readers)

	for writer := range writers {
		wg.Go(func() {
			<-start
			for i := range keysPerWriter {
				key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, i)
				value := fmt.Appendf(nil, "value-%d-%03d", writer, i)
				sequence := uint64(writer*keysPerWriter + i + 1)
				if err := sl.Put(key, value, sequence, false); err != nil {
					errCh <- err
					return
				}
			}
		})
	}

	for reader := range readers {
		wg.Go(func() {
			<-start
			rng := rand.New(rand.NewSource(int64(reader)))
			for range readsPerReader {
				writer := rng.Intn(writers)
				keyID := rng.Intn(keysPerWriter)
				key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, keyID)
				found, value, sequence := sl.Get(key)
				if found {
					expectedValue := fmt.Appendf(nil, "value-%d-%03d", writer, keyID)
					expectedSequence := uint64(writer*keysPerWriter + keyID + 1)
					if err := compareEntry(value, sequence, expectedValue, expectedSequence); err != nil {
						errCh <- err
						return
					}
				}
			}
		})
	}

	close(start)
	wg.Wait()
	close(errCh)
	for err := range errCh {
		require.NoError(t, err)
	}

	for writer := range writers {
		for i := range keysPerWriter {
			key := fmt.Appendf(nil, "writer-%d-key-%03d", writer, i)
			found, value, sequence := sl.Get(key)
			require.True(t, found, "key %q", key)
			require.Equal(t, fmt.Appendf(nil, "value-%d-%03d", writer, i), value)
			require.Equal(t, uint64(writer*keysPerWriter+i+1), sequence)
		}
	}
}

func compareEntry(value []byte, sequence uint64, expectedValue []byte, expectedSequence uint64) error {
	if string(value) != string(expectedValue) {
		return fmt.Errorf("value = %q, want %q", value, expectedValue)
	}
	if sequence != expectedSequence {
		return fmt.Errorf("sequence = %d, want %d", sequence, expectedSequence)
	}
	return nil
}
