// Copyright 2020 Andrew Thornton. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package levelqueue

import (
	"strconv"
	"sync"
	"testing"

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

func TestUniqueQueue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	err = queue.RPush([]byte("test"))
	require.NoError(t, err)

	assert.EqualValues(t, 1, queue.Len())

	err = queue.RPush([]byte("test"))
	require.Error(t, err)

	assert.EqualValues(t, 1, queue.Len())

	err = queue.LPush([]byte("test"))
	require.Error(t, err)

	assert.EqualValues(t, 1, queue.Len())

	data, err := queue.LPop()
	require.NoError(t, err)
	assert.Equal(t, "test", string(data))

	// should be empty
	data, err = queue.LPop()
	require.Error(t, err)
	assert.Equal(t, []byte(nil), data)
	assert.Equal(t, ErrNotFound, err)

	assert.EqualValues(t, 0, queue.Len())

	err = queue.LPush([]byte("test2"))
	require.NoError(t, err)

	err = queue.LPush([]byte("test2"))
	require.Error(t, err)

	assert.EqualValues(t, 1, queue.Len())

	data, err = queue.LPop()
	require.NoError(t, err)
	assert.Equal(t, "test2", string(data))

	assert.EqualValues(t, 0, queue.Len())

	data, err = queue.LPop()
	require.Error(t, err)
	assert.Equal(t, []byte(nil), data)
	assert.Equal(t, ErrNotFound, err)

	data, err = queue.RPop()
	require.Error(t, err)
	assert.Equal(t, []byte(nil), data)
	assert.Equal(t, ErrNotFound, err)

	err = queue.Close()
	require.NoError(t, err)

	queue, err = OpenUnique(dbDir)
	require.NoError(t, err)

	err = queue.RPush([]byte("test3"))
	require.NoError(t, err)
	assert.EqualValues(t, 1, queue.Len())

	has, err := queue.Has([]byte("test3"))
	require.NoError(t, err)
	assert.True(t, has)

	has, err = queue.Has([]byte("test4"))
	require.NoError(t, err)
	assert.False(t, has)

	err = queue.LPush([]byte("test4"))
	require.NoError(t, err)

	assert.EqualValues(t, 2, queue.Len())

	data, err = queue.RPop()
	require.NoError(t, err)
	assert.Equal(t, "test3", string(data))

	data, err = queue.RPop()
	require.NoError(t, err)
	assert.Equal(t, "test4", string(data))

	has, err = queue.Has([]byte("test3"))
	require.NoError(t, err)
	assert.False(t, has)

	has, err = queue.Has([]byte("test4"))
	require.NoError(t, err)
	assert.False(t, has)
}

func TestGoroutines_uniquequeue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	for i := range 10 {
		err := queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(t, err)
	}

	var w sync.WaitGroup
	for i := range 10 {
		w.Add(1)
		go func(i int) {
			if i%2 == 0 {
				err := queue.RPush(append([]byte("concurrent"), []byte(strconv.Itoa(i))...))
				assert.NoError(t, err)
			} else {
				_, err := queue.RPop()
				assert.NoError(t, err)
			}
			w.Done()
		}(i)
	}
	w.Wait()
}

func TestGoroutines2_uniquequeue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	for i := range 10 {
		err := queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(t, err)
	}

	var w sync.WaitGroup
	for i := range 10 {
		w.Add(1)
		go func(i int) {
			if i%2 == 0 {
				err := queue.LPush(append([]byte("conc"), []byte(strconv.Itoa(i))...))
				assert.NoError(t, err)
			} else {
				_, err := queue.RPop()
				assert.NoError(t, err)
			}
			w.Done()
		}(i)
	}
	w.Wait()
}

func TestGoroutines3_uniquequeue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	for i := range 10 {
		err := queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(t, err)
	}

	var w sync.WaitGroup
	for i := range 10 {
		w.Add(1)
		go func(i int) {
			if i%2 == 0 {
				err := queue.LPush(append([]byte("conc"), []byte(strconv.Itoa(i))...))
				assert.NoError(t, err)
			} else {
				_, err := queue.LPop()
				assert.NoError(t, err)
			}
			w.Done()
		}(i)
	}
	w.Wait()
}

func TestHandle_uniquequeue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	for i := range 10 {
		err := queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(t, err)
	}

	var w sync.WaitGroup
	for i := range 10 {
		w.Add(1)
		go func(i int) {
			if i%2 == 0 {
				err := queue.RPush(append([]byte("conc"), []byte(strconv.Itoa(i))...))
				assert.NoError(t, err)
			} else {
				err := queue.RHandle(func([]byte) error {
					return nil
				})
				assert.NoError(t, err)
			}
			w.Done()
		}(i)
	}
	w.Wait()
}

func TestHandle2_uniquequeue(t *testing.T) {
	dbDir := t.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(t, err)

	for i := range 10 {
		err := queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(t, err)
	}
	assert.EqualValues(t, 10, queue.Len())

	var w sync.WaitGroup
	for i := range 10 {
		w.Add(1)
		go func(i int) {
			if i%2 == 0 {
				err := queue.LPush(append([]byte("conc"), []byte(strconv.Itoa(i))...))
				assert.NoError(t, err)
			} else {
				assert.NoError(t, queue.LHandle(func([]byte) error {
					return nil
				}))
			}
			w.Done()
		}(i)
	}
	w.Wait()
}

func BenchmarkPush_uniquequeue(b *testing.B) {
	dbDir := b.TempDir()

	queue, err := OpenUnique(dbDir)
	require.NoError(b, err)

	for i := 0; i < b.N; i++ {
		err = queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(b, err)
	}
}

func BenchmarkPop_uniquequeue(b *testing.B) {
	dbDir := b.TempDir()

	queue, err := Open(dbDir)
	require.NoError(b, err)
	b.StopTimer()
	for i := 0; i < b.N; i++ {
		err = queue.RPush(append([]byte("test"), []byte(strconv.Itoa(i))...))
		require.NoError(b, err)
	}
	b.StartTimer()

	for i := 0; i < b.N; i++ {
		_, err = queue.RPop()
		require.NoError(b, err)
	}
}
