package kvdb

import (
	"fmt"
	"math"
	"testing"

	"github.com/btcsuite/btcwallet/walletdb"
	"github.com/stretchr/testify/require"
)

func testBucketCreation(t *testing.T, db walletdb.DB) {
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		// empty bucket name
		b, err := tx.CreateTopLevelBucket(nil)
		require.Error(t, walletdb.ErrBucketNameRequired, err)
		require.Nil(t, b)

		// empty bucket name
		b, err = tx.CreateTopLevelBucket([]byte(""))
		require.Error(t, walletdb.ErrBucketNameRequired, err)
		require.Nil(t, b)

		// "apple"
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.NoError(t, err)
		require.NotNil(t, apple)

		// Check bucket tx.
		require.Equal(t, tx, apple.Tx())

		// "apple" already created
		b, err = tx.CreateTopLevelBucket([]byte("apple"))
		require.NoError(t, err)
		require.NotNil(t, b)

		// "apple/banana"
		banana, err := apple.CreateBucket([]byte("banana"))
		require.NoError(t, err)
		require.NotNil(t, banana)

		banana, err = apple.CreateBucketIfNotExists([]byte("banana"))
		require.NoError(t, err)
		require.NotNil(t, banana)

		// Try creating "apple/banana" again
		b, err = apple.CreateBucket([]byte("banana"))
		require.Error(t, walletdb.ErrBucketExists, err)
		require.Nil(t, b)

		// "apple/mango"
		mango, err := apple.CreateBucket([]byte("mango"))
		require.Nil(t, err)
		require.NotNil(t, mango)

		// "apple/banana/pear"
		pear, err := banana.CreateBucket([]byte("pear"))
		require.Nil(t, err)
		require.NotNil(t, pear)

		// empty bucket
		require.Nil(t, apple.NestedReadWriteBucket(nil))
		require.Nil(t, apple.NestedReadWriteBucket([]byte("")))

		// "apple/pear" doesn't exist
		require.Nil(t, apple.NestedReadWriteBucket([]byte("pear")))

		// "apple/banana" exits
		require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana")))
		require.NotNil(t, apple.NestedReadBucket([]byte("banana")))
		return nil
	}, func() {})

	require.Nil(t, err)
}

func testBucketDeletion(t *testing.T, db walletdb.DB) {
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		// "apple"
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		// "apple/banana"
		banana, err := apple.CreateBucket([]byte("banana"))
		require.Nil(t, err)
		require.NotNil(t, banana)

		kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}}

		for _, kv := range kvs {
			require.NoError(t, banana.Put([]byte(kv.key), []byte(kv.val)))
			require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key)))
		}

		// Delete a k/v from "apple/banana"
		require.NoError(t, banana.Delete([]byte("key2")))
		// Try getting/putting/deleting invalid k/v's.
		require.Nil(t, banana.Get(nil))
		require.Error(t, walletdb.ErrKeyRequired, banana.Put(nil, []byte("val")))
		require.Error(t, walletdb.ErrKeyRequired, banana.Delete(nil))

		// Try deleting a k/v that doesn't exist.
		require.NoError(t, banana.Delete([]byte("nokey")))

		// "apple/pear"
		pear, err := apple.CreateBucket([]byte("pear"))
		require.Nil(t, err)
		require.NotNil(t, pear)

		// Put some values into "apple/pear"
		for _, kv := range kvs {
			require.Nil(t, pear.Put([]byte(kv.key), []byte(kv.val)))
			require.Equal(t, []byte(kv.val), pear.Get([]byte(kv.key)))
		}

		// Create nested bucket "apple/pear/cherry"
		cherry, err := pear.CreateBucket([]byte("cherry"))
		require.Nil(t, err)
		require.NotNil(t, cherry)

		// Put some values into "apple/pear/cherry"
		for _, kv := range kvs {
			require.NoError(t, cherry.Put([]byte(kv.key), []byte(kv.val)))
		}

		// Read back values in "apple/pear/cherry" trough a read bucket.
		cherryReadBucket := pear.NestedReadBucket([]byte("cherry"))
		for _, kv := range kvs {
			require.Equal(
				t, []byte(kv.val),
				cherryReadBucket.Get([]byte(kv.key)),
			)
		}

		// Try deleting some invalid buckets.
		require.Error(t,
			walletdb.ErrBucketNameRequired, apple.DeleteNestedBucket(nil),
		)

		// Try deleting a non existing bucket.
		require.Error(
			t,
			walletdb.ErrBucketNotFound,
			apple.DeleteNestedBucket([]byte("missing")),
		)

		// Delete "apple/pear"
		require.Nil(t, apple.DeleteNestedBucket([]byte("pear")))

		// "apple/pear" deleted
		require.Nil(t, apple.NestedReadWriteBucket([]byte("pear")))

		// "aple/banana" exists
		require.NotNil(t, apple.NestedReadWriteBucket([]byte("banana")))
		return nil
	}, func() {})

	require.Nil(t, err)
}

type bucketIterator = func(walletdb.ReadWriteBucket,
	func(key, val []byte) error) error

func testBucketForEach(t *testing.T, db walletdb.DB) {
	testBucketIterator(t, db, func(bucket walletdb.ReadWriteBucket,
		callback func(key, val []byte) error) error {

		return bucket.ForEach(callback)
	})
}

func testBucketIterator(t *testing.T, db walletdb.DB,
	iterator bucketIterator) {

	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		// "apple"
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		// "apple/banana"
		banana, err := apple.CreateBucket([]byte("banana"))
		require.Nil(t, err)
		require.NotNil(t, banana)

		kvs := []KV{{"key1", "val1"}, {"key2", "val2"}, {"key3", "val3"}}

		// put some values into "apple" and "apple/banana" too
		for _, kv := range kvs {
			require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val)))
			require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key)))

			require.Nil(t, banana.Put([]byte(kv.key), []byte(kv.val)))
			require.Equal(t, []byte(kv.val), banana.Get([]byte(kv.key)))
		}

		got := make(map[string]string)
		err = apple.ForEach(func(key, val []byte) error {
			got[string(key)] = string(val)
			return nil
		})

		expected := map[string]string{
			"key1":   "val1",
			"key2":   "val2",
			"key3":   "val3",
			"banana": "",
		}

		require.NoError(t, err)
		require.Equal(t, expected, got)

		got = make(map[string]string)
		err = iterator(banana, func(key, val []byte) error {
			got[string(key)] = string(val)
			return nil
		})

		require.NoError(t, err)
		// remove the sub-bucket key
		delete(expected, "banana")
		require.Equal(t, expected, got)

		return nil
	}, func() {})

	require.Nil(t, err)
}

func testBucketForEachWithError(t *testing.T, db walletdb.DB) {
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		// "apple"
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		// "apple/banana"
		banana, err := apple.CreateBucket([]byte("banana"))
		require.Nil(t, err)
		require.NotNil(t, banana)

		// "apple/pear"
		pear, err := apple.CreateBucket([]byte("pear"))
		require.Nil(t, err)
		require.NotNil(t, pear)

		kvs := []KV{{"key1", "val1"}, {"key2", "val2"}}

		// Put some values into "apple" and "apple/banana" too.
		for _, kv := range kvs {
			require.Nil(t, apple.Put([]byte(kv.key), []byte(kv.val)))
			require.Equal(t, []byte(kv.val), apple.Get([]byte(kv.key)))
		}

		got := make(map[string]string)
		i := 0
		// Error while iterating value keys.
		err = apple.ForEach(func(key, val []byte) error {
			if i == 2 {
				return fmt.Errorf("error")
			}

			got[string(key)] = string(val)
			i++
			return nil
		})

		expected := map[string]string{
			"banana": "",
			"key1":   "val1",
		}

		require.Equal(t, expected, got)
		require.Error(t, err)

		got = make(map[string]string)
		i = 0
		// Erro while iterating buckets.
		err = apple.ForEach(func(key, val []byte) error {
			if i == 3 {
				return fmt.Errorf("error")
			}

			got[string(key)] = string(val)
			i++
			return nil
		})

		expected = map[string]string{
			"banana": "",
			"key1":   "val1",
			"key2":   "val2",
		}

		require.Equal(t, expected, got)
		require.Error(t, err)
		return nil
	}, func() {})

	require.Nil(t, err)
}

func testBucketSequence(t *testing.T, db walletdb.DB) {
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		banana, err := apple.CreateBucket([]byte("banana"))
		require.Nil(t, err)
		require.NotNil(t, banana)

		require.Equal(t, uint64(0), apple.Sequence())
		require.Equal(t, uint64(0), banana.Sequence())

		require.Nil(t, apple.SetSequence(math.MaxUint64))
		require.Equal(t, uint64(math.MaxUint64), apple.Sequence())

		for i := uint64(0); i < uint64(5); i++ {
			s, err := apple.NextSequence()
			require.Nil(t, err)
			require.Equal(t, i, s)
		}

		return nil
	}, func() {})

	require.Nil(t, err)
}

// TestKeyClash tests that one cannot create a bucket if a value with the same
// key exists and the same is true in reverse: that a value cannot be put if
// a bucket with the same key exists.
func testKeyClash(t *testing.T, db walletdb.DB) {
	// First:
	// put: /apple/key -> val
	// create bucket: /apple/banana
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		require.NoError(t, apple.Put([]byte("key"), []byte("val")))

		banana, err := apple.CreateBucket([]byte("banana"))
		require.Nil(t, err)
		require.NotNil(t, banana)

		return nil
	}, func() {})

	require.Nil(t, err)

	// Next try to:
	// put: /apple/banana -> val => will fail (as /apple/banana is a bucket)
	// create bucket: /apple/key => will fail (as /apple/key is a value)
	err = Update(db, func(tx walletdb.ReadWriteTx) error {
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.Nil(t, err)
		require.NotNil(t, apple)

		require.Error(t,
			walletdb.ErrIncompatibleValue,
			apple.Put([]byte("banana"), []byte("val")),
		)

		b, err := apple.CreateBucket([]byte("key"))
		require.Nil(t, b)
		require.Error(t, walletdb.ErrIncompatibleValue, b)

		b, err = apple.CreateBucketIfNotExists([]byte("key"))
		require.Nil(t, b)
		require.Error(t, walletdb.ErrIncompatibleValue, b)

		return nil
	}, func() {})

	require.Nil(t, err)
}

// TestBucketCreateDelete tests that creating then deleting then creating a
// bucket succeeds.
func testBucketCreateDelete(t *testing.T, db walletdb.DB) {
	err := Update(db, func(tx walletdb.ReadWriteTx) error {
		apple, err := tx.CreateTopLevelBucket([]byte("apple"))
		require.NoError(t, err)
		require.NotNil(t, apple)

		banana, err := apple.CreateBucket([]byte("banana"))
		require.NoError(t, err)
		require.NotNil(t, banana)

		return nil
	}, func() {})
	require.NoError(t, err)

	err = Update(db, func(tx walletdb.ReadWriteTx) error {
		apple := tx.ReadWriteBucket([]byte("apple"))
		require.NotNil(t, apple)
		require.NoError(t, apple.DeleteNestedBucket([]byte("banana")))

		return nil
	}, func() {})
	require.NoError(t, err)

	err = Update(db, func(tx walletdb.ReadWriteTx) error {
		apple := tx.ReadWriteBucket([]byte("apple"))
		require.NotNil(t, apple)
		require.NoError(t, apple.Put([]byte("banana"), []byte("value")))

		return nil
	}, func() {})
	require.NoError(t, err)
}

func testTopLevelBucketCreation(t *testing.T, db walletdb.DB) {
	require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error {
		// Try to delete all data (there is none).
		err := tx.DeleteTopLevelBucket([]byte("top"))
		require.ErrorIs(t, walletdb.ErrBucketNotFound, err)

		// Create top level bucket.
		top, err := tx.CreateTopLevelBucket([]byte("top"))
		require.NoError(t, err)
		require.NotNil(t, top)

		// Create second top level bucket with special characters.
		top2, err := tx.CreateTopLevelBucket([]byte{1, 2, 3})
		require.NoError(t, err)
		require.NotNil(t, top2)

		top2 = tx.ReadWriteBucket([]byte{1, 2, 3})
		require.NotNil(t, top2)

		// List top level buckets.
		var tlKeys [][]byte
		require.NoError(t, tx.ForEachBucket(func(k []byte) error {
			tlKeys = append(tlKeys, k)
			return nil
		}))
		require.Equal(t, [][]byte{{1, 2, 3}, []byte("top")}, tlKeys)

		// Create third top level bucket with special uppercase.
		top3, err := tx.CreateTopLevelBucket([]byte("UpperBucket"))
		require.NoError(t, err)
		require.NotNil(t, top3)

		top3 = tx.ReadWriteBucket([]byte("UpperBucket"))
		require.NotNil(t, top3)

		require.NoError(t, tx.DeleteTopLevelBucket([]byte("top")))
		require.NoError(t, tx.DeleteTopLevelBucket([]byte{1, 2, 3}))
		require.NoError(t, tx.DeleteTopLevelBucket([]byte("UpperBucket")))

		tx.ForEachBucket(func(k []byte) error {
			require.Fail(t, "no top level buckets expected")
			return nil
		})

		return nil
	}, func() {}))
}

func testBucketOperations(t *testing.T, db walletdb.DB) {
	require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error {
		// Create top level bucket.
		top, err := tx.CreateTopLevelBucket([]byte("top"))
		require.NoError(t, err)
		require.NotNil(t, top)

		// Assert that key doesn't exist.
		require.Nil(t, top.Get([]byte("key")))

		require.NoError(t, top.ForEach(func(k, v []byte) error {
			require.Fail(t, "unexpected data")
			return nil
		}))

		// Put key.
		require.NoError(t, top.Put([]byte("key"), []byte("val")))
		require.Equal(t, []byte("val"), top.Get([]byte("key")))

		// Overwrite key.
		require.NoError(t, top.Put([]byte("key"), []byte("val2")))
		require.Equal(t, []byte("val2"), top.Get([]byte("key")))

		// Put nil value.
		require.NoError(t, top.Put([]byte("nilkey"), nil))
		require.Equal(t, []byte(""), top.Get([]byte("nilkey")))

		// Put empty value.
		require.NoError(t, top.Put([]byte("nilkey"), []byte{}))
		require.Equal(t, []byte(""), top.Get([]byte("nilkey")))

		// Try to create bucket with same name as previous key.
		_, err = top.CreateBucket([]byte("key"))
		require.ErrorIs(t, err, walletdb.ErrIncompatibleValue)

		_, err = top.CreateBucketIfNotExists([]byte("key"))
		require.ErrorIs(t, err, walletdb.ErrIncompatibleValue)

		// Create sub-bucket.
		sub2, err := top.CreateBucket([]byte("sub2"))
		require.NoError(t, err)
		require.NotNil(t, sub2)

		// Assert that re-creating the bucket fails.
		_, err = top.CreateBucket([]byte("sub2"))
		require.ErrorIs(t, err, walletdb.ErrBucketExists)

		// Assert that create-if-not-exists succeeds.
		_, err = top.CreateBucketIfNotExists([]byte("sub2"))
		require.NoError(t, err)

		// Assert that fetching the bucket succeeds.
		sub2 = top.NestedReadWriteBucket([]byte("sub2"))
		require.NotNil(t, sub2)

		// Try to put key with same name as bucket.
		require.ErrorIs(t, top.Put([]byte("sub2"), []byte("val")), walletdb.ErrIncompatibleValue)

		// Put key into sub bucket.
		require.NoError(t, sub2.Put([]byte("subkey"), []byte("subval")))
		require.Equal(t, []byte("subval"), sub2.Get([]byte("subkey")))

		// Overwrite key in sub bucket.
		require.NoError(t, sub2.Put([]byte("subkey"), []byte("subval2")))
		require.Equal(t, []byte("subval2"), sub2.Get([]byte("subkey")))

		// Check for each result.
		kvs := make(map[string][]byte)
		require.NoError(t, top.ForEach(func(k, v []byte) error {
			kvs[string(k)] = v
			return nil
		}))
		require.Equal(t, map[string][]byte{
			"key":    []byte("val2"),
			"nilkey": []byte(""),
			"sub2":   nil,
		}, kvs)

		// Delete key.
		require.NoError(t, top.Delete([]byte("key")))

		// Delete non-existent key.
		require.NoError(t, top.Delete([]byte("keynonexistent")))

		// Test cursor.
		cursor := top.ReadWriteCursor()
		k, v := cursor.First()
		require.Equal(t, []byte("nilkey"), k)
		require.Equal(t, []byte(""), v)

		k, v = cursor.Last()
		require.Equal(t, []byte("sub2"), k)
		require.Nil(t, v)

		k, v = cursor.Prev()
		require.Equal(t, []byte("nilkey"), k)
		require.Equal(t, []byte(""), v)

		k, v = cursor.Prev()
		require.Nil(t, k)
		require.Nil(t, v)

		k, v = cursor.Next()
		require.Equal(t, []byte("sub2"), k)
		require.Nil(t, v)

		k, v = cursor.Next()
		require.Nil(t, k)
		require.Nil(t, v)

		k, v = cursor.Seek([]byte("nilkey"))
		require.Equal(t, []byte("nilkey"), k)
		require.Equal(t, []byte(""), v)

		require.NoError(t, sub2.Put([]byte("k1"), []byte("v1")))
		require.NoError(t, sub2.Put([]byte("k2"), []byte("v2")))
		require.NoError(t, sub2.Put([]byte("k3"), []byte("v3")))

		cursor = sub2.ReadWriteCursor()
		cursor.First()
		for i := 0; i < 4; i++ {
			require.NoError(t, cursor.Delete())
		}
		require.NoError(t, sub2.ForEach(func(k, v []byte) error {
			require.Fail(t, "unexpected data")
			return nil
		}))

		_, err = sub2.CreateBucket([]byte("sub3"))
		require.NoError(t, err)
		require.ErrorIs(t, cursor.Delete(), walletdb.ErrIncompatibleValue)

		//Try to delete all data.
		require.NoError(t, tx.DeleteTopLevelBucket([]byte("top")))
		require.Nil(t, tx.ReadBucket([]byte("top")))

		return nil
	}, func() {}))
}

func testSubBucketSequence(t *testing.T, db walletdb.DB) {
	require.NoError(t, Update(db, func(tx walletdb.ReadWriteTx) error {
		// Create top level bucket.
		top, err := tx.CreateTopLevelBucket([]byte("top"))
		require.NoError(t, err)
		require.NotNil(t, top)

		// Create sub-bucket.
		sub2, err := top.CreateBucket([]byte("sub2"))
		require.NoError(t, err)
		require.NotNil(t, sub2)

		// Test sequence.
		require.Equal(t, uint64(0), top.Sequence())

		require.NoError(t, top.SetSequence(100))
		require.Equal(t, uint64(100), top.Sequence())

		require.NoError(t, top.SetSequence(101))
		require.Equal(t, uint64(101), top.Sequence())

		next, err := top.NextSequence()
		require.NoError(t, err)
		require.Equal(t, uint64(102), next)

		next, err = sub2.NextSequence()
		require.NoError(t, err)
		require.Equal(t, uint64(1), next)

		return nil
	}, func() {}))
}
