// Copyright 2018 The dexon-consensus Authors
// This file is part of the dexon-consensus library.
//
// The dexon-consensus library is free software: you can redistribute it
// and/or modify it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// The dexon-consensus library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the dexon-consensus library. If not, see
// <http://www.gnu.org/licenses/>.

package core

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/suite"

	"github.com/dexon-foundation/dexon-consensus/common"
	"github.com/dexon-foundation/dexon-consensus/core/crypto"
	"github.com/dexon-foundation/dexon-consensus/core/db"
	"github.com/dexon-foundation/dexon-consensus/core/test"
	"github.com/dexon-foundation/dexon-consensus/core/types"
	typesDKG "github.com/dexon-foundation/dexon-consensus/core/types/dkg"
	"github.com/dexon-foundation/dexon-consensus/core/utils"
)

// network implements core.Network.
type network struct {
	nID  types.NodeID
	conn *networkConnection
}

// PullBlocks tries to pull blocks from the DEXON network.
func (n *network) PullBlocks(common.Hashes) {
}

// PullVotes tries to pull votes from the DEXON network.
func (n *network) PullVotes(types.Position) {
}

// PullRandomness tries to pull randomness from the DEXON network.
func (n *network) PullRandomness(common.Hashes) {
}

// BroadcastVote broadcasts vote to all nodes in DEXON network.
func (n *network) BroadcastVote(vote *types.Vote) {
	n.conn.broadcast(n.nID, vote)
}

// BroadcastBlock broadcasts block to all nodes in DEXON network.
func (n *network) BroadcastBlock(block *types.Block) {
	n.conn.broadcast(n.nID, block)
}

// BroadcastAgreementResult broadcasts agreement result to DKG set.
func (n *network) BroadcastAgreementResult(
	randRequest *types.AgreementResult) {
	n.conn.broadcast(n.nID, randRequest)
}

// SendDKGPrivateShare sends PrivateShare to a DKG participant.
func (n *network) SendDKGPrivateShare(
	recv crypto.PublicKey, prvShare *typesDKG.PrivateShare) {
	n.conn.send(types.NewNodeID(recv), prvShare)
}

// BroadcastDKGPrivateShare broadcasts PrivateShare to all DKG participants.
func (n *network) BroadcastDKGPrivateShare(
	prvShare *typesDKG.PrivateShare) {
	n.conn.broadcast(n.nID, prvShare)
}

// BroadcastDKGPartialSignature broadcasts partialSignature to all
// DKG participants.
func (n *network) BroadcastDKGPartialSignature(
	psig *typesDKG.PartialSignature) {
	n.conn.broadcast(n.nID, psig)
}

// ReceiveChan returns a channel to receive messages from DEXON network.
func (n *network) ReceiveChan() <-chan interface{} {
	return make(chan interface{})
}

func (nc *networkConnection) broadcast(from types.NodeID, msg interface{}) {
	for nID := range nc.cons {
		if nID == from {
			continue
		}
		nc.send(nID, msg)
	}
}

func (nc *networkConnection) send(to types.NodeID, msg interface{}) {
	ch, exist := nc.cons[to]
	if !exist {
		return
	}
	msgCopy := msg
	// Clone msg if necessary.
	switch val := msg.(type) {
	case *types.Block:
		msgCopy = val.Clone()
	case *typesDKG.PrivateShare:
		// Use Marshal/Unmarshal to do deep copy.
		data, err := json.Marshal(val)
		if err != nil {
			panic(err)
		}
		valCopy := &typesDKG.PrivateShare{}
		if err := json.Unmarshal(data, valCopy); err != nil {
			panic(err)
		}
		msgCopy = valCopy
	}
	ch <- msgCopy
}

type networkConnection struct {
	s    *ConsensusTestSuite
	cons map[types.NodeID]chan interface{}
}

func (nc *networkConnection) newNetwork(nID types.NodeID) *network {
	return &network{
		nID:  nID,
		conn: nc,
	}
}

func (nc *networkConnection) setCon(nID types.NodeID, con *Consensus) {
	ch := make(chan interface{}, 1000)
	go func() {
		for {
			msg := <-ch
			var err error
			// Testify package does not support concurrent call.
			// Use panic() to detact error.
			switch val := msg.(type) {
			case *types.Block:
				err = con.preProcessBlock(val)
			case *types.Vote:
				err = con.ProcessVote(val)
			case *types.AgreementResult:
				err = con.ProcessAgreementResult(val)
			case *typesDKG.PrivateShare:
				err = con.cfgModule.processPrivateShare(val)
			case *typesDKG.PartialSignature:
				err = con.cfgModule.processPartialSignature(val)
			}
			if err != nil {
				panic(err)
			}
		}
	}()
	nc.cons[nID] = ch
}

type ConsensusTestSuite struct {
	suite.Suite
	conn *networkConnection
}

func (s *ConsensusTestSuite) newNetworkConnection() *networkConnection {
	return &networkConnection{
		s:    s,
		cons: make(map[types.NodeID]chan interface{}),
	}
}

func (s *ConsensusTestSuite) prepareConsensus(
	dMoment time.Time,
	gov *test.Governance,
	prvKey crypto.PrivateKey,
	conn *networkConnection) (
	*test.App, *Consensus) {

	app := test.NewApp(0, nil, nil)
	dbInst, err := db.NewMemBackedDB()
	s.Require().NoError(err)
	nID := types.NewNodeID(prvKey.PublicKey())
	network := conn.newNetwork(nID)
	con := NewConsensus(
		dMoment, app, gov, dbInst, network, prvKey, &common.NullLogger{})
	conn.setCon(nID, con)
	return app, con
}

func (s *ConsensusTestSuite) prepareConsensusWithDB(
	dMoment time.Time,
	gov *test.Governance,
	prvKey crypto.PrivateKey,
	conn *networkConnection,
	dbInst db.Database) (
	*test.App, *Consensus) {

	app := test.NewApp(0, nil, nil)
	nID := types.NewNodeID(prvKey.PublicKey())
	network := conn.newNetwork(nID)
	con := NewConsensus(
		dMoment, app, gov, dbInst, network, prvKey, &common.NullLogger{})
	conn.setCon(nID, con)
	return app, con
}

func (s *ConsensusTestSuite) TestRegisteredDKGRecover() {
	conn := s.newNetworkConnection()
	prvKeys, pubKeys, err := test.NewKeys(1)
	s.Require().NoError(err)
	gov, err := test.NewGovernance(test.NewState(DKGDelayRound,
		pubKeys, time.Second, &common.NullLogger{}, true), ConfigRoundShift)
	s.Require().NoError(err)
	gov.State().RequestChange(test.StateChangeRoundLength, uint64(200))
	dMoment := time.Now().UTC()
	dbInst, err := db.NewMemBackedDB()
	s.Require().NoError(err)
	_, con := s.prepareConsensusWithDB(dMoment, gov, prvKeys[0], conn, dbInst)

	s.Require().Nil(con.cfgModule.dkg)

	con.cfgModule.registerDKG(con.ctx, 0, 0, 10)
	con.cfgModule.dkgLock.Lock()
	defer con.cfgModule.dkgLock.Unlock()

	_, newCon := s.prepareConsensusWithDB(dMoment, gov, prvKeys[0], conn, dbInst)

	newCon.cfgModule.registerDKG(newCon.ctx, 0, 0, 10)
	newCon.cfgModule.dkgLock.Lock()
	defer newCon.cfgModule.dkgLock.Unlock()

	s.Require().NotNil(newCon.cfgModule.dkg)
	s.Require().True(newCon.cfgModule.dkg.prvShares.Equal(con.cfgModule.dkg.prvShares))
}

func (s *ConsensusTestSuite) TestDKGCRS() {
	n := 21
	lambda := 200 * time.Millisecond
	if testing.Short() {
		n = 7
		lambda = 100 * time.Millisecond
	}
	if isTravisCI() {
		lambda *= 5
	}
	conn := s.newNetworkConnection()
	prvKeys, pubKeys, err := test.NewKeys(n)
	s.Require().NoError(err)
	gov, err := test.NewGovernance(test.NewState(DKGDelayRound,
		pubKeys, lambda, &common.NullLogger{}, true), ConfigRoundShift)
	s.Require().NoError(err)
	gov.State().RequestChange(test.StateChangeRoundLength, uint64(200))
	cons := map[types.NodeID]*Consensus{}
	dMoment := time.Now().UTC()
	for _, key := range prvKeys {
		_, con := s.prepareConsensus(dMoment, gov, key, conn)
		nID := types.NewNodeID(key.PublicKey())
		cons[nID] = con
	}
	time.Sleep(gov.Configuration(0).MinBlockInterval * 4)
	for _, con := range cons {
		go con.runDKG(0, 0, 0, 0)
	}
	crsFinish := make(chan struct{}, len(cons))
	for _, con := range cons {
		go func(con *Consensus) {
			height := uint64(0)
		Loop:
			for {
				select {
				case <-crsFinish:
					break Loop
				case <-time.After(lambda):
				}
				con.event.NotifyHeight(height)
				height++
			}
		}(con)
	}
	for _, con := range cons {
		func() {
			con.dkgReady.L.Lock()
			defer con.dkgReady.L.Unlock()
			for con.dkgRunning != 2 {
				con.dkgReady.Wait()
			}
		}()
	}
	for _, con := range cons {
		go func(con *Consensus) {
			con.runCRS(0, gov.CRS(0), false)
			crsFinish <- struct{}{}
		}(con)
	}
	s.NotNil(gov.CRS(1))
}

func (s *ConsensusTestSuite) TestSyncBA() {
	lambdaBA := time.Second
	conn := s.newNetworkConnection()
	prvKeys, pubKeys, err := test.NewKeys(4)
	s.Require().NoError(err)
	gov, err := test.NewGovernance(test.NewState(DKGDelayRound,
		pubKeys, lambdaBA, &common.NullLogger{}, true), ConfigRoundShift)
	s.Require().NoError(err)
	prvKey := prvKeys[0]
	_, con := s.prepareConsensus(time.Now().UTC(), gov, prvKey, conn)
	go con.Run()
	defer con.Stop()
	hash := common.NewRandomHash()
	signers := make([]*utils.Signer, 0, len(prvKeys))
	for _, prvKey := range prvKeys {
		signers = append(signers, utils.NewSigner(prvKey))
	}
	pos := types.Position{
		Round:  0,
		Height: 20,
	}
	baResult := &types.AgreementResult{
		BlockHash: hash,
		Position:  pos,
	}
	for _, signer := range signers {
		vote := types.NewVote(types.VoteCom, hash, 0)
		vote.Position = pos
		s.Require().NoError(signer.SignVote(vote))
		baResult.Votes = append(baResult.Votes, *vote)
	}
	// Make sure each agreement module is running. ProcessAgreementResult only
	// works properly when agreement module is running:
	//  - the bias for round begin time would be 4 * lambda.
	//  - the ticker is 1 lambdaa.
	time.Sleep(5 * lambdaBA)
	s.Require().NoError(con.ProcessAgreementResult(baResult))
	aID := con.baMgr.baModule.agreementID()
	s.Equal(pos, aID)

	// Negative cases are moved to TestVerifyAgreementResult in utils_test.go.
}

func (s *ConsensusTestSuite) TestInitialHeightEventTriggered() {
	// Initial block is the last block of corresponding round, in this case,
	// we should make sure all height event handlers could be triggered after
	// returned from Consensus.prepare().
	prvKeys, pubKeys, err := test.NewKeys(4)
	s.Require().NoError(err)
	// Prepare a governance instance, whose DKG-reset-count for round 2 is 1.
	gov, err := test.NewGovernance(test.NewState(DKGDelayRound,
		pubKeys, time.Second, &common.NullLogger{}, true), ConfigRoundShift)
	gov.State().RequestChange(test.StateChangeRoundLength, uint64(100))
	s.Require().NoError(err)
	gov.NotifyRound(2, 201)
	gov.NotifyRound(3, 301)
	hash := common.NewRandomHash()
	gov.ProposeCRS(2, hash[:])
	hash = common.NewRandomHash()
	gov.ResetDKG(hash[:])
	s.Require().Equal(gov.DKGResetCount(2), uint64(1))
	prvKey := prvKeys[0]
	initBlock := &types.Block{
		Hash:     common.NewRandomHash(),
		Position: types.Position{Round: 1, Height: 200},
	}
	dbInst, err := db.NewMemBackedDB()
	s.Require().NoError(err)
	nID := types.NewNodeID(prvKey.PublicKey())
	conn := s.newNetworkConnection()
	network := conn.newNetwork(nID)
	con, err := NewConsensusFromSyncer(
		initBlock,
		false,
		time.Now().UTC(),
		test.NewApp(0, nil, nil),
		gov,
		dbInst,
		network,
		prvKey,
		[]*types.Block(nil),
		[]interface{}(nil),
		&common.NullLogger{},
	)
	s.Require().NoError(err)
	// Here is the tricky part, check if block chain module can handle the
	// block with height == 200.
	s.Require().Equal(con.bcModule.configs[0].RoundID(), uint64(1))
	s.Require().Equal(con.bcModule.configs[0].RoundEndHeight(), uint64(301))
}

func TestConsensus(t *testing.T) {
	suite.Run(t, new(ConsensusTestSuite))
}