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

import (
	"crypto/sha256"
	"encoding/binary"
	"fmt"
	"path/filepath"
	"reflect"
	"sync"
	"testing"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/database"
	"github.com/btcsuite/btcd/database/ffldb"
	"github.com/btcsuite/btcd/wire"
)

func TestMapSlice(t *testing.T) {
	tests := []struct {
		keys []wire.OutPoint
	}{
		{
			keys: func() []wire.OutPoint {
				outPoints := make([]wire.OutPoint, 1000)
				for i := uint32(0); i < uint32(len(outPoints)); i++ {
					var buf [4]byte
					binary.BigEndian.PutUint32(buf[:], i)
					hash := sha256.Sum256(buf[:])

					op := wire.OutPoint{Hash: hash, Index: i}
					outPoints[i] = op
				}
				return outPoints
			}(),
		},
	}

	for _, test := range tests {
		m := make(map[wire.OutPoint]*UtxoEntry)

		maxSize := calculateRoughMapSize(1000, bucketSize)

		maxEntriesFirstMap := 500
		ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap)
		ms := mapSlice{
			maps:                []map[wire.OutPoint]*UtxoEntry{ms1},
			maxEntries:          []int{maxEntriesFirstMap},
			maxTotalMemoryUsage: uint64(maxSize),
		}

		for _, key := range test.keys {
			m[key] = nil
			ms.put(key, nil, 0)
		}

		// Put in the same elements twice to test that the map slice won't hold duplicates.
		for _, key := range test.keys {
			m[key] = nil
			ms.put(key, nil, 0)
		}

		if len(m) != ms.length() {
			t.Fatalf("expected len of %d, got %d", len(m), ms.length())
		}

		// Delete the first element in the first map.
		ms.delete(test.keys[0])
		delete(m, test.keys[0])

		// Try to insert the last element in the mapslice again.
		ms.put(test.keys[len(test.keys)-1], &UtxoEntry{}, 0)
		m[test.keys[len(test.keys)-1]] = &UtxoEntry{}

		// Check that the duplicate didn't make it in.
		if len(m) != ms.length() {
			t.Fatalf("expected len of %d, got %d", len(m), ms.length())
		}

		ms.put(test.keys[0], &UtxoEntry{}, 0)
		m[test.keys[0]] = &UtxoEntry{}

		if len(m) != ms.length() {
			t.Fatalf("expected len of %d, got %d", len(m), ms.length())
		}

		for _, key := range test.keys {
			expected, found := m[key]
			if !found {
				t.Fatalf("expected key %s to exist in the go map", key.String())
			}

			got, found := ms.get(key)
			if !found {
				t.Fatalf("expected key %s to exist in the map slice", key.String())
			}

			if !reflect.DeepEqual(got, expected) {
				t.Fatalf("expected value of %v, got %v", expected, got)
			}
		}
	}
}

// TestMapsliceConcurrency just tests that the mapslice won't result in a panic
// on concurrent access.
func TestMapsliceConcurrency(t *testing.T) {
	tests := []struct {
		keys []wire.OutPoint
	}{
		{
			keys: func() []wire.OutPoint {
				outPoints := make([]wire.OutPoint, 10000)
				for i := uint32(0); i < uint32(len(outPoints)); i++ {
					var buf [4]byte
					binary.BigEndian.PutUint32(buf[:], i)
					hash := sha256.Sum256(buf[:])

					op := wire.OutPoint{Hash: hash, Index: i}
					outPoints[i] = op
				}
				return outPoints
			}(),
		},
	}

	for _, test := range tests {
		maxSize := calculateRoughMapSize(1000, bucketSize)

		maxEntriesFirstMap := 500
		ms1 := make(map[wire.OutPoint]*UtxoEntry, maxEntriesFirstMap)
		ms := mapSlice{
			maps:                []map[wire.OutPoint]*UtxoEntry{ms1},
			maxEntries:          []int{maxEntriesFirstMap},
			maxTotalMemoryUsage: uint64(maxSize),
		}

		var wg sync.WaitGroup

		wg.Add(1)
		go func(m *mapSlice, keys []wire.OutPoint) {
			defer wg.Done()
			for i := 0; i < 5000; i++ {
				m.put(keys[i], nil, 0)
			}
		}(&ms, test.keys)

		wg.Add(1)
		go func(m *mapSlice, keys []wire.OutPoint) {
			defer wg.Done()
			for i := 5000; i < 10000; i++ {
				m.put(keys[i], nil, 0)
			}
		}(&ms, test.keys)

		wg.Add(1)
		go func(m *mapSlice) {
			defer wg.Done()
			for i := 0; i < 10000; i++ {
				m.size()
			}
		}(&ms)

		wg.Add(1)
		go func(m *mapSlice) {
			defer wg.Done()
			for i := 0; i < 10000; i++ {
				m.length()
			}
		}(&ms)

		wg.Add(1)
		go func(m *mapSlice, keys []wire.OutPoint) {
			defer wg.Done()
			for i := 0; i < 10000; i++ {
				m.get(keys[i])
			}
		}(&ms, test.keys)

		wg.Add(1)
		go func(m *mapSlice, keys []wire.OutPoint) {
			defer wg.Done()
			for i := 0; i < 5000; i++ {
				m.delete(keys[i])
			}
		}(&ms, test.keys)

		wg.Wait()
	}
}

// getValidP2PKHScript returns a valid P2PKH script.  Useful as unspendables cannot be
// added to the cache.
func getValidP2PKHScript() []byte {
	validP2PKHScript := []byte{
		// OP_DUP
		0x76,
		// OP_HASH160
		0xa9,
		// OP_DATA_20
		0x14,
		// <20-byte pubkey hash>
		0xf0, 0x7a, 0xb8, 0xce, 0x72, 0xda, 0x4e, 0x76,
		0x0b, 0x74, 0x7d, 0x48, 0xd6, 0x65, 0xec, 0x96,
		0xad, 0xf0, 0x24, 0xf5,
		// OP_EQUALVERIFY
		0x88,
		// OP_CHECKSIG
		0xac,
	}
	return validP2PKHScript
}

// outpointFromInt generates an outpoint from an int by hashing the int and making
// the given int the index.
func outpointFromInt(i int) wire.OutPoint {
	// Boilerplate to create an outpoint.
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[:], uint32(i))
	hash := sha256.Sum256(buf[:])
	return wire.OutPoint{Hash: hash, Index: uint32(i)}
}

func TestUtxoCacheEntrySize(t *testing.T) {
	type block struct {
		txOuts []*wire.TxOut
		outOps []wire.OutPoint
		txIns  []*wire.TxIn
	}
	tests := []struct {
		name         string
		blocks       []block
		expectedSize uint64
	}{
		{
			name: "one entry",
			blocks: func() []block {
				return []block{
					{
						txOuts: []*wire.TxOut{
							{Value: 10000, PkScript: getValidP2PKHScript()},
						},
						outOps: []wire.OutPoint{
							outpointFromInt(0),
						},
					},
				}
			}(),
			expectedSize: pubKeyHashLen + baseEntrySize,
		},
		{
			name: "10 entries, 4 spend",
			blocks: func() []block {
				blocks := make([]block, 0, 10)
				for i := 0; i < 10; i++ {
					op := outpointFromInt(i)

					block := block{
						txOuts: []*wire.TxOut{
							{Value: 10000, PkScript: getValidP2PKHScript()},
						},
						outOps: []wire.OutPoint{
							op,
						},
					}

					// Spend all outs in blocks less than 4.
					if i < 4 {
						block.txIns = []*wire.TxIn{
							{PreviousOutPoint: op},
						}
					}

					blocks = append(blocks, block)
				}
				return blocks
			}(),
			// Multiplied by 6 since we'll have 6 entries left.
			expectedSize: (pubKeyHashLen + baseEntrySize) * 6,
		},
		{
			name: "spend everything",
			blocks: func() []block {
				blocks := make([]block, 0, 500)
				for i := 0; i < 500; i++ {
					op := outpointFromInt(i)

					block := block{
						txOuts: []*wire.TxOut{
							{Value: 1000, PkScript: getValidP2PKHScript()},
						},
						outOps: []wire.OutPoint{
							op,
						},
					}

					// Spend all outs in blocks less than 4.
					block.txIns = []*wire.TxIn{
						{PreviousOutPoint: op},
					}

					blocks = append(blocks, block)
				}
				return blocks
			}(),
			expectedSize: 0,
		},
	}

	for _, test := range tests {
		// Size is just something big enough so that the mapslice doesn't
		// run out of memory.
		s := newUtxoCache(nil, 1*1024*1024)

		for height, block := range test.blocks {
			for i, out := range block.txOuts {
				s.addTxOut(block.outOps[i], out, true, int32(height))
			}

			for _, in := range block.txIns {
				s.addTxIn(in, nil)
			}
		}

		if s.totalEntryMemory != test.expectedSize {
			t.Errorf("Failed test %s. Expected size of %d, got %d",
				test.name, test.expectedSize, s.totalEntryMemory)
		}
	}
}

// assertConsistencyState asserts the utxo consistency states of the blockchain.
func assertConsistencyState(chain *BlockChain, hash *chainhash.Hash) error {
	var bytes []byte
	err := chain.db.View(func(dbTx database.Tx) (err error) {
		bytes = dbFetchUtxoStateConsistency(dbTx)
		return
	})
	if err != nil {
		return fmt.Errorf("Error fetching utxo state consistency: %v", err)
	}
	actualHash, err := chainhash.NewHash(bytes)
	if err != nil {
		return err
	}
	if !actualHash.IsEqual(hash) {
		return fmt.Errorf("Unexpected consistency hash: %v instead of %v",
			actualHash, hash)
	}

	return nil
}

// assertNbEntriesOnDisk asserts that the total number of utxo entries on the
// disk is equal to the given expected number.
func assertNbEntriesOnDisk(chain *BlockChain, expectedNumber int) error {
	var nb int
	err := chain.db.View(func(dbTx database.Tx) error {
		cursor := dbTx.Metadata().Bucket(utxoSetBucketName).Cursor()
		nb = 0
		for b := cursor.First(); b; b = cursor.Next() {
			nb++
			_, err := deserializeUtxoEntry(cursor.Value())
			if err != nil {
				return fmt.Errorf("Failed to deserialize entry: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("Error fetching utxo entries: %v", err)
	}
	if nb != expectedNumber {
		return fmt.Errorf("Expected %d elements in the UTXO set, but found %d",
			expectedNumber, nb)
	}

	return nil
}

// utxoCacheTestChain creates a test BlockChain to be used for utxo cache tests.
// It uses the regression test parameters, a coin matutiry of 1 block and sets
// the cache size limit to 10 MiB.
func utxoCacheTestChain(testName string) (*BlockChain, *chaincfg.Params, func()) {
	params := chaincfg.RegressionNetParams
	chain, tearDown, err := chainSetup(testName, &params)
	if err != nil {
		panic(fmt.Sprintf("error loading blockchain with database: %v", err))
	}

	chain.TstSetCoinbaseMaturity(1)
	chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024
	chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage

	return chain, &params, tearDown
}

func TestUtxoCacheFlush(t *testing.T) {
	chain, params, tearDown := utxoCacheTestChain("TestUtxoCacheFlush")
	defer tearDown()
	cache := chain.utxoCache
	tip := btcutil.NewBlock(params.GenesisBlock)

	// The chainSetup init triggers the consistency status write.
	err := assertConsistencyState(chain, params.GenesisHash)
	if err != nil {
		t.Fatal(err)
	}

	err = assertNbEntriesOnDisk(chain, 0)
	if err != nil {
		t.Fatal(err)
	}

	// LastFlushHash starts with genesis.
	if cache.lastFlushHash != *params.GenesisHash {
		t.Fatalf("lastFlushHash before first flush expected to be "+
			"genesis block hash, instead was %v", cache.lastFlushHash)
	}

	// First, add 10 utxos without flushing.
	outPoints := make([]wire.OutPoint, 10)
	for i := range outPoints {
		op := outpointFromInt(i)
		outPoints[i] = op

		// Add the txout.
		txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()}
		cache.addTxOut(op, &txOut, true, int32(i))
	}

	if cache.cachedEntries.length() != len(outPoints) {
		t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length())
	}

	// All entries should be fresh and modified.
	for _, m := range cache.cachedEntries.maps {
		for outpoint, entry := range m {
			if entry == nil {
				t.Fatalf("Unexpected nil entry found for %v", outpoint)
			}
			if !entry.isModified() {
				t.Fatal("Entry should be marked modified")
			}
			if !entry.isFresh() {
				t.Fatal("Entry should be marked fresh")
			}
		}
	}

	// Spend the last outpoint and pop it off from the outpoints slice.
	var spendOp wire.OutPoint
	spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1]
	cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil)

	if cache.cachedEntries.length() != len(outPoints) {
		t.Fatalf("Expected %d entries, has %d instead",
			len(outPoints), cache.cachedEntries.length())
	}

	// Not flushed yet.
	err = assertConsistencyState(chain, params.GenesisHash)
	if err != nil {
		t.Fatal(err)
	}

	err = assertNbEntriesOnDisk(chain, 0)
	if err != nil {
		t.Fatal(err)
	}

	// Flush.
	err = chain.db.Update(func(dbTx database.Tx) error {
		return cache.flush(dbTx, FlushRequired, chain.stateSnapshot)
	})
	if err != nil {
		t.Fatalf("unexpected error while flushing cache: %v", err)
	}
	if cache.cachedEntries.length() != 0 {
		t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length())
	}

	err = assertConsistencyState(chain, tip.Hash())
	if err != nil {
		t.Fatal(err)
	}
	err = assertNbEntriesOnDisk(chain, len(outPoints))
	if err != nil {
		t.Fatal(err)
	}

	// Fetch the flushed utxos.
	entries, err := cache.fetchEntries(outPoints)
	if err != nil {
		t.Fatal(err)
	}

	// Check that the returned entries are not marked fresh and modified.
	for _, entry := range entries {
		if entry.isFresh() {
			t.Fatal("Entry should not be marked fresh")
		}
		if entry.isModified() {
			t.Fatal("Entry should not be marked modified")
		}
	}

	// Check that the fetched entries in the cache are not marked fresh and modified.
	for _, m := range cache.cachedEntries.maps {
		for outpoint, elem := range m {
			if elem == nil {
				t.Fatalf("Unexpected nil entry found for %v", outpoint)
			}
			if elem.isFresh() {
				t.Fatal("Entry should not be marked fresh")
			}
			if elem.isModified() {
				t.Fatal("Entry should not be marked modified")
			}
		}
	}

	// Spend 5 utxos.
	prevLen := len(outPoints)
	for i := 0; i < 5; i++ {
		spendOp, outPoints = outPoints[len(outPoints)-1], outPoints[:len(outPoints)-1]
		cache.addTxIn(&wire.TxIn{PreviousOutPoint: spendOp}, nil)
	}

	// Should still have the entries in cache so they can be flushed to disk.
	if cache.cachedEntries.length() != prevLen {
		t.Fatalf("Expected 10 entries, has %d instead", cache.cachedEntries.length())
	}

	// Flush.
	err = chain.db.Update(func(dbTx database.Tx) error {
		return cache.flush(dbTx, FlushRequired, chain.stateSnapshot)
	})
	if err != nil {
		t.Fatalf("unexpected error while flushing cache: %v", err)
	}
	if cache.cachedEntries.length() != 0 {
		t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length())
	}

	err = assertConsistencyState(chain, tip.Hash())
	if err != nil {
		t.Fatal(err)
	}
	err = assertNbEntriesOnDisk(chain, len(outPoints))
	if err != nil {
		t.Fatal(err)
	}

	// Add 5 utxos without flushing and test for periodic flushes.
	outPoints1 := make([]wire.OutPoint, 5)
	for i := range outPoints1 {
		// i + prevLen here to avoid collision since we're just hashing
		// the int.
		op := outpointFromInt(i + prevLen)
		outPoints1[i] = op

		// Add the txout.
		txOut := wire.TxOut{Value: 10000, PkScript: getValidP2PKHScript()}
		cache.addTxOut(op, &txOut, true, int32(i+prevLen))
	}
	if cache.cachedEntries.length() != len(outPoints1) {
		t.Fatalf("Expected %d entries, has %d instead",
			len(outPoints1), cache.cachedEntries.length())
	}

	// Attempt to flush with flush periodic.  Shouldn't flush.
	err = chain.db.Update(func(dbTx database.Tx) error {
		return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot)
	})
	if err != nil {
		t.Fatalf("unexpected error while flushing cache: %v", err)
	}
	if cache.cachedEntries.length() == 0 {
		t.Fatalf("Expected %d entries, has %d instead",
			len(outPoints1), cache.cachedEntries.length())
	}

	// Arbitrarily set the last flush time to 6 minutes ago.
	cache.lastFlushTime = time.Now().Add(-time.Minute * 6)

	// Attempt to flush with flush periodic.  Should flush now.
	err = chain.db.Update(func(dbTx database.Tx) error {
		return cache.flush(dbTx, FlushPeriodic, chain.stateSnapshot)
	})
	if err != nil {
		t.Fatalf("unexpected error while flushing cache: %v", err)
	}
	if cache.cachedEntries.length() != 0 {
		t.Fatalf("Expected 0 entries, has %d instead", cache.cachedEntries.length())
	}

	err = assertConsistencyState(chain, tip.Hash())
	if err != nil {
		t.Fatal(err)
	}
	err = assertNbEntriesOnDisk(chain, len(outPoints)+len(outPoints1))
	if err != nil {
		t.Fatal(err)
	}
}

func TestFlushNeededAfterPrune(t *testing.T) {
	// Construct a synthetic block chain with a block index consisting of
	// the following structure.
	// 	genesis -> 1 -> 2 -> ... -> 15 -> 16  -> 17  -> 18
	tip := tstTip
	chain := newFakeChain(&chaincfg.MainNetParams)
	chain.utxoCache = newUtxoCache(nil, 0)
	branchNodes := chainedNodes(chain.bestChain.Genesis(), 18)
	for _, node := range branchNodes {
		chain.index.SetStatusFlags(node, statusValid)
		chain.index.AddNode(node)
	}
	chain.bestChain.SetTip(tip(branchNodes))

	tests := []struct {
		name          string
		lastFlushHash chainhash.Hash
		delHashes     []chainhash.Hash
		expected      bool
	}{
		{
			name: "deleted block up to height 9, last flush hash at block 10",
			delHashes: func() []chainhash.Hash {
				delBlockHashes := make([]chainhash.Hash, 0, 9)
				for i := range branchNodes {
					if branchNodes[i].height < 10 {
						delBlockHashes = append(delBlockHashes, branchNodes[i].hash)
					}
				}

				return delBlockHashes
			}(),
			lastFlushHash: func() chainhash.Hash {
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[9].height != 10 {
					panic("was looking for height 10")
				}
				return branchNodes[9].hash
			}(),
			expected: false,
		},
		{
			name: "deleted blocks up to height 10, last flush hash at block 10",
			delHashes: func() []chainhash.Hash {
				delBlockHashes := make([]chainhash.Hash, 0, 10)
				for i := range branchNodes {
					if branchNodes[i].height < 11 {
						delBlockHashes = append(delBlockHashes, branchNodes[i].hash)
					}
				}
				return delBlockHashes
			}(),
			lastFlushHash: func() chainhash.Hash {
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[9].height != 10 {
					panic("was looking for height 10")
				}
				return branchNodes[9].hash
			}(),
			expected: true,
		},
		{
			name: "deleted block height 17, last flush hash at block 5",
			delHashes: func() []chainhash.Hash {
				delBlockHashes := make([]chainhash.Hash, 1)
				delBlockHashes[0] = branchNodes[16].hash
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[16].height != 17 {
					panic("was looking for height 17")
				}
				return delBlockHashes
			}(),
			lastFlushHash: func() chainhash.Hash {
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[4].height != 5 {
					panic("was looking for height 5")
				}
				return branchNodes[4].hash
			}(),
			expected: true,
		},
		{
			name: "deleted block height 3, last flush hash at block 4",
			delHashes: func() []chainhash.Hash {
				delBlockHashes := make([]chainhash.Hash, 1)
				delBlockHashes[0] = branchNodes[2].hash
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[2].height != 3 {
					panic("was looking for height 3")
				}
				return delBlockHashes
			}(),
			lastFlushHash: func() chainhash.Hash {
				// Just some sanity checking to make sure the height is 10.
				if branchNodes[3].height != 4 {
					panic("was looking for height 4")
				}
				return branchNodes[3].hash
			}(),
			expected: false,
		},
	}

	for _, test := range tests {
		chain.utxoCache.lastFlushHash = test.lastFlushHash
		got, err := chain.flushNeededAfterPrune(test.delHashes)
		if err != nil {
			t.Fatal(err)
		}

		if got != test.expected {
			t.Fatalf("for test %s, expected need flush to return %v but got %v",
				test.name, test.expected, got)
		}
	}
}

func TestFlushOnPrune(t *testing.T) {
	chain, tearDown, err := chainSetup("TestFlushOnPrune", &chaincfg.MainNetParams)
	if err != nil {
		panic(fmt.Sprintf("error loading blockchain with database: %v", err))
	}
	defer tearDown()

	chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024
	chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage

	// Set the maxBlockFileSize and the prune target small so that we can trigger a
	// prune to happen.
	maxBlockFileSize := uint32(8192)
	chain.pruneTarget = uint64(maxBlockFileSize) * 2

	// Read blocks from the file.
	blocks, err := loadBlocks("blk_0_to_14131.dat")
	if err != nil {
		t.Fatalf("failed to read block from file. %v", err)
	}

	syncBlocks := func() {
		// Modify block 1 to be a different hash.  This is to artificially
		// create a stale branch in the chain.
		staleMsgBlock := blocks[1].MsgBlock().Copy()
		staleMsgBlock.Header.Nonce = 0
		staleBlock := btcutil.NewBlock(staleMsgBlock)

		// Add the stale block here to create a chain view like so. The
		// block will be the main chain at first but become stale as we
		// keep adding blocks.  BFNoPoWCheck is given as the pow check will
		// fail.
		//
		// (genesis block) -> 1 -> 2 -> 3 -> ...
		//                \-> 1a
		_, _, err = chain.ProcessBlock(staleBlock, BFNoPoWCheck)
		if err != nil {
			t.Fatal(err)
		}

		for i, block := range blocks {
			if i == 0 {
				// Skip the genesis block.
				continue
			}
			_, _, err = chain.ProcessBlock(block, BFNone)
			if err != nil {
				t.Fatalf("Failed to process block %v(%v). %v",
					block.Hash().String(), block.Height(), err)
			}
		}
	}

	// Sync the chain.
	ffldb.TstRunWithMaxBlockFileSize(chain.db, maxBlockFileSize, syncBlocks)

	// Function that errors out if the block that should exist doesn't exist.
	shouldExist := func(dbTx database.Tx, blockHash *chainhash.Hash) error {
		bytes, err := dbTx.FetchBlock(blockHash)
		if err != nil {
			return err
		}
		block, err := btcutil.NewBlockFromBytes(bytes)
		if err != nil {
			return fmt.Errorf("didn't find block %v. %v", blockHash, err)
		}

		if !block.Hash().IsEqual(blockHash) {
			return fmt.Errorf("expected to find block %v but got %v",
				blockHash, block.Hash())
		}

		return nil
	}

	// Function that errors out if the block that shouldn't exist exists.
	shouldNotExist := func(dbTx database.Tx, blockHash *chainhash.Hash) error {
		bytes, err := dbTx.FetchBlock(chaincfg.MainNetParams.GenesisHash)
		if err == nil {
			return fmt.Errorf("expected block %s to be pruned", blockHash.String())
		}
		if len(bytes) != 0 {
			return fmt.Errorf("expected block %s to be pruned but got %v",
				blockHash, bytes)
		}

		return nil
	}

	// The below code checks that the correct blocks were pruned.
	err = chain.db.View(func(dbTx database.Tx) error {
		exist := false
		for _, block := range blocks {
			// Blocks up to the last flush hash should not exist.
			// The utxocache is big enough so that it shouldn't flush
			// on it being full.  It should only flush on prunes.
			if block.Hash().IsEqual(&chain.utxoCache.lastFlushHash) {
				exist = true
			}

			if exist {
				err = shouldExist(dbTx, block.Hash())
				if err != nil {
					return err
				}
			} else {
				err = shouldNotExist(dbTx, block.Hash())
				if err != nil {
					return err
				}
			}
		}

		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestInitConsistentState(t *testing.T) {
	//  Boilerplate for creating a chain.
	dbName := "TestFlushOnPrune"
	chain, tearDown, err := chainSetup(dbName, &chaincfg.MainNetParams)
	if err != nil {
		panic(fmt.Sprintf("error loading blockchain with database: %v", err))
	}
	defer tearDown()
	chain.utxoCache.maxTotalMemoryUsage = 10 * 1024 * 1024
	chain.utxoCache.cachedEntries.maxTotalMemoryUsage = chain.utxoCache.maxTotalMemoryUsage

	// Read blocks from the file.
	blocks, err := loadBlocks("blk_0_to_14131.dat")
	if err != nil {
		t.Fatalf("failed to read block from file. %v", err)
	}

	// Sync up to height 13,000.  Flush the utxocache at height 11_000.
	cacheFlushHeight := 9000
	initialSyncHeight := 12_000
	for i, block := range blocks {
		if i == 0 {
			// Skip the genesis block.
			continue
		}

		isMainChain, _, err := chain.ProcessBlock(block, BFNone)
		if err != nil {
			t.Fatal(err)
		}

		if !isMainChain {
			t.Fatalf("expected block %s to be on the main chain", block.Hash())
		}

		if i == cacheFlushHeight {
			err = chain.FlushUtxoCache(FlushRequired)
			if err != nil {
				t.Fatal(err)
			}
		}
		if i == initialSyncHeight {
			break
		}
	}

	// Sanity check.
	if chain.BestSnapshot().Height != int32(initialSyncHeight) {
		t.Fatalf("expected the chain to sync up to height %d", initialSyncHeight)
	}

	// Close the database without flushing the utxocache.  This leaves the
	// chaintip at height 13,000 but the utxocache consistent state at 11,000.
	err = chain.db.Close()
	if err != nil {
		t.Fatal(err)
	}
	chain.db = nil

	// Re-open the database and pass the re-opened db to internal structs.
	dbPath := filepath.Join(testDbRoot, dbName)
	ndb, err := database.Open(testDbType, dbPath, blockDataNet)
	if err != nil {
		t.Fatal(err)
	}
	chain.db = ndb
	chain.utxoCache.db = ndb
	chain.index.db = ndb

	// Sanity check to see that the utxo cache was flushed before the
	// current chain tip.
	var statusBytes []byte
	ndb.View(func(dbTx database.Tx) error {
		statusBytes = dbFetchUtxoStateConsistency(dbTx)
		return nil
	})
	statusHash, err := chainhash.NewHash(statusBytes)
	if err != nil {
		t.Fatal(err)
	}
	if !statusHash.IsEqual(blocks[cacheFlushHeight].Hash()) {
		t.Fatalf("expected the utxocache to be flushed at "+
			"block hash %s but got %s",
			blocks[cacheFlushHeight].Hash(), statusHash)
	}

	// Call InitConsistentState.  This will make the utxocache catch back
	// up to the tip.
	err = chain.InitConsistentState(chain.bestChain.tip(), nil)
	if err != nil {
		t.Fatal(err)
	}

	// Sync the reset of the blocks.
	for i, block := range blocks {
		if i <= initialSyncHeight {
			continue
		}
		isMainChain, _, err := chain.ProcessBlock(block, BFNone)
		if err != nil {
			t.Fatal(err)
		}

		if !isMainChain {
			t.Fatalf("expected block %s to be on the main chain", block.Hash())
		}
	}

	if chain.BestSnapshot().Height != blocks[len(blocks)-1].Height() {
		t.Fatalf("expected the chain to sync up to height %d",
			blocks[len(blocks)-1].Height())
	}
}
