package state

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/tendermint/abci/example/dummy"
	abci "github.com/tendermint/abci/types"
	crypto "github.com/tendermint/go-crypto"
	"github.com/tendermint/tendermint/proxy"
	"github.com/tendermint/tendermint/types"
	dbm "github.com/tendermint/tmlibs/db"
	"github.com/tendermint/tmlibs/log"
)

var (
	privKey      = crypto.GenPrivKeyEd25519FromSecret([]byte("execution_test"))
	chainID      = "execution_chain"
	testPartSize = 65536
	nTxsPerBlock = 10
)

func TestValidateBlock(t *testing.T) {
	state := state()
	state.SetLogger(log.TestingLogger())

	// proper block must pass
	block := makeBlock(state, 1)
	err := state.ValidateBlock(block)
	require.NoError(t, err)

	// wrong chain fails
	block = makeBlock(state, 1)
	block.ChainID = "not-the-real-one"
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong height fails
	block = makeBlock(state, 1)
	block.Height += 10
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong total tx fails
	block = makeBlock(state, 1)
	block.TotalTxs += 10
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong blockid fails
	block = makeBlock(state, 1)
	block.LastBlockID.PartsHeader.Total += 10
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong app hash fails
	block = makeBlock(state, 1)
	block.AppHash = []byte("wrong app hash")
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong consensus hash fails
	block = makeBlock(state, 1)
	block.ConsensusHash = []byte("wrong consensus hash")
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong results hash fails
	block = makeBlock(state, 1)
	block.LastResultsHash = []byte("wrong results hash")
	err = state.ValidateBlock(block)
	require.Error(t, err)

	// wrong validators hash fails
	block = makeBlock(state, 1)
	block.ValidatorsHash = []byte("wrong validators hash")
	err = state.ValidateBlock(block)
	require.Error(t, err)
}

func TestApplyBlock(t *testing.T) {
	cc := proxy.NewLocalClientCreator(dummy.NewDummyApplication())
	proxyApp := proxy.NewAppConns(cc, nil)
	err := proxyApp.Start()
	require.Nil(t, err)
	defer proxyApp.Stop()

	state := state()
	state.SetLogger(log.TestingLogger())

	block := makeBlock(state, 1)

	err = state.ApplyBlock(types.NopEventBus{}, proxyApp.Consensus(), block, block.MakePartSet(testPartSize).Header(), types.MockMempool{})
	require.Nil(t, err)

	// TODO check state and mempool
}

// TestBeginBlockAbsentValidators ensures we send absent validators list.
func TestBeginBlockAbsentValidators(t *testing.T) {
	app := &testApp{}
	cc := proxy.NewLocalClientCreator(app)
	proxyApp := proxy.NewAppConns(cc, nil)
	err := proxyApp.Start()
	require.Nil(t, err)
	defer proxyApp.Stop()

	state := state()
	state.SetLogger(log.TestingLogger())

	// there were 2 validators
	val1PrivKey := crypto.GenPrivKeyEd25519()
	val2PrivKey := crypto.GenPrivKeyEd25519()
	lastValidators := types.NewValidatorSet([]*types.Validator{
		types.NewValidator(val1PrivKey.PubKey(), 10),
		types.NewValidator(val2PrivKey.PubKey(), 5),
	})

	prevHash := state.LastBlockID.Hash
	prevParts := types.PartSetHeader{}
	prevBlockID := types.BlockID{prevHash, prevParts}

	now := time.Now().UTC()
	testCases := []struct {
		desc                     string
		lastCommitPrecommits     []*types.Vote
		expectedAbsentValidators []int32
	}{
		{"none absent", []*types.Vote{{ValidatorIndex: 0, Timestamp: now}, {ValidatorIndex: 1, Timestamp: now}}, []int32{}},
		{"one absent", []*types.Vote{{ValidatorIndex: 0, Timestamp: now}, nil}, []int32{1}},
		{"multiple absent", []*types.Vote{nil, nil}, []int32{0, 1}},
	}

	for _, tc := range testCases {
		lastCommit := &types.Commit{BlockID: prevBlockID, Precommits: tc.lastCommitPrecommits}

		block, _ := state.MakeBlock(2, makeTxs(2), lastCommit)
		_, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), lastValidators)
		require.Nil(t, err, tc.desc)

		// -> app must receive an index of the absent validator
		assert.Equal(t, tc.expectedAbsentValidators, app.AbsentValidators, tc.desc)
	}
}

//----------------------------------------------------------------------------

// make some bogus txs
func makeTxs(height int64) (txs []types.Tx) {
	for i := 0; i < nTxsPerBlock; i++ {
		txs = append(txs, types.Tx([]byte{byte(height), byte(i)}))
	}
	return txs
}

func state() *State {
	s, _ := MakeGenesisState(dbm.NewMemDB(), &types.GenesisDoc{
		ChainID: chainID,
		Validators: []types.GenesisValidator{
			{privKey.PubKey(), 10000, "test"},
		},
		AppHash: nil,
	})
	return s
}

func makeBlock(state *State, height int64) *types.Block {
	block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit))
	return block
}

//----------------------------------------------------------------------------

var _ abci.Application = (*testApp)(nil)

type testApp struct {
	abci.BaseApplication

	AbsentValidators []int32
}

func NewDummyApplication() *testApp {
	return &testApp{}
}

func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) {
	return abci.ResponseInfo{}
}

func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock {
	app.AbsentValidators = req.AbsentValidators
	return abci.ResponseBeginBlock{}
}

func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx {
	return abci.ResponseDeliverTx{Tags: []*abci.KVPair{}}
}

func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx {
	return abci.ResponseCheckTx{}
}

func (app *testApp) Commit() abci.ResponseCommit {
	return abci.ResponseCommit{}
}

func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) {
	return
}