// Copyright (c) 2015-2016 The btcsuite developers
// Copyright (c) 2016-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

// This file intended to be copied into each backend driver directory.  Each
// driver should have their own driver_test.go file which creates a database and
// invokes the testInterface function in this file to ensure the driver properly
// implements the interface.
//
// NOTE: When copying this file into the backend driver folder, the package name
// will need to be changed accordingly.

package ffldb_test

import (
	"bytes"
	"compress/bzip2"
	"encoding/gob"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"sync/atomic"
	"testing"
	"time"

	"github.com/decred/dcrd/chaincfg/chainhash"
	"github.com/decred/dcrd/database/v3"
	"github.com/decred/dcrd/dcrutil/v4"
	"github.com/decred/dcrd/wire"
)

var (
	// blockDataNet is the expected network in the test block data.
	blockDataNet = wire.SimNet

	// blockDataFile is the path to a file containing the first 168 blocks
	// of the simulation network.
	blockDataFile = filepath.Join("..", "testdata", "blocks0to168.bz2")

	// errSubTestFail is used to signal that a sub test returned false.
	errSubTestFail = fmt.Errorf("sub test failure")
)

// loadBlocks loads the blocks contained in the testdata directory and returns
// a slice of them.
func loadBlocks(t *testing.T, dataFile string, network wire.CurrencyNet) ([]*dcrutil.Block, error) {
	t.Helper()

	// Open the file that contains the blocks for reading.
	fi, err := os.Open(dataFile)
	if err != nil {
		t.Errorf("failed to open file %v, err %v", dataFile, err)
		return nil, err
	}
	defer func() {
		if err := fi.Close(); err != nil {
			t.Errorf("failed to close file %v %v", dataFile,
				err)
		}
	}()

	bcStream := bzip2.NewReader(fi)

	// Create a buffer of the read file.
	bcBuf := new(bytes.Buffer)
	bcBuf.ReadFrom(bcStream)

	// Create decoder from the buffer and a map to store the data.
	bcDecoder := gob.NewDecoder(bcBuf)
	blockChain := make(map[int64][]byte)

	// Decode the blockchain into the map.
	if err := bcDecoder.Decode(&blockChain); err != nil {
		t.Errorf("error decoding test blockchain: %v", err.Error())
	}

	// Fetch blocks 1 to 168 and perform various tests.
	blocks := make([]*dcrutil.Block, 169)
	for i := 0; i <= 168; i++ {
		bl, err := dcrutil.NewBlockFromBytes(blockChain[int64(i)])
		if err != nil {
			t.Errorf("NewBlockFromBytes error: %v", err.Error())
		}

		blocks[i] = bl
	}

	return blocks, nil
}

// checkDbError ensures the passed error is a database.Error that matches the
// passed error kind.
func checkDbError(t *testing.T, testName string, gotErr error, wantErr database.ErrorKind) bool {
	t.Helper()

	if !errors.Is(gotErr, wantErr) {
		t.Errorf("%s: unexpected error code - got %v, want %v",
			testName, gotErr, wantErr)
		return false
	}

	return true
}

// testContext is used to store context information about a running test which
// is passed into helper functions.
type testContext struct {
	t           *testing.T
	db          database.DB
	bucketDepth int
	isWritable  bool
	blocks      []*dcrutil.Block
}

// keyPair houses a key/value pair.  It is used over maps so ordering can be
// maintained.
type keyPair struct {
	key   []byte
	value []byte
}

// lookupKey is a convenience method to lookup the requested key from the
// provided keypair slice along with whether or not the key was found.
func lookupKey(key []byte, values []keyPair) ([]byte, bool) {
	for _, item := range values {
		if bytes.Equal(item.key, key) {
			return item.value, true
		}
	}

	return nil, false
}

// toGetValues returns a copy of the provided keypairs with all of the nil
// values set to an empty byte slice.  This is used to ensure that keys set to
// nil values result in empty byte slices when retrieved instead of nil.
func toGetValues(values []keyPair) []keyPair {
	ret := make([]keyPair, len(values))
	copy(ret, values)
	for i := range ret {
		if ret[i].value == nil {
			ret[i].value = make([]byte, 0)
		}
	}
	return ret
}

// rollbackValues returns a copy of the provided keypairs with all values set to
// nil.  This is used to test that values are properly rolled back.
func rollbackValues(values []keyPair) []keyPair {
	ret := make([]keyPair, len(values))
	copy(ret, values)
	for i := range ret {
		ret[i].value = nil
	}
	return ret
}

// testCursorKeyPair checks that the provide key and value match the expected
// keypair at the provided index.  It also ensures the index is in range for the
// provided slice of expected keypairs.
func testCursorKeyPair(tc *testContext, k, v []byte, index int, values []keyPair) bool {
	if index >= len(values) || index < 0 {
		tc.t.Errorf("Cursor: exceeded the expected range of values - "+
			"index %d, num values %d", index, len(values))
		return false
	}

	pair := &values[index]
	if !bytes.Equal(k, pair.key) {
		tc.t.Errorf("Mismatched cursor key: index %d does not match "+
			"the expected key - got %q, want %q", index, k,
			pair.key)
		return false
	}
	if !bytes.Equal(v, pair.value) {
		tc.t.Errorf("Mismatched cursor value: index %d does not match "+
			"the expected value - got %q, want %q", index, v,
			pair.value)
		return false
	}

	return true
}

// testGetValues checks that all of the provided key/value pairs can be
// retrieved from the database and the retrieved values match the provided
// values.
func testGetValues(tc *testContext, bucket database.Bucket, values []keyPair) bool {
	for _, item := range values {
		gotValue := bucket.Get(item.key)
		if !reflect.DeepEqual(gotValue, item.value) {
			tc.t.Errorf("Get: unexpected value for %q - got %q, "+
				"want %q", item.key, gotValue, item.value)
			return false
		}
	}

	return true
}

// testPutValues stores all of the provided key/value pairs in the provided
// bucket while checking for errors.
func testPutValues(tc *testContext, bucket database.Bucket, values []keyPair) bool {
	for _, item := range values {
		if err := bucket.Put(item.key, item.value); err != nil {
			tc.t.Errorf("Put: unexpected error: %v", err)
			return false
		}
	}

	return true
}

// testDeleteValues removes all of the provided key/value pairs from the
// provided bucket.
func testDeleteValues(tc *testContext, bucket database.Bucket, values []keyPair) bool {
	for _, item := range values {
		if err := bucket.Delete(item.key); err != nil {
			tc.t.Errorf("Delete: unexpected error: %v", err)
			return false
		}
	}

	return true
}

// testCursorInterface ensures the cursor interface is working properly by
// exercising all of its functions on the passed bucket.
func testCursorInterface(tc *testContext, bucket database.Bucket) bool {
	// Ensure a cursor can be obtained for the bucket.
	cursor := bucket.Cursor()
	if cursor == nil {
		tc.t.Error("Bucket.Cursor: unexpected nil cursor returned")
		return false
	}

	// Ensure the cursor returns the same bucket it was created for.
	if cursor.Bucket() != bucket {
		tc.t.Error("Cursor.Bucket: does not match the bucket it was " +
			"created for")
		return false
	}

	if tc.isWritable {
		unsortedValues := []keyPair{
			{[]byte("cursor"), []byte("val1")},
			{[]byte("abcd"), []byte("val2")},
			{[]byte("bcd"), []byte("val3")},
			{[]byte("defg"), nil},
		}
		sortedValues := []keyPair{
			{[]byte("abcd"), []byte("val2")},
			{[]byte("bcd"), []byte("val3")},
			{[]byte("cursor"), []byte("val1")},
			{[]byte("defg"), nil},
		}

		// Store the values to be used in the cursor tests in unsorted
		// order and ensure they were actually stored.
		if !testPutValues(tc, bucket, unsortedValues) {
			return false
		}
		if !testGetValues(tc, bucket, toGetValues(unsortedValues)) {
			return false
		}

		// Ensure the cursor returns all items in byte-sorted order when
		// iterating forward.
		curIdx := 0
		for ok := cursor.First(); ok; ok = cursor.Next() {
			k, v := cursor.Key(), cursor.Value()
			if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
				return false
			}
			curIdx++
		}
		if curIdx != len(unsortedValues) {
			tc.t.Errorf("Cursor: expected to iterate %d values, "+
				"but only iterated %d", len(unsortedValues),
				curIdx)
			return false
		}

		// Ensure the cursor returns all items in reverse byte-sorted
		// order when iterating in reverse.
		curIdx = len(sortedValues) - 1
		for ok := cursor.Last(); ok; ok = cursor.Prev() {
			k, v := cursor.Key(), cursor.Value()
			if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
				return false
			}
			curIdx--
		}
		if curIdx > -1 {
			tc.t.Errorf("Reverse cursor: expected to iterate %d "+
				"values, but only iterated %d",
				len(sortedValues), len(sortedValues)-(curIdx+1))
			return false
		}

		// Ensure forward iteration works as expected after seeking.
		middleIdx := (len(sortedValues) - 1) / 2
		seekKey := sortedValues[middleIdx].key
		curIdx = middleIdx
		for ok := cursor.Seek(seekKey); ok; ok = cursor.Next() {
			k, v := cursor.Key(), cursor.Value()
			if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
				return false
			}
			curIdx++
		}
		if curIdx != len(sortedValues) {
			tc.t.Errorf("Cursor after seek: expected to iterate "+
				"%d values, but only iterated %d",
				len(sortedValues)-middleIdx, curIdx-middleIdx)
			return false
		}

		// Ensure reverse iteration works as expected after seeking.
		curIdx = middleIdx
		for ok := cursor.Seek(seekKey); ok; ok = cursor.Prev() {
			k, v := cursor.Key(), cursor.Value()
			if !testCursorKeyPair(tc, k, v, curIdx, sortedValues) {
				return false
			}
			curIdx--
		}
		if curIdx > -1 {
			tc.t.Errorf("Reverse cursor after seek: expected to "+
				"iterate %d values, but only iterated %d",
				len(sortedValues)-middleIdx, middleIdx-curIdx)
			return false
		}

		// Ensure the cursor deletes items properly.
		if !cursor.First() {
			tc.t.Errorf("Cursor.First: no value")
			return false
		}
		k := cursor.Key()
		if err := cursor.Delete(); err != nil {
			tc.t.Errorf("Cursor.Delete: unexpected error: %v", err)
			return false
		}
		if val := bucket.Get(k); val != nil {
			tc.t.Errorf("Cursor.Delete: value for key %q was not "+
				"deleted", k)
			return false
		}
	}

	return true
}

// testNestedBucket reruns the testBucketInterface against a nested bucket along
// with a counter to only test a couple of level deep.
func testNestedBucket(tc *testContext, testBucket database.Bucket) bool {
	// Don't go more than 2 nested levels deep.
	if tc.bucketDepth > 1 {
		return true
	}

	tc.bucketDepth++
	defer func() {
		tc.bucketDepth--
	}()

	return testBucketInterface(tc, testBucket)
}

// testBucketInterface ensures the bucket interface is working properly by
// exercising all of its functions.  This includes the cursor interface for the
// cursor returned from the bucket.
func testBucketInterface(tc *testContext, bucket database.Bucket) bool {
	if bucket.Writable() != tc.isWritable {
		tc.t.Errorf("Bucket writable state does not match.")
		return false
	}

	if tc.isWritable {
		// keyValues holds the keys and values to use when putting
		// values into the bucket.
		keyValues := []keyPair{
			{[]byte("bucketkey1"), []byte("foo1")},
			{[]byte("bucketkey2"), []byte("foo2")},
			{[]byte("bucketkey3"), []byte("foo3")},
			{[]byte("bucketkey4"), nil},
		}
		expectedKeyValues := toGetValues(keyValues)
		if !testPutValues(tc, bucket, keyValues) {
			return false
		}

		if !testGetValues(tc, bucket, expectedKeyValues) {
			return false
		}

		// Ensure errors returned from the user-supplied ForEach
		// function are returned.
		forEachError := fmt.Errorf("example foreach error")
		err := bucket.ForEach(func(k, v []byte) error {
			return forEachError
		})
		if !errors.Is(err, forEachError) {
			tc.t.Errorf("ForEach: inner function error not "+
				"returned - got %v, want %v", err, forEachError)
			return false
		}

		// Iterate all of the keys using ForEach while making sure the
		// stored values are the expected values.
		keysFound := make(map[string]struct{}, len(keyValues))
		err = bucket.ForEach(func(k, v []byte) error {
			wantV, found := lookupKey(k, expectedKeyValues)
			if !found {
				return fmt.Errorf("ForEach: key '%s' should "+
					"exist", k)
			}

			if !reflect.DeepEqual(v, wantV) {
				return fmt.Errorf("ForEach: value for key '%s' "+
					"does not match - got %s, want %s", k,
					v, wantV)
			}

			keysFound[string(k)] = struct{}{}
			return nil
		})
		if err != nil {
			tc.t.Errorf("%v", err)
			return false
		}

		// Ensure all keys were iterated.
		for _, item := range keyValues {
			if _, ok := keysFound[string(item.key)]; !ok {
				tc.t.Errorf("ForEach: key '%s' was not iterated "+
					"when it should have been", item.key)
				return false
			}
		}

		// Delete the keys and ensure they were deleted.
		if !testDeleteValues(tc, bucket, keyValues) {
			return false
		}
		if !testGetValues(tc, bucket, rollbackValues(keyValues)) {
			return false
		}

		// Ensure creating a new bucket works as expected.
		testBucketName := []byte("testbucket")
		testBucket, err := bucket.CreateBucket(testBucketName)
		if err != nil {
			tc.t.Errorf("CreateBucket: unexpected error: %v", err)
			return false
		}
		if !testNestedBucket(tc, testBucket) {
			return false
		}

		// Ensure errors returned from the user-supplied ForEachBucket
		// function are returned.
		err = bucket.ForEachBucket(func(k []byte) error {
			return forEachError
		})
		if !errors.Is(err, forEachError) {
			tc.t.Errorf("ForEachBucket: inner function error not "+
				"returned - got %v, want %v", err, forEachError)
			return false
		}

		// Ensure creating a bucket that already exists fails with the
		// expected error.
		wantErrKind := database.ErrBucketExists
		_, err = bucket.CreateBucket(testBucketName)
		if !checkDbError(tc.t, "CreateBucket", err, wantErrKind) {
			return false
		}

		// Ensure CreateBucketIfNotExists returns an existing bucket.
		testBucket, err = bucket.CreateBucketIfNotExists(testBucketName)
		if err != nil {
			tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
				"error: %v", err)
			return false
		}
		if !testNestedBucket(tc, testBucket) {
			return false
		}

		// Ensure retrieving an existing bucket works as expected.
		testBucket = bucket.Bucket(testBucketName)
		if !testNestedBucket(tc, testBucket) {
			return false
		}

		// Ensure deleting a bucket works as intended.
		if err := bucket.DeleteBucket(testBucketName); err != nil {
			tc.t.Errorf("DeleteBucket: unexpected error: %v", err)
			return false
		}
		if b := bucket.Bucket(testBucketName); b != nil {
			tc.t.Errorf("DeleteBucket: bucket '%s' still exists",
				testBucketName)
			return false
		}

		// Ensure deleting a bucket that doesn't exist returns the
		// expected error.
		wantErrKind = database.ErrBucketNotFound
		err = bucket.DeleteBucket(testBucketName)
		if !checkDbError(tc.t, "DeleteBucket", err, wantErrKind) {
			return false
		}

		// Ensure CreateBucketIfNotExists creates a new bucket when
		// it doesn't already exist.
		testBucket, err = bucket.CreateBucketIfNotExists(testBucketName)
		if err != nil {
			tc.t.Errorf("CreateBucketIfNotExists: unexpected "+
				"error: %v", err)
			return false
		}
		if !testNestedBucket(tc, testBucket) {
			return false
		}

		// Ensure the cursor interface works as expected.
		if !testCursorInterface(tc, testBucket) {
			return false
		}

		// Delete the test bucket to avoid leaving it around for future
		// calls.
		if err := bucket.DeleteBucket(testBucketName); err != nil {
			tc.t.Errorf("DeleteBucket: unexpected error: %v", err)
			return false
		}
		if b := bucket.Bucket(testBucketName); b != nil {
			tc.t.Errorf("DeleteBucket: bucket '%s' still exists",
				testBucketName)
			return false
		}
	} else {
		// Put should fail with bucket that is not writable.
		testName := "unwritable tx put"
		wantErrKind := database.ErrTxNotWritable
		failBytes := []byte("fail")
		err := bucket.Put(failBytes, failBytes)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Delete should fail with bucket that is not writable.
		testName = "unwritable tx delete"
		err = bucket.Delete(failBytes)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// CreateBucket should fail with bucket that is not writable.
		testName = "unwritable tx create bucket"
		_, err = bucket.CreateBucket(failBytes)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// CreateBucketIfNotExists should fail with bucket that is not
		// writable.
		testName = "unwritable tx create bucket if not exists"
		_, err = bucket.CreateBucketIfNotExists(failBytes)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// DeleteBucket should fail with bucket that is not writable.
		testName = "unwritable tx delete bucket"
		err = bucket.DeleteBucket(failBytes)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure the cursor interface works as expected with read-only
		// buckets.
		if !testCursorInterface(tc, bucket) {
			return false
		}
	}

	return true
}

// rollbackOnPanic rolls the passed transaction back if the code in the calling
// function panics.  This is useful in case the tests unexpectedly panic which
// would leave any manually created transactions with the database mutex locked
// thereby leading to a deadlock and masking the real reason for the panic.  It
// also logs a test error and repanics so the original panic can be traced.
func rollbackOnPanic(t *testing.T, tx database.Tx) {
	t.Helper()

	if err := recover(); err != nil {
		t.Errorf("Unexpected panic: %v", err)
		_ = tx.Rollback()
		panic(err)
	}
}

// testMetadataManualTxInterface ensures that the manual transactions metadata
// interface works as expected.
func testMetadataManualTxInterface(tc *testContext) bool {
	// populateValues tests that populating values works as expected.
	//
	// When the writable flag is false, a read-only transaction is created,
	// standard bucket tests for read-only transactions are performed, and
	// the Commit function is checked to ensure it fails as expected.
	//
	// Otherwise, a read-write transaction is created, the values are
	// written, standard bucket tests for read-write transactions are
	// performed, and then the transaction is either committed or rolled
	// back depending on the flag.
	bucket1Name := []byte("bucket1")
	populateValues := func(writable, rollback bool, putValues []keyPair) bool {
		tx, err := tc.db.Begin(writable)
		if err != nil {
			tc.t.Errorf("Begin: unexpected error %v", err)
			return false
		}
		defer rollbackOnPanic(tc.t, tx)

		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			tc.t.Errorf("Metadata: unexpected nil bucket")
			_ = tx.Rollback()
			return false
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			tc.t.Errorf("Bucket1: unexpected nil bucket")
			return false
		}

		tc.isWritable = writable
		if !testBucketInterface(tc, bucket1) {
			_ = tx.Rollback()
			return false
		}

		if !writable {
			// The transaction is not writable, so it should fail
			// the commit.
			testName := "unwritable tx commit"
			wantErrKind := database.ErrTxNotWritable
			err := tx.Commit()
			if !checkDbError(tc.t, testName, err, wantErrKind) {
				_ = tx.Rollback()
				return false
			}
		} else {
			if !testPutValues(tc, bucket1, putValues) {
				return false
			}

			if rollback {
				// Rollback the transaction.
				if err := tx.Rollback(); err != nil {
					tc.t.Errorf("Rollback: unexpected "+
						"error %v", err)
					return false
				}
			} else {
				// The commit should succeed.
				if err := tx.Commit(); err != nil {
					tc.t.Errorf("Commit: unexpected error "+
						"%v", err)
					return false
				}
			}
		}

		return true
	}

	// checkValues starts a read-only transaction and checks that all of
	// the key/value pairs specified in the expectedValues parameter match
	// what's in the database.
	checkValues := func(expectedValues []keyPair) bool {
		tx, err := tc.db.Begin(false)
		if err != nil {
			tc.t.Errorf("Begin: unexpected error %v", err)
			return false
		}
		defer rollbackOnPanic(tc.t, tx)

		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			tc.t.Errorf("Metadata: unexpected nil bucket")
			_ = tx.Rollback()
			return false
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			tc.t.Errorf("Bucket1: unexpected nil bucket")
			return false
		}

		if !testGetValues(tc, bucket1, expectedValues) {
			_ = tx.Rollback()
			return false
		}

		// Rollback the read-only transaction.
		if err := tx.Rollback(); err != nil {
			tc.t.Errorf("Commit: unexpected error %v", err)
			return false
		}

		return true
	}

	// deleteValues starts a read-write transaction and deletes the keys
	// in the passed key/value pairs.
	deleteValues := func(values []keyPair) bool {
		tx, err := tc.db.Begin(true)
		if err != nil {
			tc.t.Errorf("Begin: unexpected error %v", err)
			return false
		}
		defer rollbackOnPanic(tc.t, tx)

		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			tc.t.Errorf("Metadata: unexpected nil bucket")
			_ = tx.Rollback()
			return false
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			tc.t.Errorf("Bucket1: unexpected nil bucket")
			return false
		}

		// Delete the keys and ensure they were deleted.
		if !testDeleteValues(tc, bucket1, values) {
			_ = tx.Rollback()
			return false
		}
		if !testGetValues(tc, bucket1, rollbackValues(values)) {
			_ = tx.Rollback()
			return false
		}

		// Commit the changes and ensure it was successful.
		if err := tx.Commit(); err != nil {
			tc.t.Errorf("Commit: unexpected error %v", err)
			return false
		}

		return true
	}

	// keyValues holds the keys and values to use when putting values into a
	// bucket.
	var keyValues = []keyPair{
		{[]byte("umtxkey1"), []byte("foo1")},
		{[]byte("umtxkey2"), []byte("foo2")},
		{[]byte("umtxkey3"), []byte("foo3")},
		{[]byte("umtxkey4"), nil},
	}

	// Ensure that attempting populating the values using a read-only
	// transaction fails as expected.
	if !populateValues(false, true, keyValues) {
		return false
	}
	if !checkValues(rollbackValues(keyValues)) {
		return false
	}

	// Ensure that attempting populating the values using a read-write
	// transaction and then rolling it back yields the expected values.
	if !populateValues(true, true, keyValues) {
		return false
	}
	if !checkValues(rollbackValues(keyValues)) {
		return false
	}

	// Ensure that attempting populating the values using a read-write
	// transaction and then committing it stores the expected values.
	if !populateValues(true, false, keyValues) {
		return false
	}
	if !checkValues(toGetValues(keyValues)) {
		return false
	}

	// Clean up the keys.
	if !deleteValues(keyValues) {
		return false
	}

	return true
}

// testManagedTxPanics ensures calling Rollback of Commit inside a managed
// transaction panics.
func testManagedTxPanics(tc *testContext) bool {
	testPanic := func(fn func()) (paniced bool) {
		// Setup a defer to catch the expected panic and update the
		// return variable.
		defer func() {
			if err := recover(); err != nil {
				paniced = true
			}
		}()

		fn()
		return false
	}

	// Ensure calling Commit on a managed read-only transaction panics.
	paniced := testPanic(func() {
		tc.db.View(func(tx database.Tx) error {
			tx.Commit()
			return nil
		})
	})
	if !paniced {
		tc.t.Error("Commit called inside View did not panic")
		return false
	}

	// Ensure calling Rollback on a managed read-only transaction panics.
	paniced = testPanic(func() {
		tc.db.View(func(tx database.Tx) error {
			tx.Rollback()
			return nil
		})
	})
	if !paniced {
		tc.t.Error("Rollback called inside View did not panic")
		return false
	}

	// Ensure calling Commit on a managed read-write transaction panics.
	paniced = testPanic(func() {
		tc.db.Update(func(tx database.Tx) error {
			tx.Commit()
			return nil
		})
	})
	if !paniced {
		tc.t.Error("Commit called inside Update did not panic")
		return false
	}

	// Ensure calling Rollback on a managed read-write transaction panics.
	paniced = testPanic(func() {
		tc.db.Update(func(tx database.Tx) error {
			tx.Rollback()
			return nil
		})
	})
	if !paniced {
		tc.t.Error("Rollback called inside Update did not panic")
		return false
	}

	return true
}

// testMetadataTxInterface tests all facets of the managed read/write and
// manual transaction metadata interfaces as well as the bucket interfaces under
// them.
func testMetadataTxInterface(tc *testContext) bool {
	if !testManagedTxPanics(tc) {
		return false
	}

	bucket1Name := []byte("bucket1")
	err := tc.db.Update(func(tx database.Tx) error {
		_, err := tx.Metadata().CreateBucket(bucket1Name)
		return err
	})
	if err != nil {
		tc.t.Errorf("Update: unexpected error creating bucket: %v", err)
		return false
	}

	if !testMetadataManualTxInterface(tc) {
		return false
	}

	// keyValues holds the keys and values to use when putting values
	// into a bucket.
	keyValues := []keyPair{
		{[]byte("mtxkey1"), []byte("foo1")},
		{[]byte("mtxkey2"), []byte("foo2")},
		{[]byte("mtxkey3"), []byte("foo3")},
		{[]byte("mtxkey4"), nil},
	}

	// Test the bucket interface via a managed read-only transaction.
	err = tc.db.View(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			return fmt.Errorf("bucket1: unexpected nil bucket")
		}

		tc.isWritable = false
		if !testBucketInterface(tc, bucket1) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Ensure errors returned from the user-supplied View function are
	// returned.
	viewError := fmt.Errorf("example view error")
	err = tc.db.View(func(tx database.Tx) error {
		return viewError
	})
	if !errors.Is(err, viewError) {
		tc.t.Errorf("View: inner function error not returned - got "+
			"%v, want %v", err, viewError)
		return false
	}

	// Test the bucket interface via a managed read-write transaction.
	// Also, put a series of values and force a rollback so the following
	// code can ensure the values were not stored.
	forceRollbackError := fmt.Errorf("force rollback")
	err = tc.db.Update(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			return fmt.Errorf("bucket1: unexpected nil bucket")
		}

		tc.isWritable = true
		if !testBucketInterface(tc, bucket1) {
			return errSubTestFail
		}

		if !testPutValues(tc, bucket1, keyValues) {
			return errSubTestFail
		}

		// Return an error to force a rollback.
		return forceRollbackError
	})
	if !errors.Is(err, forceRollbackError) {
		if errors.Is(err, errSubTestFail) {
			return false
		}

		tc.t.Errorf("Update: inner function error not returned - got "+
			"%v, want %v", err, forceRollbackError)
		return false
	}

	// Ensure the values that should not have been stored due to the forced
	// rollback above were not actually stored.
	err = tc.db.View(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		if !testGetValues(tc, metadataBucket, rollbackValues(keyValues)) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Store a series of values via a managed read-write transaction.
	err = tc.db.Update(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			return fmt.Errorf("bucket1: unexpected nil bucket")
		}

		if !testPutValues(tc, bucket1, keyValues) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Ensure the values stored above were committed as expected.
	err = tc.db.View(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			return fmt.Errorf("bucket1: unexpected nil bucket")
		}

		if !testGetValues(tc, bucket1, toGetValues(keyValues)) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Clean up the values stored above in a managed read-write transaction.
	err = tc.db.Update(func(tx database.Tx) error {
		metadataBucket := tx.Metadata()
		if metadataBucket == nil {
			return fmt.Errorf("Metadata: unexpected nil bucket")
		}

		bucket1 := metadataBucket.Bucket(bucket1Name)
		if bucket1 == nil {
			return fmt.Errorf("bucket1: unexpected nil bucket")
		}

		if !testDeleteValues(tc, bucket1, keyValues) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	return true
}

// testFetchBlockIOMissing ensures that all of the block retrieval API functions
// work as expected when requesting blocks that don't exist.
func testFetchBlockIOMissing(tc *testContext, tx database.Tx) bool {
	wantErrKind := database.ErrBlockNotFound

	// ---------------------
	// Non-bulk Block IO API
	// ---------------------

	// Test the individual block APIs one block at a time to ensure they
	// return the expected error.  Also, build the data needed to test the
	// bulk APIs below while looping.
	allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
	allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
	for i, block := range tc.blocks {
		blockHash := block.Hash()
		allBlockHashes[i] = *blockHash

		txLocs, _, err := block.TxLoc()
		if err != nil {
			tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
				err)
			return false
		}

		// Ensure FetchBlock returns expected error.
		testName := fmt.Sprintf("FetchBlock #%d on missing block", i)
		_, err = tx.FetchBlock(blockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure FetchBlockHeader returns expected error.
		testName = fmt.Sprintf("FetchBlockHeader #%d on missing block",
			i)
		_, err = tx.FetchBlockHeader(blockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure the first transaction fetched as a block region from
		// the database returns the expected error.
		region := database.BlockRegion{
			Hash:   blockHash,
			Offset: uint32(txLocs[0].TxStart),
			Len:    uint32(txLocs[0].TxLen),
		}
		allBlockRegions[i] = region
		_, err = tx.FetchBlockRegion(&region)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure HasBlock returns false.
		hasBlock, err := tx.HasBlock(blockHash)
		if err != nil {
			tc.t.Errorf("HasBlock #%d: unexpected err: %v", i, err)
			return false
		}
		if hasBlock {
			tc.t.Errorf("HasBlock #%d: should not have block", i)
			return false
		}
	}

	// -----------------
	// Bulk Block IO API
	// -----------------

	// Ensure FetchBlocks returns expected error.
	testName := "FetchBlocks on missing blocks"
	_, err := tx.FetchBlocks(allBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure FetchBlockHeaders returns expected error.
	testName = "FetchBlockHeaders on missing blocks"
	_, err = tx.FetchBlockHeaders(allBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure FetchBlockRegions returns expected error.
	testName = "FetchBlockRegions on missing blocks"
	_, err = tx.FetchBlockRegions(allBlockRegions)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure HasBlocks returns false for all blocks.
	hasBlocks, err := tx.HasBlocks(allBlockHashes)
	if err != nil {
		tc.t.Errorf("HasBlocks: unexpected err: %v", err)
	}
	for i, hasBlock := range hasBlocks {
		if hasBlock {
			tc.t.Errorf("HasBlocks #%d: should not have block", i)
			return false
		}
	}

	return true
}

// testFetchBlockIO ensures all of the block retrieval API functions work as
// expected for the provide set of blocks.  The blocks must already be stored in
// the database, or at least stored into the passed transaction.  It also
// tests several error conditions such as ensuring the expected errors are
// returned when fetching blocks, headers, and regions that don't exist.
func testFetchBlockIO(tc *testContext, tx database.Tx) bool {
	// ---------------------
	// Non-bulk Block IO API
	// ---------------------

	// Test the individual block APIs one block at a time.  Also, build the
	// data needed to test the bulk APIs below while looping.
	allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
	allBlockBytes := make([][]byte, len(tc.blocks))
	allBlockTxLocs := make([][]wire.TxLoc, len(tc.blocks))
	allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
	for i, block := range tc.blocks {
		blockHash := block.Hash()
		allBlockHashes[i] = *blockHash

		blockBytes, err := block.Bytes()
		if err != nil {
			tc.t.Errorf("block.Bytes(%d): unexpected error: %v", i,
				err)
			return false
		}
		allBlockBytes[i] = blockBytes

		txLocs, _, err := block.TxLoc()
		if err != nil {
			tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
				err)
			return false
		}
		allBlockTxLocs[i] = txLocs

		// Ensure the block data fetched from the database matches the
		// expected bytes.
		gotBlockBytes, err := tx.FetchBlock(blockHash)
		if err != nil {
			tc.t.Errorf("FetchBlock(%s): unexpected error: %v",
				blockHash, err)
			return false
		}
		if !bytes.Equal(gotBlockBytes, blockBytes) {
			tc.t.Errorf("FetchBlock(%s): bytes mismatch: got %x, "+
				"want %x", blockHash, gotBlockBytes, blockBytes)
			return false
		}

		// Ensure the block header fetched from the database matches the
		// expected bytes.
		wantHeaderBytes := blockBytes[0:wire.MaxBlockHeaderPayload]
		gotHeaderBytes, err := tx.FetchBlockHeader(blockHash)
		if err != nil {
			tc.t.Errorf("FetchBlockHeader(%s): unexpected error: %v",
				blockHash, err)
			return false
		}
		if !bytes.Equal(gotHeaderBytes, wantHeaderBytes) {
			tc.t.Errorf("FetchBlockHeader(%s): bytes mismatch: "+
				"got %x, want %x", blockHash, gotHeaderBytes,
				wantHeaderBytes)
			return false
		}

		// Ensure the first transaction fetched as a block region from
		// the database matches the expected bytes.
		region := database.BlockRegion{
			Hash:   blockHash,
			Offset: uint32(txLocs[0].TxStart),
			Len:    uint32(txLocs[0].TxLen),
		}
		allBlockRegions[i] = region
		endRegionOffset := region.Offset + region.Len
		wantRegionBytes := blockBytes[region.Offset:endRegionOffset]
		gotRegionBytes, err := tx.FetchBlockRegion(&region)
		if err != nil {
			tc.t.Errorf("FetchBlockRegion(%s): unexpected error: %v",
				blockHash, err)
			return false
		}
		if !bytes.Equal(gotRegionBytes, wantRegionBytes) {
			tc.t.Errorf("FetchBlockRegion(%s): bytes mismatch: "+
				"got %x, want %x", blockHash, gotRegionBytes,
				wantRegionBytes)
			return false
		}

		// Ensure the block header fetched from the database matches the
		// expected bytes.
		hasBlock, err := tx.HasBlock(blockHash)
		if err != nil {
			tc.t.Errorf("HasBlock(%s): unexpected error: %v",
				blockHash, err)
			return false
		}
		if !hasBlock {
			tc.t.Errorf("HasBlock(%s): database claims it doesn't "+
				"have the block when it should", blockHash)
			return false
		}

		// -----------------------
		// Invalid blocks/regions.
		// -----------------------

		// Ensure fetching a block that doesn't exist returns the
		// expected error.
		badBlockHash := &chainhash.Hash{}
		testName := fmt.Sprintf("FetchBlock(%s) invalid block",
			badBlockHash)
		wantErrKind := database.ErrBlockNotFound
		_, err = tx.FetchBlock(badBlockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure fetching a block header that doesn't exist returns
		// the expected error.
		testName = fmt.Sprintf("FetchBlockHeader(%s) invalid block",
			badBlockHash)
		_, err = tx.FetchBlockHeader(badBlockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure fetching a block region in a block that doesn't exist
		// return the expected error.
		testName = fmt.Sprintf("FetchBlockRegion(%s) invalid hash",
			badBlockHash)
		wantErrKind = database.ErrBlockNotFound
		region.Hash = badBlockHash
		region.Offset = ^uint32(0)
		_, err = tx.FetchBlockRegion(&region)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure fetching a block region that is out of bounds returns
		// the expected error.
		testName = fmt.Sprintf("FetchBlockRegion(%s) invalid region",
			blockHash)
		wantErrKind = database.ErrBlockRegionInvalid
		region.Hash = blockHash
		region.Offset = ^uint32(0)
		_, err = tx.FetchBlockRegion(&region)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}
	}

	// -----------------
	// Bulk Block IO API
	// -----------------

	// Ensure the bulk block data fetched from the database matches the
	// expected bytes.
	blockData, err := tx.FetchBlocks(allBlockHashes)
	if err != nil {
		tc.t.Errorf("FetchBlocks: unexpected error: %v", err)
		return false
	}
	if len(blockData) != len(allBlockBytes) {
		tc.t.Errorf("FetchBlocks: unexpected number of results - got "+
			"%d, want %d", len(blockData), len(allBlockBytes))
		return false
	}
	for i := 0; i < len(blockData); i++ {
		blockHash := allBlockHashes[i]
		wantBlockBytes := allBlockBytes[i]
		gotBlockBytes := blockData[i]
		if !bytes.Equal(gotBlockBytes, wantBlockBytes) {
			tc.t.Errorf("FetchBlocks(%s): bytes mismatch: got %x, "+
				"want %x", blockHash, gotBlockBytes,
				wantBlockBytes)
			return false
		}
	}

	// Ensure the bulk block headers fetched from the database match the
	// expected bytes.
	blockHeaderData, err := tx.FetchBlockHeaders(allBlockHashes)
	if err != nil {
		tc.t.Errorf("FetchBlockHeaders: unexpected error: %v", err)
		return false
	}
	if len(blockHeaderData) != len(allBlockBytes) {
		tc.t.Errorf("FetchBlockHeaders: unexpected number of results "+
			"- got %d, want %d", len(blockHeaderData),
			len(allBlockBytes))
		return false
	}
	for i := 0; i < len(blockHeaderData); i++ {
		blockHash := allBlockHashes[i]
		wantHeaderBytes := allBlockBytes[i][0:wire.MaxBlockHeaderPayload]
		gotHeaderBytes := blockHeaderData[i]
		if !bytes.Equal(gotHeaderBytes, wantHeaderBytes) {
			tc.t.Errorf("FetchBlockHeaders(%s): bytes mismatch: "+
				"got %x, want %x", blockHash, gotHeaderBytes,
				wantHeaderBytes)
			return false
		}
	}

	// Ensure the first transaction of every block fetched in bulk block
	// regions from the database matches the expected bytes.
	allRegionBytes, err := tx.FetchBlockRegions(allBlockRegions)
	if err != nil {
		tc.t.Errorf("FetchBlockRegions: unexpected error: %v", err)
		return false
	}
	if len(allRegionBytes) != len(allBlockRegions) {
		tc.t.Errorf("FetchBlockRegions: unexpected number of results "+
			"- got %d, want %d", len(allRegionBytes),
			len(allBlockRegions))
		return false
	}
	for i, gotRegionBytes := range allRegionBytes {
		region := &allBlockRegions[i]
		endRegionOffset := region.Offset + region.Len
		wantRegionBytes := blockData[i][region.Offset:endRegionOffset]
		if !bytes.Equal(gotRegionBytes, wantRegionBytes) {
			tc.t.Errorf("FetchBlockRegions(%d): bytes mismatch: "+
				"got %x, want %x", i, gotRegionBytes,
				wantRegionBytes)
			return false
		}
	}

	// Ensure the bulk determination of whether a set of block hashes are in
	// the database returns true for all loaded blocks.
	hasBlocks, err := tx.HasBlocks(allBlockHashes)
	if err != nil {
		tc.t.Errorf("HasBlocks: unexpected error: %v", err)
		return false
	}
	for i, hasBlock := range hasBlocks {
		if !hasBlock {
			tc.t.Errorf("HasBlocks(%d): should have block", i)
			return false
		}
	}

	// -----------------------
	// Invalid blocks/regions.
	// -----------------------

	// Ensure fetching blocks for which one doesn't exist returns the
	// expected error.
	testName := "FetchBlocks invalid hash"
	badBlockHashes := make([]chainhash.Hash, len(allBlockHashes)+1)
	copy(badBlockHashes, allBlockHashes)
	badBlockHashes[len(badBlockHashes)-1] = chainhash.Hash{}
	wantErrKind := database.ErrBlockNotFound
	_, err = tx.FetchBlocks(badBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure fetching block headers for which one doesn't exist returns the
	// expected error.
	testName = "FetchBlockHeaders invalid hash"
	_, err = tx.FetchBlockHeaders(badBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure fetching block regions for which one of blocks doesn't exist
	// returns expected error.
	testName = "FetchBlockRegions invalid hash"
	badBlockRegions := make([]database.BlockRegion, len(allBlockRegions)+1)
	copy(badBlockRegions, allBlockRegions)
	badBlockRegions[len(badBlockRegions)-1].Hash = &chainhash.Hash{}
	wantErrKind = database.ErrBlockNotFound
	_, err = tx.FetchBlockRegions(badBlockRegions)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure fetching block regions that are out of bounds returns the
	// expected error.
	testName = "FetchBlockRegions invalid regions"
	badBlockRegions = badBlockRegions[:len(badBlockRegions)-1]
	for i := range badBlockRegions {
		badBlockRegions[i].Offset = ^uint32(0)
	}
	wantErrKind = database.ErrBlockRegionInvalid
	_, err = tx.FetchBlockRegions(badBlockRegions)
	return checkDbError(tc.t, testName, err, wantErrKind)
}

// testBlockIOTxInterface ensures that the block IO interface works as expected
// for both managed read/write and manual transactions.  This function leaves
// all of the stored blocks in the database.
func testBlockIOTxInterface(tc *testContext) bool {
	// Ensure attempting to store a block with a read-only transaction fails
	// with the expected error.
	err := tc.db.View(func(tx database.Tx) error {
		wantErrKind := database.ErrTxNotWritable
		for i, block := range tc.blocks {
			testName := fmt.Sprintf("StoreBlock(%d) on ro tx", i)
			err := tx.StoreBlock(block)
			if !checkDbError(tc.t, testName, err, wantErrKind) {
				return errSubTestFail
			}
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Populate the database with loaded blocks and ensure all of the data
	// fetching APIs work properly on them within the transaction before a
	// commit or rollback.  Then, force a rollback so the code below can
	// ensure none of the data actually gets stored.
	forceRollbackError := fmt.Errorf("force rollback")
	err = tc.db.Update(func(tx database.Tx) error {
		// Store all blocks in the same transaction.
		for i, block := range tc.blocks {
			err := tx.StoreBlock(block)
			if err != nil {
				tc.t.Errorf("StoreBlock #%d: unexpected error: "+
					"%v", i, err)
				return errSubTestFail
			}
		}

		// Ensure attempting to store the same block again, before the
		// transaction has been committed, returns the expected error.
		wantErrKind := database.ErrBlockExists
		for i, block := range tc.blocks {
			testName := fmt.Sprintf("duplicate block entry #%d "+
				"(before commit)", i)
			err := tx.StoreBlock(block)
			if !checkDbError(tc.t, testName, err, wantErrKind) {
				return errSubTestFail
			}
		}

		// Ensure that all data fetches from the stored blocks before
		// the transaction has been committed work as expected.
		if !testFetchBlockIO(tc, tx) {
			return errSubTestFail
		}

		return forceRollbackError
	})
	if !errors.Is(err, forceRollbackError) {
		if errors.Is(err, errSubTestFail) {
			return false
		}

		tc.t.Errorf("Update: inner function error not returned - got "+
			"%v, want %v", err, forceRollbackError)
		return false
	}

	// Ensure rollback was successful
	err = tc.db.View(func(tx database.Tx) error {
		if !testFetchBlockIOMissing(tc, tx) {
			return errSubTestFail
		}
		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Populate the database with loaded blocks and ensure all of the data
	// fetching APIs work properly.
	err = tc.db.Update(func(tx database.Tx) error {
		// Store a bunch of blocks in the same transaction.
		for i, block := range tc.blocks {
			err := tx.StoreBlock(block)
			if err != nil {
				tc.t.Errorf("StoreBlock #%d: unexpected error: "+
					"%v", i, err)
				return errSubTestFail
			}
		}

		// Ensure attempting to store the same block again while in the
		// same transaction, but before it has been committed, returns
		// the expected error.
		for i, block := range tc.blocks {
			testName := fmt.Sprintf("duplicate block entry #%d "+
				"(before commit)", i)
			wantErrKind := database.ErrBlockExists
			err := tx.StoreBlock(block)
			if !checkDbError(tc.t, testName, err, wantErrKind) {
				return errSubTestFail
			}
		}

		// Ensure that all data fetches from the stored blocks before
		// the transaction has been committed work as expected.
		if !testFetchBlockIO(tc, tx) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Ensure all data fetch tests work as expected using a managed
	// read-only transaction after the data was successfully committed
	// above.
	err = tc.db.View(func(tx database.Tx) error {
		if !testFetchBlockIO(tc, tx) {
			return errSubTestFail
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	// Ensure all data fetch tests work as expected using a managed
	// read-write transaction after the data was successfully committed
	// above.
	err = tc.db.Update(func(tx database.Tx) error {
		if !testFetchBlockIO(tc, tx) {
			return errSubTestFail
		}

		// Ensure attempting to store existing blocks again returns the
		// expected error.  Note that this is different from the
		// previous version since this is a new transaction after the
		// blocks have been committed.
		wantErrKind := database.ErrBlockExists
		for i, block := range tc.blocks {
			testName := fmt.Sprintf("duplicate block entry #%d "+
				"(before commit)", i)
			err := tx.StoreBlock(block)
			if !checkDbError(tc.t, testName, err, wantErrKind) {
				return errSubTestFail
			}
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errSubTestFail) {
			tc.t.Errorf("%v", err)
		}
		return false
	}

	return true
}

// testClosedTxInterface ensures that both the metadata and block IO API
// functions behave as expected when attempted against a closed transaction.
func testClosedTxInterface(tc *testContext, tx database.Tx) bool {
	wantErrKind := database.ErrTxClosed
	bucket := tx.Metadata()
	cursor := tx.Metadata().Cursor()
	bucketName := []byte("closedtxbucket")
	keyName := []byte("closedtxkey")

	// ------------
	// Metadata API
	// ------------

	// Ensure that attempting to get an existing bucket returns nil when the
	// transaction is closed.
	if b := bucket.Bucket(bucketName); b != nil {
		tc.t.Errorf("Bucket: did not return nil on closed tx")
		return false
	}

	// Ensure CreateBucket returns expected error.
	testName := "CreateBucket on closed tx"
	_, err := bucket.CreateBucket(bucketName)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure CreateBucketIfNotExists returns expected error.
	testName = "CreateBucketIfNotExists on closed tx"
	_, err = bucket.CreateBucketIfNotExists(bucketName)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure Delete returns expected error.
	testName = "Delete on closed tx"
	err = bucket.Delete(keyName)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure DeleteBucket returns expected error.
	testName = "DeleteBucket on closed tx"
	err = bucket.DeleteBucket(bucketName)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure ForEach returns expected error.
	testName = "ForEach on closed tx"
	err = bucket.ForEach(nil)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure ForEachBucket returns expected error.
	testName = "ForEachBucket on closed tx"
	err = bucket.ForEachBucket(nil)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure Get returns expected error.
	if k := bucket.Get(keyName); k != nil {
		tc.t.Errorf("Get: did not return nil on closed tx")
		return false
	}

	// Ensure Put returns expected error.
	testName = "Put on closed tx"
	err = bucket.Put(keyName, []byte("test"))
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// -------------------
	// Metadata Cursor API
	// -------------------

	// Ensure attempting to get a bucket from a cursor on a closed tx gives
	// back nil.
	if b := cursor.Bucket(); b != nil {
		tc.t.Error("Cursor.Bucket: returned non-nil on closed tx")
		return false
	}

	// Ensure Cursor.Delete returns expected error.
	testName = "Cursor.Delete on closed tx"
	err = cursor.Delete()
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure Cursor.First on a closed tx returns false and nil key/value.
	if cursor.First() {
		tc.t.Error("Cursor.First: claims ok on closed tx")
		return false
	}
	if cursor.Key() != nil || cursor.Value() != nil {
		tc.t.Error("Cursor.First: key and/or value are not nil on " +
			"closed tx")
		return false
	}

	// Ensure Cursor.Last on a closed tx returns false and nil key/value.
	if cursor.Last() {
		tc.t.Error("Cursor.Last: claims ok on closed tx")
		return false
	}
	if cursor.Key() != nil || cursor.Value() != nil {
		tc.t.Error("Cursor.Last: key and/or value are not nil on " +
			"closed tx")
		return false
	}

	// Ensure Cursor.Next on a closed tx returns false and nil key/value.
	if cursor.Next() {
		tc.t.Error("Cursor.Next: claims ok on closed tx")
		return false
	}
	if cursor.Key() != nil || cursor.Value() != nil {
		tc.t.Error("Cursor.Next: key and/or value are not nil on " +
			"closed tx")
		return false
	}

	// Ensure Cursor.Prev on a closed tx returns false and nil key/value.
	if cursor.Prev() {
		tc.t.Error("Cursor.Prev: claims ok on closed tx")
		return false
	}
	if cursor.Key() != nil || cursor.Value() != nil {
		tc.t.Error("Cursor.Prev: key and/or value are not nil on " +
			"closed tx")
		return false
	}

	// Ensure Cursor.Seek on a closed tx returns false and nil key/value.
	if cursor.Seek([]byte{}) {
		tc.t.Error("Cursor.Seek: claims ok on closed tx")
		return false
	}
	if cursor.Key() != nil || cursor.Value() != nil {
		tc.t.Error("Cursor.Seek: key and/or value are not nil on " +
			"closed tx")
		return false
	}

	// ---------------------
	// Non-bulk Block IO API
	// ---------------------

	// Test the individual block APIs one block at a time to ensure they
	// return the expected error.  Also, build the data needed to test the
	// bulk APIs below while looping.
	allBlockHashes := make([]chainhash.Hash, len(tc.blocks))
	allBlockRegions := make([]database.BlockRegion, len(tc.blocks))
	for i, block := range tc.blocks {
		blockHash := block.Hash()
		allBlockHashes[i] = *blockHash

		txLocs, _, err := block.TxLoc()
		if err != nil {
			tc.t.Errorf("block.TxLoc(%d): unexpected error: %v", i,
				err)
			return false
		}

		// Ensure StoreBlock returns expected error.
		testName = "StoreBlock on closed tx"
		err = tx.StoreBlock(block)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure FetchBlock returns expected error.
		testName = fmt.Sprintf("FetchBlock #%d on closed tx", i)
		_, err = tx.FetchBlock(blockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure FetchBlockHeader returns expected error.
		testName = fmt.Sprintf("FetchBlockHeader #%d on closed tx", i)
		_, err = tx.FetchBlockHeader(blockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure the first transaction fetched as a block region from
		// the database returns the expected error.
		region := database.BlockRegion{
			Hash:   blockHash,
			Offset: uint32(txLocs[0].TxStart),
			Len:    uint32(txLocs[0].TxLen),
		}
		allBlockRegions[i] = region
		_, err = tx.FetchBlockRegion(&region)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}

		// Ensure HasBlock returns expected error.
		testName = fmt.Sprintf("HasBlock #%d on closed tx", i)
		_, err = tx.HasBlock(blockHash)
		if !checkDbError(tc.t, testName, err, wantErrKind) {
			return false
		}
	}

	// -----------------
	// Bulk Block IO API
	// -----------------

	// Ensure FetchBlocks returns expected error.
	testName = "FetchBlocks on closed tx"
	_, err = tx.FetchBlocks(allBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure FetchBlockHeaders returns expected error.
	testName = "FetchBlockHeaders on closed tx"
	_, err = tx.FetchBlockHeaders(allBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure FetchBlockRegions returns expected error.
	testName = "FetchBlockRegions on closed tx"
	_, err = tx.FetchBlockRegions(allBlockRegions)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// Ensure HasBlocks returns expected error.
	testName = "HasBlocks on closed tx"
	_, err = tx.HasBlocks(allBlockHashes)
	if !checkDbError(tc.t, testName, err, wantErrKind) {
		return false
	}

	// ---------------
	// Commit/Rollback
	// ---------------

	// Ensure that attempting to rollback or commit a transaction that is
	// already closed returns the expected error.
	err = tx.Rollback()
	if !checkDbError(tc.t, "closed tx rollback", err, wantErrKind) {
		return false
	}
	err = tx.Commit()
	return checkDbError(tc.t, "closed tx commit", err, wantErrKind)
}

// testTxClosed ensures that both the metadata and block IO API functions behave
// as expected when attempted against both read-only and read-write
// transactions.
func testTxClosed(tc *testContext) bool {
	bucketName := []byte("closedtxbucket")
	keyName := []byte("closedtxkey")

	// Start a transaction, create a bucket and key used for testing, and
	// immediately perform a commit on it so it is closed.
	tx, err := tc.db.Begin(true)
	if err != nil {
		tc.t.Errorf("Begin(true): unexpected error: %v", err)
		return false
	}
	defer rollbackOnPanic(tc.t, tx)
	if _, err := tx.Metadata().CreateBucket(bucketName); err != nil {
		tc.t.Errorf("CreateBucket: unexpected error: %v", err)
		return false
	}
	if err := tx.Metadata().Put(keyName, []byte("test")); err != nil {
		tc.t.Errorf("Put: unexpected error: %v", err)
		return false
	}
	if err := tx.Commit(); err != nil {
		tc.t.Errorf("Commit: unexpected error: %v", err)
		return false
	}

	// Ensure invoking all of the functions on the closed read-write
	// transaction behave as expected.
	if !testClosedTxInterface(tc, tx) {
		return false
	}

	// Repeat the tests with a rolled-back read-only transaction.
	tx, err = tc.db.Begin(false)
	if err != nil {
		tc.t.Errorf("Begin(false): unexpected error: %v", err)
		return false
	}
	defer rollbackOnPanic(tc.t, tx)
	if err := tx.Rollback(); err != nil {
		tc.t.Errorf("Rollback: unexpected error: %v", err)
		return false
	}

	// Ensure invoking all of the functions on the closed read-only
	// transaction behave as expected.
	return testClosedTxInterface(tc, tx)
}

// testConcurrecy ensure the database properly supports concurrent readers and
// only a single writer.  It also ensures views act as snapshots at the time
// they are acquired.
func testConcurrecy(tc *testContext) bool {
	// sleepTime is how long each of the concurrent readers should sleep to
	// aid in detection of whether or not the data is actually being read
	// concurrently.  It starts with a sane lower bound.
	var sleepTime = time.Millisecond * 250

	// Determine about how long it takes for a single block read.  When it's
	// longer than the default minimum sleep time, adjust the sleep time to
	// help prevent durations that are too short which would cause erroneous
	// test failures on slower systems.
	startTime := time.Now()
	err := tc.db.View(func(tx database.Tx) error {
		_, err := tx.FetchBlock(tc.blocks[0].Hash())
		return err
	})
	if err != nil {
		tc.t.Errorf("Unexpected error in view: %v", err)
		return false
	}
	elapsed := time.Since(startTime)
	if sleepTime < elapsed {
		sleepTime = elapsed
	}
	tc.t.Logf("Time to load block 0: %v, using sleep time: %v", elapsed,
		sleepTime)

	// reader takes a block number to load and channel to return the result
	// of the operation on.  It is used below to launch multiple concurrent
	// readers.
	numReaders := len(tc.blocks)
	resultChan := make(chan bool, numReaders)
	reader := func(blockNum int) {
		err := tc.db.View(func(tx database.Tx) error {
			time.Sleep(sleepTime)
			_, err := tx.FetchBlock(tc.blocks[blockNum].Hash())
			return err
		})
		if err != nil {
			tc.t.Errorf("Unexpected error in concurrent view: %v",
				err)
			resultChan <- false
		}
		resultChan <- true
	}

	// Start up several concurrent readers for the same block and wait for
	// the results.
	startTime = time.Now()
	for i := 0; i < numReaders; i++ {
		go reader(0)
	}
	for i := 0; i < numReaders; i++ {
		if result := <-resultChan; !result {
			return false
		}
	}
	elapsed = time.Since(startTime)
	tc.t.Logf("%d concurrent reads of same block elapsed: %v", numReaders,
		elapsed)

	// Consider it a failure if it took longer than half the time it would
	// take with no concurrency.
	if elapsed > sleepTime*time.Duration(numReaders/2) {
		tc.t.Errorf("Concurrent views for same block did not appear to "+
			"run simultaneously: elapsed %v", elapsed)
		return false
	}

	// Start up several concurrent readers for different blocks and wait for
	// the results.
	startTime = time.Now()
	for i := 0; i < numReaders; i++ {
		go reader(i)
	}
	for i := 0; i < numReaders; i++ {
		if result := <-resultChan; !result {
			return false
		}
	}
	elapsed = time.Since(startTime)
	tc.t.Logf("%d concurrent reads of different blocks elapsed: %v",
		numReaders, elapsed)

	// Consider it a failure if it took longer than half the time it would
	// take with no concurrency.
	if elapsed > sleepTime*time.Duration(numReaders/2) {
		tc.t.Errorf("Concurrent views for different blocks did not "+
			"appear to run simultaneously: elapsed %v", elapsed)
		return false
	}

	// Start up a few readers and wait for them to acquire views.  Each
	// reader waits for a signal from the writer to be finished to ensure
	// that the data written by the writer is not seen by the view since it
	// was started before the data was set.
	concurrentKey := []byte("notthere")
	concurrentVal := []byte("someval")
	started := make(chan struct{})
	writeComplete := make(chan struct{})
	reader = func(blockNum int) {
		err := tc.db.View(func(tx database.Tx) error {
			started <- struct{}{}

			// Wait for the writer to complete.
			<-writeComplete

			// Since this reader was created before the write took
			// place, the data it added should not be visible.
			val := tx.Metadata().Get(concurrentKey)
			if val != nil {
				return fmt.Errorf("%s should not be visible",
					concurrentKey)
			}
			return nil
		})
		if err != nil {
			tc.t.Errorf("Unexpected error in concurrent view: %v",
				err)
			resultChan <- false
		}
		resultChan <- true
	}
	for i := 0; i < numReaders; i++ {
		go reader(0)
	}
	for i := 0; i < numReaders; i++ {
		<-started
	}

	// All readers are started and waiting for completion of the writer.
	// Set some data the readers are expecting to not find and signal the
	// readers the write is done by closing the writeComplete channel.
	err = tc.db.Update(func(tx database.Tx) error {
		return tx.Metadata().Put(concurrentKey, concurrentVal)
	})
	if err != nil {
		tc.t.Errorf("Unexpected error in update: %v", err)
		return false
	}
	close(writeComplete)

	// Wait for reader results.
	for i := 0; i < numReaders; i++ {
		if result := <-resultChan; !result {
			return false
		}
	}

	// Start a few writers and ensure the total time is at least the
	// writeSleepTime * numWriters.  This ensures only one write transaction
	// can be active at a time.
	writeSleepTime := time.Millisecond * 250
	writer := func() {
		err := tc.db.Update(func(tx database.Tx) error {
			time.Sleep(writeSleepTime)
			return nil
		})
		if err != nil {
			tc.t.Errorf("Unexpected error in concurrent view: %v",
				err)
			resultChan <- false
		}
		resultChan <- true
	}
	numWriters := 3
	startTime = time.Now()
	for i := 0; i < numWriters; i++ {
		go writer()
	}
	for i := 0; i < numWriters; i++ {
		if result := <-resultChan; !result {
			return false
		}
	}
	elapsed = time.Since(startTime)
	tc.t.Logf("%d concurrent writers elapsed using sleep time %v: %v",
		numWriters, writeSleepTime, elapsed)

	// The total time must have been at least the sum of all sleeps if the
	// writes blocked properly.
	if elapsed < writeSleepTime*time.Duration(numWriters) {
		tc.t.Errorf("Concurrent writes appeared to run simultaneously: "+
			"elapsed %v", elapsed)
		return false
	}

	return true
}

// testConcurrentClose ensures that closing the database with open transactions
// blocks until the transactions are finished.
//
// The database will be closed upon returning from this function.
func testConcurrentClose(tc *testContext) bool {
	// Start up a few readers and wait for them to acquire views.  Each
	// reader waits for a signal to complete to ensure the transactions stay
	// open until they are explicitly signalled to be closed.
	var activeReaders int32
	numReaders := 3
	started := make(chan struct{})
	finishReaders := make(chan struct{})
	resultChan := make(chan bool, numReaders+1)
	reader := func() {
		err := tc.db.View(func(tx database.Tx) error {
			atomic.AddInt32(&activeReaders, 1)
			started <- struct{}{}
			<-finishReaders
			atomic.AddInt32(&activeReaders, -1)
			return nil
		})
		if err != nil {
			tc.t.Errorf("Unexpected error in concurrent view: %v",
				err)
			resultChan <- false
		}
		resultChan <- true
	}
	for i := 0; i < numReaders; i++ {
		go reader()
	}
	for i := 0; i < numReaders; i++ {
		<-started
	}

	// Close the database in a separate goroutine.  This should block until
	// the transactions are finished.  Once the close has taken place, the
	// dbClosed channel is closed to signal the main goroutine below.
	dbClosed := make(chan struct{})
	go func() {
		started <- struct{}{}
		err := tc.db.Close()
		if err != nil {
			tc.t.Errorf("Unexpected error in concurrent view: %v",
				err)
			resultChan <- false
		}
		close(dbClosed)
		resultChan <- true
	}()
	<-started

	// Wait a short period and then signal the reader transactions to
	// finish.  When the db closed channel is received, ensure there are no
	// active readers open.
	time.AfterFunc(time.Millisecond*250, func() { close(finishReaders) })
	<-dbClosed
	if nr := atomic.LoadInt32(&activeReaders); nr != 0 {
		tc.t.Errorf("Close did not appear to block with active "+
			"readers: %d active", nr)
		return false
	}

	// Wait for all results.
	for i := 0; i < numReaders+1; i++ {
		if result := <-resultChan; !result {
			return false
		}
	}

	return true
}

// testInterface tests performs tests for the various interfaces of the database
// package which require state in the database for the given database type.
func testInterface(t *testing.T, db database.DB) {
	t.Helper()

	// Create a test context to pass around.
	context := testContext{t: t, db: db}

	// Load the test blocks and store in the test context for use throughout
	// the tests.
	blocks, err := loadBlocks(t, blockDataFile, blockDataNet)
	if err != nil {
		t.Errorf("loadBlocks: Unexpected error: %v", err)
		return
	}
	context.blocks = blocks

	// Test the transaction metadata interface including managed and manual
	// transactions as well as buckets.
	if !testMetadataTxInterface(&context) {
		return
	}

	// Test the transaction block IO interface using managed and manual
	// transactions.  This function leaves all of the stored blocks in the
	// database since they're used later.
	if !testBlockIOTxInterface(&context) {
		return
	}

	// Test all of the transaction interface functions against a closed
	// transaction work as expected.
	if !testTxClosed(&context) {
		return
	}

	// Test the database properly supports concurrency.
	if !testConcurrecy(&context) {
		return
	}

	// Test that closing the database with open transactions blocks until
	// the transactions are finished.
	//
	// The database will be closed upon returning from this function, so it
	// must be the last thing called.
	testConcurrentClose(&context)
}
