package storm

import (
	"bytes"
	"encoding/binary"
	"io/ioutil"
	"math"
	"os"
	"path/filepath"
	"reflect"
	"testing"
	"time"

	"github.com/asdine/storm/v3/codec/json"
	bolt "go.etcd.io/bbolt"
	"github.com/stretchr/testify/require"
)

func TestNewStorm(t *testing.T) {
	db, err := Open("")

	require.Error(t, err)
	require.Nil(t, db)

	dir, err := ioutil.TempDir(os.TempDir(), "storm")
	require.NoError(t, err)
	defer os.RemoveAll(dir)

	file := filepath.Join(dir, "storm.db")
	db, err = Open(file)
	defer db.Close()

	require.Implements(t, (*Node)(nil), db)

	require.NoError(t, err)
	require.NotNil(t, db.Bolt)
	require.Equal(t, defaultCodec, db.Codec())

	var v string
	err = db.Get(dbinfo, "version", &v)
	require.NoError(t, err)
	require.Equal(t, Version, v)
}

func TestNewStormWithStormOptions(t *testing.T) {
	dir, _ := ioutil.TempDir(os.TempDir(), "storm")
	defer os.RemoveAll(dir)

	dc := new(dummyCodec)
	db1, _ := Open(filepath.Join(dir, "storm1.db"), BoltOptions(0660, &bolt.Options{Timeout: 10 * time.Second}), Codec(dc), Root("a", "b"))
	require.Equal(t, dc, db1.Codec())
	require.Equal(t, []string{"a", "b"}, db1.Node.(*node).rootBucket)

	err := db1.Save(&SimpleUser{ID: 1})
	require.NoError(t, err)

	db2, _ := Open(filepath.Join(dir, "storm2.db"), Codec(dc))
	require.Equal(t, dc, db2.Codec())
}

func TestNewStormWithBatch(t *testing.T) {
	dir, _ := ioutil.TempDir(os.TempDir(), "storm")
	defer os.RemoveAll(dir)

	db1, _ := Open(filepath.Join(dir, "storm1.db"), Batch())
	defer db1.Close()

	require.True(t, db1.Node.(*node).batchMode)
	n := db1.From().(*node)
	require.True(t, n.batchMode)
	n = db1.WithBatch(true).(*node)
	require.True(t, n.batchMode)
	n = db1.WithBatch(false).(*node)
	require.False(t, n.batchMode)
	n = n.From().(*node)
	require.False(t, n.batchMode)
	n = n.WithBatch(true).(*node)
	require.True(t, n.batchMode)
}

func TestBoltDB(t *testing.T) {
	dir, _ := ioutil.TempDir(os.TempDir(), "storm")
	defer os.RemoveAll(dir)
	bDB, err := bolt.Open(filepath.Join(dir, "bolt.db"), 0600, &bolt.Options{Timeout: 10 * time.Second})
	require.NoError(t, err)
	// no need to close bolt.DB Storm will take care of it
	sDB, err := Open("my.db", UseDB(bDB))
	require.NoError(t, err)
	defer sDB.Close()
	err = sDB.Save(&SimpleUser{ID: 10})
	require.NoError(t, err)
}

type dummyCodec int

func (c dummyCodec) Marshal(v interface{}) ([]byte, error) {
	return []byte("dummy"), nil
}

func (c dummyCodec) Unmarshal(b []byte, v interface{}) error {
	return nil
}

func (c dummyCodec) Name() string {
	return "dummy"
}

func TestCodec(t *testing.T) {
	u1 := &SimpleUser{Name: "John"}
	encoded, err := defaultCodec.Marshal(u1)
	require.Nil(t, err)
	u2 := &SimpleUser{}
	err = defaultCodec.Unmarshal(encoded, u2)
	require.Nil(t, err)
	if !reflect.DeepEqual(u1, u2) {
		t.Fatal("Codec mismatch")
	}
}

func TestToBytes(t *testing.T) {
	b, err := toBytes([]byte("a slice of bytes"), nil)
	require.NoError(t, err)
	require.Equal(t, []byte("a slice of bytes"), b)

	b, err = toBytes("a string", nil)
	require.NoError(t, err)
	require.Equal(t, []byte("a string"), b)

	b, err = toBytes(&SimpleUser{ID: 10, Name: "John", age: 100}, json.Codec)
	require.NoError(t, err)
	require.Equal(t, `{"ID":10,"Name":"John"}`, string(b))

	tests := map[interface{}]interface{}{
		int(-math.MaxInt64):    int64(-math.MaxInt64),
		int(math.MaxInt64):     int64(math.MaxInt64),
		int8(-math.MaxInt8):    int8(-math.MaxInt8),
		int8(math.MaxInt8):     int8(math.MaxInt8),
		int16(-math.MaxInt16):  int16(-math.MaxInt16),
		int16(math.MaxInt16):   int16(math.MaxInt16),
		int32(-math.MaxInt32):  int32(-math.MaxInt32),
		int32(math.MaxInt32):   int32(math.MaxInt32),
		int64(-math.MaxInt64):  int64(-math.MaxInt64),
		int64(math.MaxInt64):   int64(math.MaxInt64),
		uint(math.MaxUint64):   uint64(math.MaxUint64),
		uint64(math.MaxUint64): uint64(math.MaxUint64),
	}

	for v, expected := range tests {
		b, err = toBytes(v, nil)
		require.NoError(t, err)
		require.NotNil(t, b)
		buf := bytes.NewReader(b)
		typ := reflect.TypeOf(expected)
		actual := reflect.New(typ)
		err = binary.Read(buf, binary.BigEndian, actual.Interface())
		require.NoError(t, err)
		require.Equal(t, expected, actual.Elem().Interface())
	}
}

func createDB(t errorHandler, opts ...func(*Options) error) (*DB, func()) {
	dir, err := ioutil.TempDir(os.TempDir(), "storm")
	if err != nil {
		t.Error(err)
	}
	db, err := Open(filepath.Join(dir, "storm.db"), opts...)
	if err != nil {
		t.Error(err)
	}

	return db, func() {
		db.Close()
		os.RemoveAll(dir)
	}
}

type errorHandler interface {
	Error(args ...interface{})
}
