package bolt_test

import (
	"errors"
	"fmt"
	"os"
	"testing"

	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/boltdb/bolt"
)

// Ensure that committing a closed transaction returns an error.
func TestTx_Commit_Closed(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	tx, _ := db.Begin(true)
	tx.CreateBucket([]byte("foo"))
	ok(t, tx.Commit())
	equals(t, tx.Commit(), bolt.ErrTxClosed)
}

// Ensure that rolling back a closed transaction returns an error.
func TestTx_Rollback_Closed(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	tx, _ := db.Begin(true)
	ok(t, tx.Rollback())
	equals(t, tx.Rollback(), bolt.ErrTxClosed)
}

// Ensure that committing a read-only transaction returns an error.
func TestTx_Commit_ReadOnly(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	tx, _ := db.Begin(false)
	equals(t, tx.Commit(), bolt.ErrTxNotWritable)
}

// Ensure that a transaction can retrieve a cursor on the root bucket.
func TestTx_Cursor(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.CreateBucket([]byte("woojits"))
		c := tx.Cursor()

		k, v := c.First()
		equals(t, "widgets", string(k))
		assert(t, v == nil, "")

		k, v = c.Next()
		equals(t, "woojits", string(k))
		assert(t, v == nil, "")

		k, v = c.Next()
		assert(t, k == nil, "")
		assert(t, v == nil, "")

		return nil
	})
}

// Ensure that creating a bucket with a read-only transaction returns an error.
func TestTx_CreateBucket_ReadOnly(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.View(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte("foo"))
		assert(t, b == nil, "")
		equals(t, bolt.ErrTxNotWritable, err)
		return nil
	})
}

// Ensure that creating a bucket on a closed transaction returns an error.
func TestTx_CreateBucket_Closed(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	tx, _ := db.Begin(true)
	tx.Commit()
	b, err := tx.CreateBucket([]byte("foo"))
	assert(t, b == nil, "")
	equals(t, bolt.ErrTxClosed, err)
}

// Ensure that a Tx can retrieve a bucket.
func TestTx_Bucket(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		b := tx.Bucket([]byte("widgets"))
		assert(t, b != nil, "")
		return nil
	})
}

// Ensure that a Tx retrieving a non-existent key returns nil.
func TestTx_Get_Missing(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		value := tx.Bucket([]byte("widgets")).Get([]byte("no_such_key"))
		assert(t, value == nil, "")
		return nil
	})
}

// Ensure that a bucket can be created and retrieved.
func TestTx_CreateBucket(t *testing.T) {
	db := NewTestDB()
	defer db.Close()

	// Create a bucket.
	db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte("widgets"))
		assert(t, b != nil, "")
		ok(t, err)
		return nil
	})

	// Read the bucket through a separate transaction.
	db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("widgets"))
		assert(t, b != nil, "")
		return nil
	})
}

// Ensure that a bucket can be created if it doesn't already exist.
func TestTx_CreateBucketIfNotExists(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists([]byte("widgets"))
		assert(t, b != nil, "")
		ok(t, err)

		b, err = tx.CreateBucketIfNotExists([]byte("widgets"))
		assert(t, b != nil, "")
		ok(t, err)

		b, err = tx.CreateBucketIfNotExists([]byte{})
		assert(t, b == nil, "")
		equals(t, bolt.ErrBucketNameRequired, err)

		b, err = tx.CreateBucketIfNotExists(nil)
		assert(t, b == nil, "")
		equals(t, bolt.ErrBucketNameRequired, err)
		return nil
	})

	// Read the bucket through a separate transaction.
	db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("widgets"))
		assert(t, b != nil, "")
		return nil
	})
}

// Ensure that a bucket cannot be created twice.
func TestTx_CreateBucket_Exists(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	// Create a bucket.
	db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte("widgets"))
		assert(t, b != nil, "")
		ok(t, err)
		return nil
	})

	// Create the same bucket again.
	db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte("widgets"))
		assert(t, b == nil, "")
		equals(t, bolt.ErrBucketExists, err)
		return nil
	})
}

// Ensure that a bucket is created with a non-blank name.
func TestTx_CreateBucket_NameRequired(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket(nil)
		assert(t, b == nil, "")
		equals(t, bolt.ErrBucketNameRequired, err)
		return nil
	})
}

// Ensure that a bucket can be deleted.
func TestTx_DeleteBucket(t *testing.T) {
	db := NewTestDB()
	defer db.Close()

	// Create a bucket and add a value.
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		return nil
	})

	// Delete the bucket and make sure we can't get the value.
	db.Update(func(tx *bolt.Tx) error {
		ok(t, tx.DeleteBucket([]byte("widgets")))
		assert(t, tx.Bucket([]byte("widgets")) == nil, "")
		return nil
	})

	db.Update(func(tx *bolt.Tx) error {
		// Create the bucket again and make sure there's not a phantom value.
		b, err := tx.CreateBucket([]byte("widgets"))
		assert(t, b != nil, "")
		ok(t, err)
		assert(t, tx.Bucket([]byte("widgets")).Get([]byte("foo")) == nil, "")
		return nil
	})
}

// Ensure that deleting a bucket on a closed transaction returns an error.
func TestTx_DeleteBucket_Closed(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	tx, _ := db.Begin(true)
	tx.Commit()
	equals(t, tx.DeleteBucket([]byte("foo")), bolt.ErrTxClosed)
}

// Ensure that deleting a bucket with a read-only transaction returns an error.
func TestTx_DeleteBucket_ReadOnly(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.View(func(tx *bolt.Tx) error {
		equals(t, tx.DeleteBucket([]byte("foo")), bolt.ErrTxNotWritable)
		return nil
	})
}

// Ensure that nothing happens when deleting a bucket that doesn't exist.
func TestTx_DeleteBucket_NotFound(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		equals(t, bolt.ErrBucketNotFound, tx.DeleteBucket([]byte("widgets")))
		return nil
	})
}

// Ensure that Tx commit handlers are called after a transaction successfully commits.
func TestTx_OnCommit(t *testing.T) {
	var x int
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.OnCommit(func() { x += 1 })
		tx.OnCommit(func() { x += 2 })
		_, err := tx.CreateBucket([]byte("widgets"))
		return err
	})
	equals(t, 3, x)
}

// Ensure that Tx commit handlers are NOT called after a transaction rolls back.
func TestTx_OnCommit_Rollback(t *testing.T) {
	var x int
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.OnCommit(func() { x += 1 })
		tx.OnCommit(func() { x += 2 })
		tx.CreateBucket([]byte("widgets"))
		return errors.New("rollback this commit")
	})
	equals(t, 0, x)
}

// Ensure that the database can be copied to a file path.
func TestTx_CopyFile(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	var dest = tempfile()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		tx.Bucket([]byte("widgets")).Put([]byte("baz"), []byte("bat"))
		return nil
	})

	ok(t, db.View(func(tx *bolt.Tx) error { return tx.CopyFile(dest, 0600) }))

	db2, err := bolt.Open(dest, 0600, nil)
	ok(t, err)
	defer db2.Close()

	db2.View(func(tx *bolt.Tx) error {
		equals(t, []byte("bar"), tx.Bucket([]byte("widgets")).Get([]byte("foo")))
		equals(t, []byte("bat"), tx.Bucket([]byte("widgets")).Get([]byte("baz")))
		return nil
	})
}

type failWriterError struct{}

func (failWriterError) Error() string {
	return "error injected for tests"
}

type failWriter struct {
	// fail after this many bytes
	After int
}

func (f *failWriter) Write(p []byte) (n int, err error) {
	n = len(p)
	if n > f.After {
		n = f.After
		err = failWriterError{}
	}
	f.After -= n
	return n, err
}

// Ensure that Copy handles write errors right.
func TestTx_CopyFile_Error_Meta(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		tx.Bucket([]byte("widgets")).Put([]byte("baz"), []byte("bat"))
		return nil
	})

	err := db.View(func(tx *bolt.Tx) error { return tx.Copy(&failWriter{}) })
	equals(t, err.Error(), "meta copy: error injected for tests")
}

// Ensure that Copy handles write errors right.
func TestTx_CopyFile_Error_Normal(t *testing.T) {
	db := NewTestDB()
	defer db.Close()
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		tx.Bucket([]byte("widgets")).Put([]byte("baz"), []byte("bat"))
		return nil
	})

	err := db.View(func(tx *bolt.Tx) error { return tx.Copy(&failWriter{3 * db.Info().PageSize}) })
	equals(t, err.Error(), "error injected for tests")
}

func ExampleTx_Rollback() {
	// Open the database.
	db, _ := bolt.Open(tempfile(), 0666, nil)
	defer os.Remove(db.Path())
	defer db.Close()

	// Create a bucket.
	db.Update(func(tx *bolt.Tx) error {
		_, err := tx.CreateBucket([]byte("widgets"))
		return err
	})

	// Set a value for a key.
	db.Update(func(tx *bolt.Tx) error {
		return tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
	})

	// Update the key but rollback the transaction so it never saves.
	tx, _ := db.Begin(true)
	b := tx.Bucket([]byte("widgets"))
	b.Put([]byte("foo"), []byte("baz"))
	tx.Rollback()

	// Ensure that our original value is still set.
	db.View(func(tx *bolt.Tx) error {
		value := tx.Bucket([]byte("widgets")).Get([]byte("foo"))
		fmt.Printf("The value for 'foo' is still: %s\n", value)
		return nil
	})

	// Output:
	// The value for 'foo' is still: bar
}

func ExampleTx_CopyFile() {
	// Open the database.
	db, _ := bolt.Open(tempfile(), 0666, nil)
	defer os.Remove(db.Path())
	defer db.Close()

	// Create a bucket and a key.
	db.Update(func(tx *bolt.Tx) error {
		tx.CreateBucket([]byte("widgets"))
		tx.Bucket([]byte("widgets")).Put([]byte("foo"), []byte("bar"))
		return nil
	})

	// Copy the database to another file.
	toFile := tempfile()
	db.View(func(tx *bolt.Tx) error { return tx.CopyFile(toFile, 0666) })
	defer os.Remove(toFile)

	// Open the cloned database.
	db2, _ := bolt.Open(toFile, 0666, nil)
	defer db2.Close()

	// Ensure that the key exists in the copy.
	db2.View(func(tx *bolt.Tx) error {
		value := tx.Bucket([]byte("widgets")).Get([]byte("foo"))
		fmt.Printf("The value for 'foo' in the clone is: %s\n", value)
		return nil
	})

	// Output:
	// The value for 'foo' in the clone is: bar
}
