// 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 dex

import (
	"math/rand"
	"sort"
	"strings"
	"testing"

	coreCommon "github.com/dexon-foundation/dexon-consensus/common"
	coreDb "github.com/dexon-foundation/dexon-consensus/core/db"
	coreTypes "github.com/dexon-foundation/dexon-consensus/core/types"
)

type byHash []*coreTypes.Vote

func (v byHash) Len() int {
	return len(v)
}

func (v byHash) Less(i int, j int) bool {
	return strings.Compare(v[i].BlockHash.String(), v[j].BlockHash.String()) < 0
}

func (v byHash) Swap(i int, j int) {
	v[i], v[j] = v[j], v[i]
}

func TestCacheVote(t *testing.T) {
	db, err := coreDb.NewMemBackedDB()
	if err != nil {
		panic(err)
	}
	cache := newCache(3, db)
	pos0 := coreTypes.Position{
		Height: uint64(0),
	}
	pos1 := coreTypes.Position{
		Height: uint64(1),
	}
	vote1 := &coreTypes.Vote{
		VoteHeader: coreTypes.VoteHeader{
			BlockHash: coreCommon.NewRandomHash(),
			Position:  pos0,
		},
	}
	vote2 := &coreTypes.Vote{
		VoteHeader: coreTypes.VoteHeader{
			BlockHash: coreCommon.NewRandomHash(),
			Position:  pos0,
		},
	}
	vote3 := &coreTypes.Vote{
		VoteHeader: coreTypes.VoteHeader{
			BlockHash: coreCommon.NewRandomHash(),
			Position:  pos1,
		},
	}
	vote4 := &coreTypes.Vote{
		VoteHeader: coreTypes.VoteHeader{
			BlockHash: coreCommon.NewRandomHash(),
			Position:  pos1,
		},
	}
	cache.addVote(vote1)
	cache.addVote(vote2)
	cache.addVote(vote3)

	votes := cache.votes(pos0)
	sort.Sort(byHash(votes))

	resultVotes := []*coreTypes.Vote{vote1, vote2}
	sort.Sort(byHash(resultVotes))

	if len(votes) != 2 {
		t.Errorf("fail to get votes: have %d, want 2", len(votes))
	}
	if !votes[0].BlockHash.Equal(resultVotes[0].BlockHash) {
		t.Errorf("get wrong vote: have %s, want %s", votes[0], resultVotes[0])
	}
	if !votes[1].BlockHash.Equal(resultVotes[1].BlockHash) {
		t.Errorf("get wrong vote: have %s, want %s", votes[1], resultVotes[1])
	}
	votes = cache.votes(pos1)
	sort.Sort(byHash(votes))
	if len(votes) != 1 {
		t.Errorf("fail to get votes: have %d, want 1", len(votes))
	}
	if !votes[0].BlockHash.Equal(vote3.BlockHash) {
		t.Errorf("get wrong vote: have %s, want %s", votes[0], vote3)
	}

	cache.addVote(vote4)

	votes = cache.votes(pos0)
	sort.Sort(byHash(votes))

	if len(votes) != 0 {
		t.Errorf("fail to get votes: have %d, want 0", len(votes))
	}
	votes = cache.votes(pos1)
	sort.Sort(byHash(votes))

	resultVotes = []*coreTypes.Vote{vote3, vote4}
	sort.Sort(byHash(resultVotes))

	if len(votes) != 2 {
		t.Errorf("fail to get votes: have %d, want 1", len(votes))
	}
	if !votes[0].BlockHash.Equal(resultVotes[0].BlockHash) {
		t.Errorf("get wrong vote: have %s, want %s", votes[0], resultVotes[0])
	}
	if !votes[1].BlockHash.Equal(resultVotes[1].BlockHash) {
		t.Errorf("get wrong vote: have %s, want %s", votes[1], resultVotes[1])
	}
}

func TestCacheBlock(t *testing.T) {
	db, err := coreDb.NewMemBackedDB()
	if err != nil {
		panic(err)
	}
	cache := newCache(3, db)
	block1 := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
	}
	block2 := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
	}
	block3 := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
	}
	block4 := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
	}
	cache.addBlock(block1)
	cache.addBlock(block2)
	cache.addBlock(block3)

	hashes := coreCommon.Hashes{block1.Hash, block2.Hash, block3.Hash, block4.Hash}
	hashMap := map[coreCommon.Hash]struct{}{
		block1.Hash: {},
		block2.Hash: {},
		block3.Hash: {},
	}
	blocks := cache.blocks(hashes)
	if len(blocks) != 3 {
		t.Errorf("fail to get blocks: have %d, want 3", len(blocks))
	}
	for _, block := range blocks {
		if _, exist := hashMap[block.Hash]; !exist {
			t.Errorf("get wrong block: have %s, want %v", block, hashMap)
		}
	}

	cache.addBlock(block4)

	blocks = cache.blocks(hashes)
	hashMap[block4.Hash] = struct{}{}
	if len(blocks) != 3 {
		t.Errorf("fail to get blocks: have %d, want 3", len(blocks))
	}
	hasNewBlock := false
	for _, block := range blocks {
		if _, exist := hashMap[block.Hash]; !exist {
			t.Errorf("get wrong block: have %s, want %v", block, hashMap)
		}
		if block.Hash.Equal(block4.Hash) {
			hasNewBlock = true
		}
	}
	if !hasNewBlock {
		t.Errorf("expect block %s in cache, have %v", block4, blocks)
	}

	block5 := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
	}
	if err := db.PutBlock(*block5); err != nil {
		panic(err)
	}
	blocks = cache.blocks(coreCommon.Hashes{block5.Hash})
	if len(blocks) != 1 {
		t.Errorf("fail to get blocks: have %d, want 1", len(blocks))
	} else {
		if !blocks[0].Hash.Equal(block5.Hash) {
			t.Errorf("get wrong block: have %s, want %s", blocks[0], block5)
		}
	}
}

func randomBytes() []byte {
	bytes := make([]byte, 32)
	for i := range bytes {
		bytes[i] = byte(rand.Int() % 256)
	}
	return bytes
}

func TestCacheRandomness(t *testing.T) {
	db, err := coreDb.NewMemBackedDB()
	if err != nil {
		panic(err)
	}
	cache := newCache(3, db)
	rand1 := &coreTypes.BlockRandomnessResult{
		BlockHash:  coreCommon.NewRandomHash(),
		Randomness: randomBytes(),
	}
	rand2 := &coreTypes.BlockRandomnessResult{
		BlockHash:  coreCommon.NewRandomHash(),
		Randomness: randomBytes(),
	}
	rand3 := &coreTypes.BlockRandomnessResult{
		BlockHash:  coreCommon.NewRandomHash(),
		Randomness: randomBytes(),
	}
	rand4 := &coreTypes.BlockRandomnessResult{
		BlockHash:  coreCommon.NewRandomHash(),
		Randomness: randomBytes(),
	}
	cache.addRandomness(rand1)
	cache.addRandomness(rand2)
	cache.addRandomness(rand3)

	hashes := coreCommon.Hashes{rand1.BlockHash, rand2.BlockHash, rand3.BlockHash, rand4.BlockHash}
	hashMap := map[coreCommon.Hash]struct{}{
		rand1.BlockHash: {},
		rand2.BlockHash: {},
		rand3.BlockHash: {},
	}
	rands := cache.randomness(hashes)
	if len(rands) != 3 {
		t.Errorf("fail to get rands: have %d, want 3", len(rands))
	}
	for _, rand := range rands {
		if _, exist := hashMap[rand.BlockHash]; !exist {
			t.Errorf("get wrong rand: have %s, want %v", rand, hashMap)
		}
	}

	cache.addRandomness(rand4)

	rands = cache.randomness(hashes)
	hashMap[rand4.BlockHash] = struct{}{}
	if len(rands) != 3 {
		t.Errorf("fail to get rands: have %d, want 3", len(rands))
	}
	hasNewRandomness := false
	for _, rand := range rands {
		if _, exist := hashMap[rand.BlockHash]; !exist {
			t.Errorf("get wrong rand: have %s, want %v", rand, hashMap)
		}
		if rand.BlockHash.Equal(rand4.BlockHash) {
			hasNewRandomness = true
		}
	}
	if !hasNewRandomness {
		t.Errorf("expect rand %s in cache, have %v", rand4, rands)
	}

	block := &coreTypes.Block{
		Hash: coreCommon.NewRandomHash(),
		Finalization: coreTypes.FinalizationResult{
			Randomness: randomBytes(),
		},
	}
	if err := db.PutBlock(*block); err != nil {
		panic(err)
	}
	rands = cache.randomness(coreCommon.Hashes{block.Hash})
	if len(rands) != 1 {
		t.Errorf("fail to get rands: have %d, want 1", len(rands))
	} else {
		if !rands[0].BlockHash.Equal(block.Hash) {
			t.Errorf("get wrong rand: have %s, want %s", rands[0], block)
		}
	}
}