/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package leveldbhelper

import (
	"fmt"
	"os"
	"testing"

	"github.com/hyperledger/fabric/common/ledger/dataformat"
	"github.com/syndtr/goleveldb/leveldb"

	"github.com/hyperledger/fabric-lib-go/common/flogging"
	"github.com/stretchr/testify/require"
)

func TestMain(m *testing.M) {
	flogging.ActivateSpec("leveldbhelper=debug")
	os.Exit(m.Run())
}

func TestDBBasicWriteAndReads(t *testing.T) {
	testDBBasicWriteAndReads(t, "db1", "db2", "")
}

func TestIterator(t *testing.T) {
	env := newTestProviderEnv(t, testDBPath)
	defer env.cleanup()
	p := env.provider

	db1 := p.GetDBHandle("db1")
	db2 := p.GetDBHandle("db2")
	db3 := p.GetDBHandle("db3")
	for i := 0; i < 20; i++ {
		require.NoError(t, db1.Put([]byte(createTestKey(i)), []byte(createTestValue("db1", i)), false))
		require.NoError(t, db2.Put([]byte(createTestKey(i)), []byte(createTestValue("db2", i)), false))
		require.NoError(t, db3.Put([]byte(createTestKey(i)), []byte(createTestValue("db3", i)), false))
	}

	rangeTestCases := []struct {
		startKey       []byte
		endKey         []byte
		expectedKeys   []string
		expectedValues []string
	}{
		{
			startKey:       []byte(createTestKey(2)),
			endKey:         []byte(createTestKey(4)),
			expectedKeys:   createTestKeys(2, 3),
			expectedValues: createTestValues("db2", 2, 3),
		},
		{
			startKey:       []byte(createTestKey(2)),
			endKey:         nil,
			expectedKeys:   createTestKeys(2, 19),
			expectedValues: createTestValues("db2", 2, 19),
		},
		{
			startKey:       nil,
			endKey:         nil,
			expectedKeys:   createTestKeys(0, 19),
			expectedValues: createTestValues("db2", 0, 19),
		},
	}

	for i, testCase := range rangeTestCases {
		t.Run(
			fmt.Sprintf("range testCase %d", i),
			func(t *testing.T) {
				itr, err := db2.GetIterator(testCase.startKey, testCase.endKey)
				require.NoError(t, err)
				defer itr.Release()
				checkItrResults(t, itr, testCase.expectedKeys, testCase.expectedValues)
			},
		)
	}

	rangeWithSeekTestCases := []struct {
		startKey          []byte
		endKey            []byte
		seekToKey         []byte
		itrAtKeyAfterSeek []byte
		expectedKeys      []string
		expectedValues    []string
	}{
		{
			startKey:          nil,
			endKey:            nil,
			seekToKey:         []byte(createTestKey(10)),
			itrAtKeyAfterSeek: []byte(createTestKey(10)),
			expectedKeys:      createTestKeys(11, 19),
			expectedValues:    createTestValues("db1", 11, 19),
		},
		{
			startKey:          []byte(createTestKey(11)),
			endKey:            nil,
			seekToKey:         []byte(createTestKey(5)),
			itrAtKeyAfterSeek: []byte(createTestKey(11)),
			expectedKeys:      createTestKeys(12, 19),
			expectedValues:    createTestValues("db1", 12, 19),
		},
		{
			startKey:          nil,
			endKey:            nil,
			seekToKey:         []byte(createTestKey(19)),
			itrAtKeyAfterSeek: []byte(createTestKey(19)),
			expectedKeys:      nil,
			expectedValues:    nil,
		},
	}

	for i, testCase := range rangeWithSeekTestCases {
		t.Run(
			fmt.Sprintf("range with seek testCase %d", i),
			func(t *testing.T) {
				itr, err := db1.GetIterator(testCase.startKey, testCase.endKey)
				require.NoError(t, err)
				defer itr.Release()
				require.True(t, itr.Seek(testCase.seekToKey))
				require.Equal(t, testCase.itrAtKeyAfterSeek, itr.Key())
				checkItrResults(t, itr, testCase.expectedKeys, testCase.expectedValues)
			},
		)
	}

	t.Run("test-first-prev", func(t *testing.T) {
		itr, err := db1.GetIterator(nil, nil)
		require.NoError(t, err)
		defer itr.Release()
		require.True(t, itr.Seek([]byte(createTestKey(10))))
		require.Equal(t, []byte(createTestKey(10)), itr.Key())
		checkItrResults(t, itr, createTestKeys(11, 19), createTestValues("db1", 11, 19))

		require.True(t, itr.First())
		require.True(t, itr.Seek([]byte(createTestKey(10))))
		require.Equal(t, []byte(createTestKey(10)), itr.Key())
		require.True(t, itr.Prev())
		checkItrResults(t, itr, createTestKeys(10, 19), createTestValues("db1", 10, 19))

		require.True(t, itr.First())
		require.False(t, itr.Seek([]byte(createTestKey(20))))
		require.True(t, itr.First())
		checkItrResults(t, itr, createTestKeys(1, 19), createTestValues("db1", 1, 19))

		require.True(t, itr.First())
		require.False(t, itr.Prev())
		checkItrResults(t, itr, createTestKeys(0, 19), createTestValues("db1", 0, 19))

		require.True(t, itr.First())
		require.True(t, itr.Last())
		checkItrResults(t, itr, nil, nil)
	})

	t.Run("test-error-path", func(t *testing.T) {
		env.provider.Close()
		itr, err := db1.GetIterator(nil, nil)
		require.EqualError(t, err, "internal leveldb error while obtaining db iterator: leveldb: closed")
		require.Nil(t, itr)
	})
}

func TestBatchedUpdates(t *testing.T) {
	env := newTestProviderEnv(t, testDBPath)
	defer env.cleanup()
	p := env.provider

	db1 := p.GetDBHandle("db1")
	db2 := p.GetDBHandle("db2")

	dbs := []*DBHandle{db1, db2}
	for _, db := range dbs {
		batch := db.NewUpdateBatch()
		batch.Put([]byte("key1"), []byte("value1"))
		batch.Put([]byte("key2"), []byte("value2"))
		batch.Put([]byte("key3"), []byte("value3"))
		require.NoError(t, db.WriteBatch(batch, true))
	}

	for _, db := range dbs {
		batch := db.NewUpdateBatch()
		batch.Delete([]byte("key2"))
		require.NoError(t, db.WriteBatch(batch, true))
	}

	for _, db := range dbs {
		val1, _ := db.Get([]byte("key1"))
		require.Equal(t, "value1", string(val1))

		val2, err2 := db.Get([]byte("key2"))
		require.NoError(t, err2, "")
		require.Nil(t, val2)

		val3, _ := db.Get([]byte("key3"))
		require.Equal(t, "value3", string(val3))
	}
}

func TestDrop(t *testing.T) {
	env := newTestProviderEnv(t, testDBPath)
	defer env.cleanup()
	p := env.provider

	db1 := p.GetDBHandle("db1")
	db2 := p.GetDBHandle("db2")
	db3 := p.GetDBHandle("db3")

	require.Contains(t, p.dbHandles, "db1")
	require.Contains(t, p.dbHandles, "db2")
	require.Contains(t, p.dbHandles, "db3")

	for i := 0; i < 20; i++ {
		require.NoError(t, db1.Put([]byte(createTestKey(i)), []byte(createTestValue("db1", i)), false))
		require.NoError(t, db2.Put([]byte(createTestKey(i)), []byte(createTestValue("db2", i)), false))
	}
	// db3 is used to test remove when multiple batches are needed (each long key has 125 bytes)
	for i := 0; i < 10000; i++ {
		require.NoError(t, db3.Put([]byte(createTestLongKey(i)), []byte(createTestValue("db3", i)), false))
	}

	expectedSetup := []struct {
		db             *DBHandle
		expectedKeys   []string
		expectedValues []string
	}{
		{
			db:             db1,
			expectedKeys:   createTestKeys(0, 19),
			expectedValues: createTestValues("db1", 0, 19),
		},
		{
			db:             db2,
			expectedKeys:   createTestKeys(0, 19),
			expectedValues: createTestValues("db2", 0, 19),
		},
		{
			db:             db3,
			expectedKeys:   createTestLongKeys(0, 9999),
			expectedValues: createTestValues("db3", 0, 9999),
		},
	}

	for _, dbSetup := range expectedSetup {
		itr, err := dbSetup.db.GetIterator(nil, nil)
		require.NoError(t, err)
		checkItrResults(t, itr, dbSetup.expectedKeys, dbSetup.expectedValues)
		itr.Release()
	}

	require.NoError(t, p.Drop("db1"))
	require.NoError(t, p.Drop("db3"))

	require.NotContains(t, p.dbHandles, "db1")
	require.NotContains(t, p.dbHandles, "db3")
	require.Contains(t, p.dbHandles, "db2")

	expectedResults := []struct {
		db             *DBHandle
		expectedKeys   []string
		expectedValues []string
	}{
		{
			db:             db1,
			expectedKeys:   nil,
			expectedValues: nil,
		},
		{
			db:             db2,
			expectedKeys:   createTestKeys(0, 19),
			expectedValues: createTestValues("db2", 0, 19),
		},
		{
			db:             db3,
			expectedKeys:   nil,
			expectedValues: nil,
		},
	}

	for _, result := range expectedResults {
		itr, err := result.db.GetIterator(nil, nil)
		require.NoError(t, err)
		checkItrResults(t, itr, result.expectedKeys, result.expectedValues)
		itr.Release()
	}

	// negative test
	p.Close()
	require.EqualError(t, db2.deleteAll(), "internal leveldb error while obtaining db iterator: leveldb: closed")
}

func TestFormatCheck(t *testing.T) {
	testCases := []struct {
		dataFormat     string
		dataExists     bool
		expectedFormat string
		expectedErr    *dataformat.ErrFormatMismatch
	}{
		{
			dataFormat:     "",
			dataExists:     true,
			expectedFormat: "",
			expectedErr:    nil,
		},
		{
			dataFormat:     "",
			dataExists:     false,
			expectedFormat: "",
			expectedErr:    nil,
		},
		{
			dataFormat:     "",
			dataExists:     false,
			expectedFormat: "2.0",
			expectedErr:    nil,
		},
		{
			dataFormat:     "",
			dataExists:     true,
			expectedFormat: "2.0",
			expectedErr:    &dataformat.ErrFormatMismatch{Format: "", ExpectedFormat: "2.0"},
		},
		{
			dataFormat:     "2.0",
			dataExists:     true,
			expectedFormat: "2.0",
			expectedErr:    nil,
		},
		{
			dataFormat:     "2.0",
			dataExists:     true,
			expectedFormat: "3.0",
			expectedErr:    &dataformat.ErrFormatMismatch{Format: "2.0", ExpectedFormat: "3.0"},
		},
	}

	for i, testCase := range testCases {
		t.Run(
			fmt.Sprintf("testCase %d", i),
			func(t *testing.T) {
				testFormatCheck(t, testCase.dataFormat, testCase.expectedFormat, testCase.dataExists, testCase.expectedErr)
			})
	}
}

func TestClose(t *testing.T) {
	env := newTestProviderEnv(t, testDBPath)
	defer env.cleanup()
	p := env.provider

	db1 := p.GetDBHandle("db1")
	db2 := p.GetDBHandle("db2")

	expectedDBHandles := map[string]*DBHandle{
		"db1": db1,
		"db2": db2,
	}
	require.Equal(t, expectedDBHandles, p.dbHandles)

	db1.Close()
	expectedDBHandles = map[string]*DBHandle{
		"db2": db2,
	}
	require.Equal(t, expectedDBHandles, p.dbHandles)

	db2.Close()
	require.Equal(t, map[string]*DBHandle{}, p.dbHandles)
}

func TestIsEmpty(t *testing.T) {
	var env *testDBProviderEnv
	var db1, db2 *DBHandle

	setup := func() {
		env = newTestProviderEnv(t, testDBPath)
		p := env.provider
		db1 = p.GetDBHandle("db1")
		db2 = p.GetDBHandle("db2")
	}

	cleanup := func() {
		env.cleanup()
	}

	t.Run("both the dbs are empty", func(t *testing.T) {
		setup()
		defer cleanup()

		empty, err := db1.IsEmpty()
		require.NoError(t, err)
		require.True(t, empty)

		empty, err = db2.IsEmpty()
		require.NoError(t, err)
		require.True(t, empty)
	})

	t.Run("only one db is empty", func(t *testing.T) {
		setup()
		defer cleanup()

		require.NoError(t, db1.Put([]byte("key"), []byte("value"), false))
		empty, err := db1.IsEmpty()
		require.NoError(t, err)
		require.False(t, empty)

		empty, err = db2.IsEmpty()
		require.NoError(t, err)
		require.True(t, empty)
	})

	t.Run("both the dbs contain data", func(t *testing.T) {
		setup()
		defer cleanup()

		require.NoError(t, db1.Put([]byte("key"), []byte("value"), false))
		require.NoError(t, db2.Put([]byte("key"), []byte("value"), false))

		empty, err := db1.IsEmpty()
		require.NoError(t, err)
		require.False(t, empty)

		empty, err = db2.IsEmpty()
		require.NoError(t, err)
		require.False(t, empty)
	})

	t.Run("iter error", func(t *testing.T) {
		setup()
		defer cleanup()

		env.provider.Close()
		empty, err := db1.IsEmpty()
		require.EqualError(t, err, "internal leveldb error while obtaining db iterator: leveldb: closed")
		require.False(t, empty)

		empty, err = db2.IsEmpty()
		require.EqualError(t, err, "internal leveldb error while obtaining db iterator: leveldb: closed")
		require.False(t, empty)
	})
}

func TestRetrieveDataFormatInfo(t *testing.T) {
	var env *testDBProviderEnv
	var provider *Provider
	setup := func() {
		env = newTestProviderEnv(t, testDBPath)
		provider = env.provider
	}

	cleanup := func() {
		env.cleanup()
	}

	t.Run("fresh db provider", func(t *testing.T) {
		setup()
		defer cleanup()

		provider.Close()
		info, err := RetrieveDataFormatInfo(testDBPath)
		require.NoError(t, err)
		require.Equal(t,
			&DataFormatInfo{
				FormatVerison: "",
				IsDBEmpty:     true,
			},
			info,
		)
	})

	t.Run("db provider with existing data", func(t *testing.T) {
		setup()
		defer cleanup()

		db := provider.GetDBHandle("dummy")
		require.NoError(t, db.Put([]byte("k"), []byte("v"), true))
		provider.Close()

		info, err := RetrieveDataFormatInfo(testDBPath)
		require.NoError(t, err)
		require.Equal(t,
			&DataFormatInfo{
				FormatVerison: "",
				IsDBEmpty:     false,
			},
			info,
		)
	})

	t.Run("db provider with existing data and version", func(t *testing.T) {
		setup()
		defer cleanup()

		require.NoError(t, provider.SetDataFormat("2.6"))
		db := provider.GetDBHandle("dummy")
		require.NoError(t, db.Put([]byte("k"), []byte("v"), true))
		env.provider.Close()
		info, err := RetrieveDataFormatInfo(testDBPath)
		require.NoError(t, err)
		require.Equal(t,
			&DataFormatInfo{
				FormatVerison: "2.6",
				IsDBEmpty:     false,
			},
			info,
		)
	})
}

func TestUpdateBatch(t *testing.T) {
	b := &UpdateBatch{
		dbName:       "mydb",
		leveldbBatch: &leveldb.Batch{},
	}

	require.Equal(t, 0, b.Size())
	require.Equal(t, 0, b.Len())

	k := []byte("key")
	v := []byte("value")

	// put a key and verify size and len
	b.Put(k, v)
	expectedSize := len("mydb") + len(dbNameKeySep) + len(k) + len(v)
	require.Equal(t, expectedSize, b.Size())
	require.Equal(t, 1, b.Len())

	// put the key again and verify size and len
	b.Put(k, v)
	expectedSize *= 2
	require.Equal(t, expectedSize, b.Size())
	require.Equal(t, 2, b.Len())

	// delete the key and verify size and len
	b.Delete(k)
	expectedSize += len("mydb") + len(dbNameKeySep) + len(k)
	require.Equal(t, expectedSize, b.Size())
	require.Equal(t, 3, b.Len())

	// reset and verify size and len
	b.Reset()
	require.Equal(t, 0, b.Size())
	require.Equal(t, 0, b.Len())

	// add key again after reset
	b.Put(k, v)
	expectedSize = len("mydb") + len(dbNameKeySep) + len(k) + len(v)
	require.Equal(t, expectedSize, b.Size())
	require.Equal(t, 1, b.Len())
}

func testFormatCheck(t *testing.T, dataFormat, expectedFormat string, dataExists bool, expectedErr *dataformat.ErrFormatMismatch) {
	require.NoError(t, os.RemoveAll(testDBPath))
	defer func() {
		require.NoError(t, os.RemoveAll(testDBPath))
	}()

	// setup test pre-conditions (create a db with dbformat)
	p, err := NewProvider(&Conf{DBPath: testDBPath, ExpectedFormat: dataFormat})
	require.NoError(t, err)
	f, err := p.GetDataFormat()
	require.NoError(t, err)
	require.Equal(t, dataFormat, f)
	if dataExists {
		require.NoError(t, p.GetDBHandle("testdb").Put([]byte("key"), []byte("value"), true))
	}

	// close and reopen with new conf
	p.Close()
	p, err = NewProvider(&Conf{DBPath: testDBPath, ExpectedFormat: expectedFormat})
	if expectedErr != nil {
		expectedErr.DBInfo = fmt.Sprintf("leveldb at [%s]", testDBPath)
		require.Equal(t, err, expectedErr)
		return
	}
	require.NoError(t, err)
	f, err = p.GetDataFormat()
	require.NoError(t, err)
	require.Equal(t, expectedFormat, f)
}

func testDBBasicWriteAndReads(t *testing.T, dbNames ...string) {
	env := newTestProviderEnv(t, testDBPath)
	defer env.cleanup()
	p := env.provider

	for _, dbName := range dbNames {
		db := p.GetDBHandle(dbName)
		require.NoError(t, db.Put([]byte("key1"), []byte("value1_"+dbName), false))
		require.NoError(t, db.Put([]byte("key2"), []byte("value2_"+dbName), false))
		require.NoError(t, db.Put([]byte("key3"), []byte("value3_"+dbName), false))
	}

	for _, dbName := range dbNames {
		db := p.GetDBHandle(dbName)
		val, err := db.Get([]byte("key1"))
		require.NoError(t, err, "")
		require.Equal(t, []byte("value1_"+dbName), val)

		val, err = db.Get([]byte("key2"))
		require.NoError(t, err, "")
		require.Equal(t, []byte("value2_"+dbName), val)

		val, err = db.Get([]byte("key3"))
		require.NoError(t, err, "")
		require.Equal(t, []byte("value3_"+dbName), val)
	}

	for _, dbName := range dbNames {
		db := p.GetDBHandle(dbName)
		require.NoError(t, db.Delete([]byte("key1"), false), "")
		val, err := db.Get([]byte("key1"))
		require.NoError(t, err, "")
		require.Nil(t, val)

		require.NoError(t, db.Delete([]byte("key2"), false), "")
		val, err = db.Get([]byte("key2"))
		require.NoError(t, err, "")
		require.Nil(t, val)

		require.NoError(t, db.Delete([]byte("key3"), false), "")
		val, err = db.Get([]byte("key3"))
		require.NoError(t, err, "")
		require.Nil(t, val)
	}
}

func checkItrResults(t *testing.T, itr *Iterator, expectedKeys []string, expectedValues []string) {
	var actualKeys []string
	var actualValues []string
	for itr.Next(); itr.Valid(); itr.Next() {
		actualKeys = append(actualKeys, string(itr.Key()))
		actualValues = append(actualValues, string(itr.Value()))
	}
	require.Equal(t, expectedKeys, actualKeys)
	require.Equal(t, expectedValues, actualValues)
	require.Equal(t, false, itr.Next())
}

func createTestKey(i int) string {
	return fmt.Sprintf("key_%06d", i)
}

const padding100 = "_0123456789_0123456789_0123456789_0123456789_0123456789_0123456789_0123456789_0123456789_0123456789_"

func createTestLongKey(i int) string {
	return fmt.Sprintf("key_%s_%10d", padding100, i)
}

func createTestValue(dbname string, i int) string {
	return fmt.Sprintf("value_%s_%06d", dbname, i)
}

func createTestKeys(start int, end int) []string {
	var keys []string
	for i := start; i <= end; i++ {
		keys = append(keys, createTestKey(i))
	}
	return keys
}

func createTestLongKeys(start int, end int) []string {
	var keys []string
	for i := start; i <= end; i++ {
		keys = append(keys, createTestLongKey(i))
	}
	return keys
}

func createTestValues(dbname string, start int, end int) []string {
	var values []string
	for i := start; i <= end; i++ {
		values = append(values, createTestValue(dbname, i))
	}
	return values
}
