// Copyright 2018 The dexon-consensus-core Authors
// This file is part of the dexon-consensus-core library.
//
// The dexon-consensus-core 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-core 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-core library. If not, see
// <http://www.gnu.org/licenses/>.

package core

import (
	"math/rand"
	"testing"
	"time"

	"github.com/dexon-foundation/dexon-consensus-core/common"
	"github.com/dexon-foundation/dexon-consensus-core/core/blockdb"
	"github.com/dexon-foundation/dexon-consensus-core/core/crypto/ecdsa"
	"github.com/dexon-foundation/dexon-consensus-core/core/test"
	"github.com/dexon-foundation/dexon-consensus-core/core/types"
	"github.com/stretchr/testify/suite"
)

// testLatticeMgr wraps compaction chain and lattice.
type testLatticeMgr struct {
	lattice  *Lattice
	ccModule *compactionChain
	app      *test.App
	db       blockdb.BlockDatabase
}

func (mgr *testLatticeMgr) prepareBlock(
	chainID uint32) (b *types.Block, err error) {

	b = &types.Block{
		Position: types.Position{
			ChainID: chainID,
		}}
	err = mgr.lattice.PrepareBlock(b, time.Now().UTC())
	return
}

// Process describes the usage of Lattice.ProcessBlock.
func (mgr *testLatticeMgr) processBlock(b *types.Block) (err error) {
	var (
		delivered []*types.Block
	)
	if err = mgr.lattice.SanityCheck(b); err != nil {
		if err == ErrRetrySanityCheckLater {
			err = nil
		} else {
			return
		}
	}
	if err = mgr.db.Put(*b); err != nil {
		if err != blockdb.ErrBlockExists {
			return
		}
		err = nil
	}
	if delivered, err = mgr.lattice.ProcessBlock(b); err != nil {
		return
	}
	// Deliver blocks.
	for _, b = range delivered {
		if err = mgr.ccModule.processBlock(b); err != nil {
			return
		}
	}
	for _, b = range mgr.ccModule.extractBlocks() {
		if err = mgr.db.Update(*b); err != nil {
			return
		}
		mgr.app.BlockDelivered(b.Hash, b.Finalization)
	}
	if err = mgr.lattice.PurgeBlocks(delivered); err != nil {
		return
	}
	return
}

type LatticeTestSuite struct {
	suite.Suite
}

func (s *LatticeTestSuite) newTestLatticeMgr(
	cfg *types.Config, dMoment time.Time) *testLatticeMgr {
	var req = s.Require()
	// Setup private key.
	prvKey, err := ecdsa.NewPrivateKey()
	req.NoError(err)
	// Setup blockdb.
	db, err := blockdb.NewMemBackedBlockDB()
	req.NoError(err)
	// Setup application.
	app := test.NewApp()
	// Setup governance.
	_, pubKeys, err := test.NewKeys(int(cfg.NotarySetSize))
	req.NoError(err)
	gov, err := test.NewGovernance(pubKeys, cfg.LambdaBA)
	req.NoError(err)
	// Setup compaction chain.
	cc := newCompactionChain(gov)
	cc.init(&types.Block{})
	mock := newMockTSigVerifier(true)
	for i := 0; i < cc.tsigVerifier.cacheSize; i++ {
		cc.tsigVerifier.verifier[uint64(i)] = mock
	}
	// Setup lattice.
	return &testLatticeMgr{
		ccModule: cc,
		app:      app,
		db:       db,
		lattice: NewLattice(
			dMoment,
			cfg,
			NewAuthenticator(prvKey),
			app,
			app,
			db,
			&common.NullLogger{})}
}

func (s *LatticeTestSuite) TestBasicUsage() {
	// One Lattice prepare blocks on chains randomly selected each time
	// and process it. Those generated blocks and kept into a buffer, and
	// process by other Lattice instances with random order.
	var (
		blockNum        = 100
		chainNum        = uint32(19)
		otherLatticeNum = 20
		req             = s.Require()
		err             error
		cfg             = types.Config{
			NumChains:        chainNum,
			NotarySetSize:    chainNum,
			PhiRatio:         float32(2) / float32(3),
			K:                0,
			MinBlockInterval: 0,
			MaxBlockInterval: 3000 * time.Second,
			RoundInterval:    time.Hour,
		}
		dMoment   = time.Now().UTC()
		master    = s.newTestLatticeMgr(&cfg, dMoment)
		apps      = []*test.App{master.app}
		revealSeq = map[string]struct{}{}
	)
	// Master-lattice generates blocks.
	for i := uint32(0); i < chainNum; i++ {
		// Produce genesis blocks should be delivered before all other blocks,
		// or the consensus time would be wrong.
		b, err := master.prepareBlock(i)
		req.NotNil(b)
		req.NoError(err)
		// We've ignored the error for "acking blocks don't exist".
		req.NoError(master.processBlock(b))
	}
	for i := 0; i < (blockNum - int(chainNum)); i++ {
		b, err := master.prepareBlock(uint32(rand.Intn(int(chainNum))))
		req.NotNil(b)
		req.NoError(err)
		// We've ignored the error for "acking blocks don't exist".
		req.NoError(master.processBlock(b))
	}
	// Now we have some blocks, replay them on different lattices.
	iter, err := master.db.GetAll()
	req.NoError(err)
	revealer, err := test.NewRandomRevealer(iter)
	req.NoError(err)
	for i := 0; i < otherLatticeNum; i++ {
		revealer.Reset()
		revealed := ""
		other := s.newTestLatticeMgr(&cfg, dMoment)
		for {
			b, err := revealer.Next()
			if err != nil {
				if err == blockdb.ErrIterationFinished {
					err = nil
					break
				}
			}
			req.NoError(err)
			req.NoError(other.processBlock(&b))
			revealed += b.Hash.String() + ","
			revealSeq[revealed] = struct{}{}
		}
		apps = append(apps, other.app)
	}
	// Make sure not only one revealing sequence.
	req.True(len(revealSeq) > 1)
	// Make sure nothing goes wrong.
	for i, app := range apps {
		err := app.Verify()
		req.NoError(err)
		for j, otherApp := range apps {
			if i >= j {
				continue
			}
			err := app.Compare(otherApp)
			s.NoError(err)
		}
	}
}

func (s *LatticeTestSuite) TestSync() {
	// One Lattice prepare blocks on chains randomly selected each time
	// and process it. Those generated blocks and kept into a buffer, and
	// process by other Lattice instances with random order.
	var (
		chainNum        = uint32(19)
		otherLatticeNum = 50
	)
	if testing.Short() {
		chainNum = 13
		otherLatticeNum = 20
	}
	var (
		blockNum = 500
		// The first `desyncNum` blocks revealed are considered "desynced" and will
		// not be delivered to lattice. After `syncNum` blocks have revealed, the
		// system is considered "synced" and start feeding blocks that are desynced
		// to processFinalizedBlock.
		desyncNum = 50
		syncNum   = 150
		req       = s.Require()
		err       error
		cfg       = types.Config{
			NumChains:        chainNum,
			NotarySetSize:    chainNum,
			PhiRatio:         float32(2) / float32(3),
			K:                0,
			MinBlockInterval: 0,
			MaxBlockInterval: 3000 * time.Second,
			RoundInterval:    time.Hour,
		}
		dMoment = time.Now().UTC()
		master  = s.newTestLatticeMgr(&cfg, dMoment)
		//apps      = []*test.App{master.app}
		revealSeq = map[string]struct{}{}
	)
	// Make sure the test setup is correct.
	s.Require().True(syncNum > desyncNum)
	// Master-lattice generates blocks.
	for i := uint32(0); i < chainNum; i++ {
		// Produce genesis blocks should be delivered before all other blocks,
		// or the consensus time would be wrong.
		b, err := master.prepareBlock(i)
		req.NotNil(b)
		req.NoError(err)
		// We've ignored the error for "acking blocks don't exist".
		req.NoError(master.processBlock(b))
	}
	for i := 0; i < (blockNum - int(chainNum)); i++ {
		b, err := master.prepareBlock(uint32(rand.Intn(int(chainNum))))
		req.NotNil(b)
		req.NoError(err)
		// We've ignored the error for "acking blocks don't exist".
		req.NoError(master.processBlock(b))
	}
	req.NoError(master.app.Verify())
	// Now we have some blocks, replay them on different lattices.
	iter, err := master.db.GetAll()
	req.NoError(err)
	revealer, err := test.NewRandomTipRevealer(iter)
	req.NoError(err)
	for i := 0; i < otherLatticeNum; i++ {
		synced := false
		syncFromHeight := uint64(0)
		revealer.Reset()
		revealed := ""
		other := s.newTestLatticeMgr(&cfg, dMoment)
		chainTip := make([]*types.Block, chainNum)
		for height := 0; ; height++ {
			b, err := revealer.Next()
			if err != nil {
				if err == blockdb.ErrIterationFinished {
					err = nil
					break
				}
			}
			req.NoError(err)
			if height >= syncNum && !synced {
				synced = true
				syncToHeight := uint64(0)
				for _, block := range chainTip {
					if block == nil {
						synced = false
						continue
					}
					result, exist := master.app.Delivered[block.Hash]
					req.True(exist)
					if syncToHeight < result.ConsensusHeight {
						syncToHeight = result.ConsensusHeight
					}
				}

				for idx := syncFromHeight; idx < syncToHeight; idx++ {
					block, err := master.db.Get(master.app.DeliverSequence[idx])
					req.Equal(idx+1, block.Finalization.Height)
					req.NoError(err)
					if err = other.db.Put(block); err != nil {
						req.Equal(blockdb.ErrBlockExists, err)
					}
					other.ccModule.processFinalizedBlock(&block)
				}
				extracted := other.ccModule.extractFinalizedBlocks()
				req.Len(extracted, int(syncToHeight-syncFromHeight))
				for _, block := range extracted {
					other.app.StronglyAcked(block.Hash)
					other.lattice.ProcessFinalizedBlock(block)
				}
				syncFromHeight = syncToHeight
			}
			if height > desyncNum {
				if chainTip[b.Position.ChainID] == nil {
					chainTip[b.Position.ChainID] = &b
				}
				if err = other.db.Put(b); err != nil {
					req.Equal(blockdb.ErrBlockExists, err)
				}
				delivered, err := other.lattice.addBlockToLattice(&b)
				req.NoError(err)
				revealed += b.Hash.String() + ","
				revealSeq[revealed] = struct{}{}
				req.NoError(other.lattice.PurgeBlocks(delivered))
				// TODO(jimmy-dexon): check if delivered set is a DAG.
			} else {
				other.app.StronglyAcked(b.Hash)
			}
		}
		for b := range master.app.Acked {
			if _, exist := other.app.Acked[b]; !exist {
				s.FailNowf("Block not delivered", "%s not exists", b)
			}
		}
	}
}

func (s *LatticeTestSuite) TestSanityCheck() {
	// This sanity check focuses on hash/signature part.
	var (
		chainNum = uint32(19)
		cfg      = types.Config{
			NumChains:        chainNum,
			PhiRatio:         float32(2) / float32(3),
			K:                0,
			MinBlockInterval: 0,
			MaxBlockInterval: 3000 * time.Second,
		}
		lattice = s.newTestLatticeMgr(&cfg, time.Now().UTC()).lattice
		auth    = lattice.authModule // Steal auth module from lattice, :(
		req     = s.Require()
		err     error
	)
	// A block properly signed should pass sanity check.
	b := &types.Block{
		Position:  types.Position{ChainID: 0},
		Timestamp: time.Now().UTC(),
	}
	req.NoError(auth.SignBlock(b))
	req.NoError(lattice.SanityCheck(b))
	// A block with incorrect signature should not pass sanity check.
	b.Signature, err = auth.prvKey.Sign(common.NewRandomHash())
	req.NoError(err)
	req.Equal(lattice.SanityCheck(b), ErrIncorrectSignature)
	// A block with un-sorted acks should not pass sanity check.
	b.Acks = common.NewSortedHashes(common.Hashes{
		common.NewRandomHash(),
		common.NewRandomHash(),
		common.NewRandomHash(),
		common.NewRandomHash(),
		common.NewRandomHash(),
	})
	b.Acks[0], b.Acks[1] = b.Acks[1], b.Acks[0]
	req.NoError(auth.SignBlock(b))
	req.Equal(lattice.SanityCheck(b), ErrAcksNotSorted)
	// A block with incorrect hash should not pass sanity check.
	b.Hash = common.NewRandomHash()
	req.Equal(lattice.SanityCheck(b), ErrIncorrectHash)
}

func TestLattice(t *testing.T) {
	suite.Run(t, new(LatticeTestSuite))
}