/*
	Copyright IBM Corp. All Rights Reserved.

	SPDX-License-Identifier: Apache-2.0
*/

package blkstorage

import (
	"os"
	"testing"

	"github.com/hyperledger/fabric-lib-go/common/metrics/disabled"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric/common/ledger/testutil"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/stretchr/testify/require"
)

func TestRollback(t *testing.T) {
	path := t.TempDir()
	blocks := testutil.ConstructTestBlocks(t, 50) // 50 blocks persisted in ~5 block files
	blocksPerFile := 50 / 5
	env := newTestEnv(t, NewConf(path, 0))
	defer env.Cleanup()
	blkfileMgrWrapper := newTestBlockfileWrapper(env, "testLedger")
	blkfileMgr := blkfileMgrWrapper.blockfileMgr
	// 1. Store blocks
	for i, b := range blocks {
		require.NoError(t, blkfileMgr.addBlock(b))
		if i != 0 && i%blocksPerFile == 0 {
			// block ranges in files [(0, 10):file0, (11,20):file1, (21,30):file2, (31, 40):file3, (41,49):file4]
			blkfileMgr.moveToNextFile()
		}
	}

	// 2. Check the BlockchainInfo
	expectedBlockchainInfo := &common.BlockchainInfo{
		Height:            50,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[49].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[48].Header),
	}
	actualBlockchainInfo := blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 3. Check the BlockfileInfo
	expectedblkfilesInfoLastBlockNumber := uint64(49)
	expectedBlkfilesInfoIsNoFiles := false
	actualBlkfilesInfo, err := blkfileMgrWrapper.blockfileMgr.loadBlkfilesInfo()
	require.NoError(t, err)
	require.Equal(t, expectedblkfilesInfoLastBlockNumber, actualBlkfilesInfo.lastPersistedBlock)
	require.Equal(t, expectedBlkfilesInfoIsNoFiles, actualBlkfilesInfo.noBlockFiles)
	require.Equal(t, actualBlkfilesInfo.latestFileNumber, 4)

	// 4. Check whether all blocks are stored correctly
	blkfileMgrWrapper.testGetBlockByNumber(blocks)
	blkfileMgrWrapper.testGetBlockByHash(blocks)
	blkfileMgrWrapper.testGetBlockByTxID(blocks)

	// 5. Close the blkfileMgrWrapper
	env.provider.Close()
	blkfileMgrWrapper.close()
	lastBlockNumberInLastFile := uint64(49)
	middleBlockNumberInLastFile := uint64(45)
	firstBlockNumberInLastFile := uint64(41)

	// 7. Rollback to one before the lastBlockNumberInLastFile
	indexConfig := &IndexConfig{AttrsToIndex: attrsToIndex}
	err = Rollback(path, "testLedger", lastBlockNumberInLastFile-uint64(1), indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, lastBlockNumberInLastFile-uint64(1), 4, indexConfig)

	// 8. Rollback to middleBlockNumberInLastFile
	err = Rollback(path, "testLedger", middleBlockNumberInLastFile, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, middleBlockNumberInLastFile, 4, indexConfig)

	// 9. Rollback to firstBlockNumberInLastFile
	err = Rollback(path, "testLedger", firstBlockNumberInLastFile, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, firstBlockNumberInLastFile, 4, indexConfig)

	// 10. Rollback to one before the firstBlockNumberInLastFile
	err = Rollback(path, "testLedger", firstBlockNumberInLastFile-1, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, firstBlockNumberInLastFile-1, 3, indexConfig)

	// 11. In the middle block file (among a range of block files), find the middle block number
	middleBlockNumberInMiddleFile := uint64(25)

	// 12. Rollback to middleBlockNumberInMiddleFile
	err = Rollback(path, "testLedger", middleBlockNumberInMiddleFile, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, middleBlockNumberInMiddleFile, 2, indexConfig)

	// 13. Rollback to block 5
	err = Rollback(path, "testLedger", 5, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, 5, 0, indexConfig)

	// 14. Rollback to block 1
	err = Rollback(path, "testLedger", 1, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, 1, 0, indexConfig)
}

// TestRollbackWithOnlyBlockIndexAttributes mimics the scenario when ledger is used for orderer
// i.e., only block is index and transancations are not indexed
func TestRollbackWithOnlyBlockIndexAttributes(t *testing.T) {
	path := t.TempDir()
	blocks := testutil.ConstructTestBlocks(t, 50) // 50 blocks persisted in ~5 block files
	blocksPerFile := 50 / 5
	onlyBlockNumIndex := []IndexableAttr{
		IndexableAttrBlockNum,
	}
	env := newTestEnvSelectiveIndexing(t, NewConf(path, 0), onlyBlockNumIndex, &disabled.Provider{})
	defer env.Cleanup()
	blkfileMgrWrapper := newTestBlockfileWrapper(env, "testLedger")
	blkfileMgr := blkfileMgrWrapper.blockfileMgr

	// 1. Store blocks
	for i, b := range blocks {
		require.NoError(t, blkfileMgr.addBlock(b))
		if i != 0 && i%blocksPerFile == 0 {
			// block ranges in files [(0, 10):file0, (11,20):file1, (21,30):file2, (31, 40):file3, (41,49):file4]
			blkfileMgr.moveToNextFile()
		}
	}

	// 2. Check the BlockchainInfo
	expectedBlockchainInfo := &common.BlockchainInfo{
		Height:            50,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[49].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[48].Header),
	}
	actualBlockchainInfo := blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 3. Check the BlockfilesInfo
	expectedBlkfilesInfoLastBlockNumber := uint64(49)
	expectedBlkfilesInfoIsNoBlkFiles := false
	actualBlkfilesInfo, err := blkfileMgrWrapper.blockfileMgr.loadBlkfilesInfo()
	require.NoError(t, err)
	require.Equal(t, expectedBlkfilesInfoLastBlockNumber, actualBlkfilesInfo.lastPersistedBlock)
	require.Equal(t, expectedBlkfilesInfoIsNoBlkFiles, actualBlkfilesInfo.noBlockFiles)
	require.Equal(t, actualBlkfilesInfo.latestFileNumber, 4)

	// 4. Close the blkfileMgrWrapper
	env.provider.Close()
	blkfileMgrWrapper.close()

	// 5. Rollback to block 2
	onlyBlockNumIndexCfg := &IndexConfig{
		AttrsToIndex: onlyBlockNumIndex,
	}
	err = Rollback(path, "testLedger", 2, onlyBlockNumIndexCfg)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, 2, 0, onlyBlockNumIndexCfg)
}

func TestRollbackWithNoIndexDir(t *testing.T) {
	path := t.TempDir()
	blocks := testutil.ConstructTestBlocks(t, 50)
	blocksPerFile := 50 / 5
	conf := NewConf(path, 0)
	env := newTestEnv(t, conf)
	defer env.Cleanup()
	blkfileMgrWrapper := newTestBlockfileWrapper(env, "testLedger")
	blkfileMgr := blkfileMgrWrapper.blockfileMgr

	// 1. Store blocks
	for i, b := range blocks {
		require.NoError(t, blkfileMgr.addBlock(b))
		if i != 0 && i%blocksPerFile == 0 {
			// block ranges in files [(0, 10):file0, (11,20):file1, (21,30):file2, (31, 40):file3, (41,49):file4]
			blkfileMgr.moveToNextFile()
		}
	}

	// 2. Check the BlockchainInfo
	expectedBlockchainInfo := &common.BlockchainInfo{
		Height:            50,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[49].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[48].Header),
	}
	actualBlockchainInfo := blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 3. Check the BlockfilesInfo
	expectedBlkfilesInfoLastBlockNumber := uint64(49)
	expectedBlkfilesInfoIsChainEmpty := false
	actualBlkfilesInfo, err := blkfileMgrWrapper.blockfileMgr.loadBlkfilesInfo()
	require.NoError(t, err)
	require.Equal(t, expectedBlkfilesInfoLastBlockNumber, actualBlkfilesInfo.lastPersistedBlock)
	require.Equal(t, expectedBlkfilesInfoIsChainEmpty, actualBlkfilesInfo.noBlockFiles)
	require.Equal(t, actualBlkfilesInfo.latestFileNumber, 4)

	// 4. Close the blkfileMgrWrapper
	env.provider.Close()
	blkfileMgrWrapper.close()

	// 5. Remove the index directory
	indexDir := conf.getIndexDir()
	err = os.RemoveAll(indexDir)
	require.NoError(t, err)

	// 6. Rollback to block 2
	indexConfig := &IndexConfig{AttrsToIndex: attrsToIndex}
	err = Rollback(path, "testLedger", 2, indexConfig)
	require.NoError(t, err)
	assertBlockStoreRollback(t, path, "testLedger", blocks, 2, 0, indexConfig)
}

func TestValidateRollbackParams(t *testing.T) {
	path := t.TempDir()
	env := newTestEnv(t, NewConf(path, 1024*24))
	defer env.Cleanup()

	blkfileMgrWrapper := newTestBlockfileWrapper(env, "testLedger")

	// 1. Create 10 blocks
	blocks := testutil.ConstructTestBlocks(t, 10)
	blkfileMgrWrapper.addBlocks(blocks)

	// 2. Valid inputs
	err := ValidateRollbackParams(path, "testLedger", 5)
	require.NoError(t, err)

	// 3. ledgerID does not exist
	err = ValidateRollbackParams(path, "noLedger", 5)
	require.Equal(t, "ledgerID [noLedger] does not exist", err.Error())

	err = ValidateRollbackParams(path, "testLedger", 15)
	require.Equal(t, "target block number [15] should be less than the biggest block number [9]", err.Error())
}

func TestDuplicateTxIDDuringRollback(t *testing.T) {
	path := t.TempDir()
	blocks := testutil.ConstructTestBlocks(t, 4)
	maxFileSize := 1024 * 1024 * 4
	env := newTestEnv(t, NewConf(path, maxFileSize))
	defer env.Cleanup()
	blkfileMgrWrapper := newTestBlockfileWrapper(env, "testLedger")
	blocks[3].Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER][0] = byte(peer.TxValidationCode_DUPLICATE_TXID)
	testutil.SetTxID(t, blocks[3], 0, "tx0")
	testutil.SetTxID(t, blocks[2], 0, "tx0")

	// 1. Store blocks
	blkfileMgrWrapper.addBlocks(blocks)

	// 2. Check the BlockchainInfo
	expectedBlockchainInfo := &common.BlockchainInfo{
		Height:            4,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[3].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[2].Header),
	}
	actualBlockchainInfo := blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 3. Retrieve tx
	blkfileMgrWrapper.testGetTransactionByTxID("tx0", blocks[2].Data.Data[0], nil)

	// 4. Close the blkfileMgrWrapper
	env.provider.Close()
	blkfileMgrWrapper.close()

	// 5. Rollback to block 2
	indexConfig := &IndexConfig{AttrsToIndex: attrsToIndex}
	err := Rollback(path, "testLedger", 2, indexConfig)
	require.NoError(t, err)

	env = newTestEnv(t, NewConf(path, maxFileSize))
	blkfileMgrWrapper = newTestBlockfileWrapper(env, "testLedger")

	// 6. Check the BlockchainInfo
	expectedBlockchainInfo = &common.BlockchainInfo{
		Height:            3,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[2].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[1].Header),
	}
	actualBlockchainInfo = blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 8. Retrieve tx (should not have been deleted)
	blkfileMgrWrapper.testGetTransactionByTxID("tx0", blocks[2].Data.Data[0], nil)
}

func assertBlockStoreRollback(t *testing.T, path, ledgerID string, blocks []*common.Block,
	rollbackedToBlkNum uint64, lastFileSuffixNum int, indexConfig *IndexConfig) {
	env := newTestEnvSelectiveIndexing(t, NewConf(path, 0), indexConfig.AttrsToIndex, &disabled.Provider{})
	blkfileMgrWrapper := newTestBlockfileWrapper(env, ledgerID)

	// 1. Check the BlockchainInfo after the rollback
	expectedBlockchainInfo := &common.BlockchainInfo{
		Height:            rollbackedToBlkNum + 1,
		CurrentBlockHash:  protoutil.BlockHeaderHash(blocks[rollbackedToBlkNum].Header),
		PreviousBlockHash: protoutil.BlockHeaderHash(blocks[rollbackedToBlkNum-1].Header),
	}
	actualBlockchainInfo := blkfileMgrWrapper.blockfileMgr.getBlockchainInfo()
	require.Equal(t, expectedBlockchainInfo, actualBlockchainInfo)

	// 2. Check the BlockfilesInfo after the rollback
	expectedBlkfilesInfoLastBlockNumber := rollbackedToBlkNum
	expectedBlkfilesInfoIsNoBlkfiles := false
	expectedBlockchainInfoLastFileSuffixNum := lastFileSuffixNum
	actualBlkfilesInfo, err := blkfileMgrWrapper.blockfileMgr.loadBlkfilesInfo()
	require.NoError(t, err)
	require.Equal(t, expectedBlkfilesInfoLastBlockNumber, actualBlkfilesInfo.lastPersistedBlock)
	require.Equal(t, expectedBlkfilesInfoIsNoBlkfiles, actualBlkfilesInfo.noBlockFiles)
	require.Equal(t, expectedBlockchainInfoLastFileSuffixNum, actualBlkfilesInfo.latestFileNumber)

	// 3. Check whether all blocks till the target block number are stored correctly
	if blkfileMgrWrapper.blockfileMgr.index.isAttributeIndexed(IndexableAttrBlockNum) {
		blkfileMgrWrapper.testGetBlockByNumber(blocks[:rollbackedToBlkNum+1])
	}
	if blkfileMgrWrapper.blockfileMgr.index.isAttributeIndexed(IndexableAttrBlockHash) {
		blkfileMgrWrapper.testGetBlockByHash(blocks[:rollbackedToBlkNum+1])
	}
	if blkfileMgrWrapper.blockfileMgr.index.isAttributeIndexed(IndexableAttrTxID) {
		blkfileMgrWrapper.testGetBlockByTxID(blocks[:rollbackedToBlkNum+1])
	}

	// 4. Check whether all blocks with number greater than target block number
	// are removed including index entries
	if blkfileMgrWrapper.blockfileMgr.index.isAttributeIndexed(IndexableAttrBlockHash) {
		blkfileMgrWrapper.testGetBlockByHashNotIndexed(blocks[rollbackedToBlkNum+1:])
	}
	if blkfileMgrWrapper.blockfileMgr.index.isAttributeIndexed(IndexableAttrTxID) {
		blkfileMgrWrapper.testGetBlockByTxIDNotIndexed(blocks[rollbackedToBlkNum+1:])
	}

	// 5. Close the blkfileMgrWrapper
	env.provider.Close()
	blkfileMgrWrapper.close()
}
