package wallet

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"reflect"
	"testing"
	"time"

	"github.com/vapor/account"
	acc "github.com/vapor/account"
	"github.com/vapor/blockchain/pseudohsm"
	"github.com/vapor/blockchain/signers"
	"github.com/vapor/blockchain/txbuilder"
	"github.com/vapor/common"
	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519/chainkd"
	dbm "github.com/vapor/database/leveldb"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

// MockBlock mock a block
func MockBlock(txs []*types.Tx) *types.Block {
	return &types.Block{
		BlockHeader:  types.BlockHeader{Timestamp: uint64(time.Now().Nanosecond())},
		Transactions: txs,
	}
}

func MockSimpleUtxo(index uint64, assetID *bc.AssetID, amount uint64, ctrlProg *account.CtrlProgram) *account.UTXO {
	if ctrlProg == nil {
		ctrlProg = &account.CtrlProgram{
			AccountID:      "",
			Address:        "",
			KeyIndex:       uint64(0),
			ControlProgram: []byte{81},
			Change:         false,
		}
	}

	utxo := &account.UTXO{
		OutputID:            bc.Hash{V0: 1},
		SourceID:            bc.Hash{V0: 1},
		AssetID:             *assetID,
		Amount:              amount,
		SourcePos:           index,
		ControlProgram:      ctrlProg.ControlProgram,
		ControlProgramIndex: ctrlProg.KeyIndex,
		AccountID:           ctrlProg.AccountID,
		Address:             ctrlProg.Address,
		ValidHeight:         0,
	}

	return utxo
}

func AddInput(sourceID bc.Hash, assetID bc.AssetID, amount uint64, pos uint64, controlProgram []byte) *types.TxInput {
	return types.NewSpendInput(nil, sourceID, assetID, amount, pos, controlProgram)
}

func AddTxOutput(assetID bc.AssetID, amount uint64, controlProgram []byte) *types.TxOutput {
	return types.NewIntraChainOutput(assetID, amount, controlProgram)
}

func BuildTx(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.Template, error) {
	tplBuilder, err := CreateTxBuilder(baseUtxo, signer)
	if err != nil {
		return nil, err
	}

	tpl, _, err := tplBuilder.Build()
	if err != nil {
		return nil, err
	}

	return tpl, nil
}

func CreateTxBuilder(baseUtxo *account.UTXO, signer *signers.Signer) (*txbuilder.TemplateBuilder, error) {
	tplBuilder := txbuilder.NewBuilder(time.Now())
	txInput := AddInput(bc.Hash{V0: 1}, baseUtxo.AssetID, 10000, uint64(1), baseUtxo.ControlProgram)
	txOutput := AddTxOutput(baseUtxo.AssetID, 100, baseUtxo.ControlProgram)
	tplBuilder.AddInput(txInput, &txbuilder.SigningInstruction{Position: 0})
	tplBuilder.AddOutput(txOutput)
	return tplBuilder, nil
}

func MockTxsP2PKH(acctMgr *account.Manager, xPub chainkd.XPub, multiTypeAccount bool) ([]*types.Tx, error) {
	txs := []*types.Tx{}
	accts := []*account.Account{}
	for i := uint32(1); i < 32; i = i + 1 + rand.Uint32()%5 {
		alias := fmt.Sprintf("testAccount%d", i)
		deriveRule := signers.BIP0044
		if multiTypeAccount {
			deriveRule = uint8(rand.Uint32() % 2)
		}
		acct, err := account.CreateAccount([]chainkd.XPub{xPub}, 1, alias, uint64(i), deriveRule)
		if err != nil {
			return nil, err
		}

		if err := acctMgr.SaveAccount(acct); err != nil {
			return nil, err
		}

		accts = append(accts, acct)
	}

	for _, acct := range accts {
		for i := uint32(1); i < 256; i = i + 1 + rand.Uint32()%16 {
			controlProg, err := account.CreateCtrlProgram(acct, uint64(i), false)
			if err != nil {
				return nil, err
			}

			if err := acctMgr.SaveControlPrograms(controlProg); err != nil {
				return nil, err
			}

			utxo := MockSimpleUtxo(0, consensus.BTMAssetID, 1000000000, controlProg)
			tpl, err := BuildTx(utxo, acct.Signer)
			if err != nil {
				return nil, err
			}

			txs = append(txs, tpl.Transaction)
		}
	}

	return txs, nil
}

func TestXPubsRecoveryLock(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	walletStore := NewMockWalletStore(testDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub, _, err := hsm.XCreate("test_pub", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(walletStore, acctMgr)
	recoveryMgr.state = newRecoveryState()
	recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
	recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)

	recoveryMgr.state.StartTime = time.Now()
	recoveryMgr.commitStatusInfo()

	if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
		t.Fatal("TestXPubsRecoveryLock err:", err)
	}

	if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != errors.Root(ErrRecoveryBusy) {
		t.Fatal("TestXPubsRecoveryLock err:", err)
	}

	if err := recoveryMgr.LoadStatusInfo(); err != errors.Root(ErrRecoveryBusy) {
		t.Fatal("TestXPubsRecoveryLock err:", err)
	}

	recoveryMgr.stopXPubsRec()
	if err := recoveryMgr.LoadStatusInfo(); err != nil {
		t.Fatal("TestXPubsRecoveryLock err:", err)
	}
	recoveryMgr.finished()
	if err := recoveryMgr.AcctResurrect([]chainkd.XPub{xpub.XPub}); err != nil {
		t.Fatal("TestXPubsRecoveryLock err:", err)
	}
}

func TestExtendScanAddresses(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	walletStore := NewMockWalletStore(testDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub, _, err := hsm.XCreate("test_pub", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(walletStore, acctMgr)
	acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
	acc2 := &account.Account{ID: "testB", Alias: "test2"}
	acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 2, DeriveRule: 3}}
	acc4 := &account.Account{ID: "testD", Alias: "test4", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 3, DeriveRule: signers.BIP0032}}

	recoveryMgr.state.stateForScope(acc1)
	recoveryMgr.state.stateForScope(acc3)
	recoveryMgr.state.stateForScope(acc4)

	cases := []struct {
		acct       *account.Account
		err        error
		addressLen uint64
	}{
		{acc1, nil, addrRecoveryWindow * 2},
		{acc2, ErrInvalidAcctID, addrRecoveryWindow * 2},
		{acc3, signers.ErrDeriveRule, addrRecoveryWindow * 2},
		{acc4, nil, addrRecoveryWindow * 3},
	}

	for _, c := range cases {
		if err := recoveryMgr.extendScanAddresses(c.acct.ID, true); err != c.err {
			t.Fatal("extend scan addresses err:", err)
		}

		if err := recoveryMgr.extendScanAddresses(c.acct.ID, false); err != c.err {
			t.Fatal("extend scan addresses err:", err)
		}

		if uint64(len(recoveryMgr.addresses)) != c.addressLen {
			t.Fatalf("extend scan addresses err: len:%d,want:%d", len(recoveryMgr.addresses), c.addressLen)
		}
	}
}

func TestRecoveryFromXPubs(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
	recoveryStore := NewMockWalletStore(recoveryDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub, _, err := hsm.XCreate("test_pub", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, false)
	recActStore := NewMockAccountStore(recoveryDB)
	recAcctMgr := account.NewManager(recActStore, nil)
	recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr)

	cases := []struct {
		xPubs []chainkd.XPub
		err   error
	}{
		{[]chainkd.XPub{xpub.XPub}, nil},
		{[]chainkd.XPub{xpub.XPub, xpub.XPub}, signers.ErrDupeXPub},
		{[]chainkd.XPub{}, signers.ErrNoXPubs},
	}

	for _, c := range cases {
		if err := recoveryMgr.AcctResurrect(c.xPubs); errors.Root(err) != c.err {
			t.Fatal("recovery from XPubs err:", err)
		}

		if err != nil {
			recoveryMgr.finished()
			continue
		}
		if err := recoveryMgr.FilterRecoveryTxs(MockBlock(txs)); err != nil {
			t.Fatal("recovery from XPubs err:", err)
		}

		Accounts, err := acctMgr.ListAccounts("")
		if err != nil {
			t.Fatal("recovery from XPubs err:", err)
		}

		for _, acct := range Accounts {
			tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
			if err != nil && err != acc.ErrFindAccount {
				t.Fatal("recovery from XPubs err:", err)
			}

			if tmp == nil {
				t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
			}

			if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
				t.Fatal("bip44 internal address index recovery from xpubs err")
			}

			if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
				t.Fatal("bip44 external address index recovery from xpubs err")
			}
		}

		recoveryMgr.finished()
	}
}

func TestRecoveryByRescanAccount(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
	recoveryStore := NewMockWalletStore(recoveryDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub, _, err := hsm.XCreate("test_pub", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, true)
	if err != nil {
		t.Fatal("recovery by rescan account err:", err)
	}

	allAccounts, err := acctMgr.ListAccounts("")
	if err != nil {
		t.Fatal("recovery by rescan account err:", err)
	}

	recActStore := NewMockAccountStore(recoveryDB)
	recAcctMgr := account.NewManager(recActStore, nil)
	for _, acct := range allAccounts {
		if err := recAcctMgr.SaveAccount(acct); err != nil {
			t.Fatal("recovery by rescan account err:", err)
		}
	}

	recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr)

	acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}

	cases := []struct {
		accounts []*account.Account
		err      error
	}{
		{allAccounts, nil},
		{[]*account.Account{acct}, signers.ErrDeriveRule},
	}

	for _, c := range cases {
		if err := recoveryMgr.AddrResurrect(c.accounts); errors.Root(err) != c.err {
			t.Fatal("recovery by rescan account err:", err)
		}

		if err != nil {
			continue
		}
		recoveryMgr.FilterRecoveryTxs(MockBlock(txs))
		accounts, err := acctMgr.ListAccounts("")
		if err != nil {
			t.Fatal("recovery from XPubs err:", err)
		}

		for _, acct := range accounts {
			tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
			if err != nil && err != acc.ErrFindAccount {
				t.Fatal("recovery from XPubs err:", err)
			}

			if tmp == nil {
				t.Fatal("accout recovery from xpubs err:", acct.KeyIndex)
			}

			if acctMgr.GetBip44ContractIndex(acct.ID, true) != recAcctMgr.GetBip44ContractIndex(tmp.ID, true) {
				t.Fatal("bip44 internal address index recovery from xpubs err")
			}

			if acctMgr.GetBip44ContractIndex(acct.ID, false) != recAcctMgr.GetBip44ContractIndex(tmp.ID, false) {
				t.Fatal("bip44 external address index recovery from xpubs err")
			}

			if acctMgr.GetContractIndex(acct.ID) != recAcctMgr.GetContractIndex(tmp.ID) {
				t.Fatal("bip32 address index recovery from xpubs err")
			}
		}
	}

}

func TestReportFound(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	testStore := NewMockWalletStore(testDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	xpub2, _, err := hsm.XCreate("test_pub2", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(testStore, acctMgr)
	acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
	acc2 := &account.Account{ID: "testB", Alias: "test2", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0032}}
	acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 2, DeriveRule: signers.BIP0044}}

	cp1 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 10, Change: false}
	cp2 := &account.CtrlProgram{AccountID: acc1.ID, Address: "address1", KeyIndex: 20, Change: true}
	cp3 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 30, Change: false}
	cp4 := &account.CtrlProgram{AccountID: acc2.ID, Address: "address1", KeyIndex: 40, Change: true}
	cp5 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 50, Change: false}
	cp6 := &account.CtrlProgram{AccountID: acc3.ID, Address: "address1", KeyIndex: 60, Change: true}

	if err := acctMgr.SaveAccount(acc2); err != nil {
		t.Fatal("ReportFound test err:", err)
	}

	if err := acctMgr.SaveAccount(acc3); err != nil {
		t.Fatal("ReportFound test err:", err)
	}

	recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
	recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
	recoveryMgr.state.stateForScope(acc1)
	recoveryMgr.state.stateForScope(acc2)
	recoveryMgr.state.stateForScope(acc3)

	cases := []struct {
		acct   *account.Account
		cp     *account.CtrlProgram
		err    error
		status *addressRecoveryState
	}{
		{acc1, cp1, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
		{acc2, cp3, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
		{acc1, cp2, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 149, 21}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 139, 11}, Account: acc1}},
		{acc2, cp4, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 169, 41}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 159, 31}, Account: acc2}},
		{acc3, cp5, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 1, 1}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
		{acc3, cp6, nil,
			&addressRecoveryState{InternalBranch: &branchRecoveryState{addrRecoveryWindow, 189, 61}, ExternalBranch: &branchRecoveryState{addrRecoveryWindow, 179, 51}, Account: acc3}},
	}

	for _, c := range cases {
		if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
			t.Fatal("ReportFound test err:", err, c.acct.ID)
		}

		status, ok := recoveryMgr.state.AccountsStatus[c.acct.ID]
		if !ok {
			t.Fatal("ReportFound test err: can not find status")
		}
		if !reflect.DeepEqual(status, c.status) {
			t.Log(c.status.Account, c.status.InternalBranch, c.status.ExternalBranch)
			t.Log(status.Account, status.InternalBranch, status.ExternalBranch)
			t.Fatal("ReportFound test err: recovery status error")
		}
	}
}

func TestLoadStatusInfo(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", "temp")
	testStore := NewMockWalletStore(testDB)
	defer os.RemoveAll("temp")

	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub, _, err := hsm.XCreate("test_pub", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(testStore, acctMgr)
	// StatusInit init recovery status manager.
	recoveryMgr.state = newRecoveryState()
	recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
	recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)

	recoveryMgr.state.StartTime = time.Now()
	if err := recoveryMgr.LoadStatusInfo(); err != nil {
		t.Fatal("TestLoadStatusInfo err:", err)
	}

	recoveryMgr.commitStatusInfo()

	recoveryMgrRestore := NewRecoveryManager(testStore, acctMgr)
	if err := recoveryMgrRestore.LoadStatusInfo(); err != nil {
		t.Fatal("TestLoadStatusInfo err:", err)
	}

	if !reflect.DeepEqual(recoveryMgrRestore.state.XPubsStatus, recoveryMgr.state.XPubsStatus) {
		t.Fatalf("TestLoadStatusInfo XPubsStatus reload err")
	}

	if !reflect.DeepEqual(recoveryMgrRestore.state.XPubs, recoveryMgr.state.XPubs) {
		t.Fatalf("TestLoadStatusInfo XPubs reload err")
	}

	if !reflect.DeepEqual(recoveryMgrRestore.state.AccountsStatus, recoveryMgr.state.AccountsStatus) {
		t.Fatalf("TestLoadStatusInfo AccountsStatus reload err")
	}

	if !recoveryMgrRestore.state.StartTime.Equal(recoveryMgr.state.StartTime) {
		t.Fatalf("TestLoadStatusInfo StartTime reload err")
	}

	acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
	recoveryMgr.state.AccountsStatus[acct.ID] = newAddressRecoveryState(addrRecoveryWindow, acct)
	if err := recoveryMgr.commitStatusInfo(); err != nil {
		t.Fatal("TestLoadStatusInfo err:", err)
	}
	if err := recoveryMgr.LoadStatusInfo(); err == nil {
		t.Fatal("TestLoadStatusInfo err")
	}

	recoveryMgr.state = nil
	if err := recoveryMgr.commitStatusInfo(); err != nil {
		t.Fatal("TestLoadStatusInfo err:", err)
	}

	if err := recoveryMgr.LoadStatusInfo(); err == nil {
		t.Fatal("TestLoadStatusInfo err")
	}
}

func TestLock(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", "temp")
	testStore := NewMockWalletStore(testDB)
	defer os.RemoveAll("temp")

	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(testStore, acctMgr)
	if !recoveryMgr.tryStartXPubsRec() {
		t.Fatal("recovery manager try lock test err")
	}

	if recoveryMgr.tryStartXPubsRec() {
		t.Fatal("recovery manager relock test err")
	}

	recoveryMgr.stopXPubsRec()

	if !recoveryMgr.tryStartXPubsRec() {
		t.Fatal("recovery manager try lock test err")
	}
}

func TestStateForScope(t *testing.T) {
	state := newRecoveryState()
	acc1 := &account.Account{ID: "test1", Alias: "testA"}
	state.stateForScope(acc1)
	if !reflect.DeepEqual(state.AccountsStatus[acc1.ID].Account, acc1) {
		t.Fatal("state for scope test err")
	}

	acc2 := &account.Account{ID: "test1", Alias: "testB"}
	state.stateForScope(acc2)

	if reflect.DeepEqual(state.AccountsStatus[acc2.ID].Account, acc2) {
		t.Fatal("state for scope test err")
	}

	acc3 := &account.Account{ID: "test2", Alias: "testC"}
	state.stateForScope(acc3)
	if !reflect.DeepEqual(state.AccountsStatus[acc3.ID].Account, acc3) {
		t.Fatal("state for scope test err")
	}
}

func TestContractIndexResidue(t *testing.T) {
	dirPath, err := ioutil.TempDir(".", "")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dirPath)

	testDB := dbm.NewDB("testdb", "leveldb", dirPath)
	testStore := NewMockWalletStore(testDB)
	hsm, err := pseudohsm.New(dirPath)
	if err != nil {
		t.Fatal(err)
	}

	xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
	if err != nil {
		t.Fatal(err)
	}

	contractIndexResidue := uint64(5)
	acctStore := NewMockAccountStore(testDB)
	acctMgr := account.NewManager(acctStore, nil)
	recoveryMgr := NewRecoveryManager(testStore, acctMgr)
	acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}

	cp1 := &account.CtrlProgram{AccountID: acct.ID, Address: "address1", KeyIndex: 10, Change: false}

	setContractIndexKey := func(acctMgr *account.Manager, accountID string, change bool) {
		testDB.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue))
	}

	delAccount := func(acctMgr *account.Manager, accountID string, change bool) {
		acctMgr.DeleteAccount(accountID)
	}

	recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
	recoveryMgr.state.XPubs = []chainkd.XPub{xpub1.XPub}
	recoveryMgr.state.stateForScope(acct)

	cases := []struct {
		acct       *account.Account
		cp         *account.CtrlProgram
		preProcess func(acctMgr *account.Manager, accountID string, change bool)
		err        error
		wantCPNum  uint64
	}{
		{acct, cp1, setContractIndexKey, nil, 5},
		{acct, cp1, delAccount, nil, 10},
	}

	for _, c := range cases {
		if c.preProcess != nil {
			c.preProcess(acctMgr, c.acct.ID, c.cp.Change)
		}

		if err := acctMgr.SaveAccount(acct); err != nil {
			t.Fatal("ReportFound test err:", err)
		}

		if err := recoveryMgr.reportFound(c.acct, c.cp); err != c.err {
			t.Fatal("ContractIndexResidue test err:", err, c.acct.ID)
		}
		cps, err := acctMgr.ListControlProgram()
		if err != nil {
			t.Fatal("list control program err:", err)
		}

		cpNum := uint64(0)
		for _, cp := range cps {
			if cp.Address == "" || cp.AccountID != c.acct.ID {
				continue
			}
			cpNum++
		}

		if cpNum != c.wantCPNum {
			t.Fatal("Test contract index residue cp num err want:", c.wantCPNum, " got:", cpNum)
		}
	}
}
