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

import (
	"testing"
	"time"

	"github.com/dexon-foundation/dexon-consensus/common"
	"github.com/dexon-foundation/dexon-consensus/core/crypto"
	"github.com/dexon-foundation/dexon-consensus/core/crypto/dkg"
	"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"
	"github.com/stretchr/testify/suite"
)

type GovernanceTestSuite struct {
	suite.Suite
}

func (s *GovernanceTestSuite) TestEqual() {
	var req = s.Require()
	// Setup a base governance.
	_, genesisNodes, err := NewKeys(20)
	req.NoError(err)
	g1, err := NewGovernance(NewState(
		1, genesisNodes, 100*time.Millisecond, &common.NullLogger{}, true), 2)
	req.NoError(err)
	// Create a governance with different lambda.
	g2, err := NewGovernance(NewState(
		1, genesisNodes, 50*time.Millisecond, &common.NullLogger{}, true), 2)
	req.NoError(err)
	req.False(g1.Equal(g2, true))
	// Create configs for 3 rounds for g1.
	g1.CatchUpWithRound(3)
	// Make a clone.
	g3 := g1.Clone()
	req.True(g1.Equal(g3, true))
	// Create a new round for g1.
	g1.CatchUpWithRound(4)
	req.False(g1.Equal(g3, true))
	// Make another clone.
	g4 := g1.Clone()
	req.True(g1.Equal(g4, true))
	// Add a node to g4.
	_, newNodes, err := NewKeys(1)
	req.NoError(err)
	g4.State().RequestChange(StateAddNode, newNodes[0])
	g1.CatchUpWithRound(5)
	g4.CatchUpWithRound(5)
	req.False(g1.Equal(g4, true))
	// Make a clone.
	g5 := g1.Clone()
	// Change its roundShift
	g5.roundShift = 3
	req.False(g1.Equal(g5, true))
	// Prohibit some change.
	g1.Prohibit(StateAddDKGFinal)
	// Make a clone and should be equal.
	g6 := g1.Clone()
	req.True(g1.Equal(g6, true))
	g6.Unprohibit(StateAddDKGFinal)
	req.False(g1.Equal(g6, true))
}

func (s *GovernanceTestSuite) TestRegisterChange() {
	var (
		req                = s.Require()
		roundLength uint64 = 100
	)
	_, genesisNodes, err := NewKeys(20)
	req.NoError(err)
	g, err := NewGovernance(NewState(
		1, genesisNodes, 100*time.Millisecond, &common.NullLogger{}, true), 2)
	req.NoError(err)
	req.NoError(g.State().RequestChange(StateChangeRoundLength,
		uint64(roundLength)))
	// Unable to register change for genesis round.
	req.Error(g.RegisterConfigChange(0, StateChangeNotarySetSize, uint32(32)))
	// Make some round prepared.
	g.CatchUpWithRound(4)
	req.Equal(g.Configuration(4).NotarySetSize, uint32(20))
	// Unable to register change for prepared round.
	req.Error(g.RegisterConfigChange(4, StateChangeNotarySetSize, uint32(32)))
	// It's ok to make some change when condition is met.
	req.NoError(g.RegisterConfigChange(5, StateChangeNotarySetSize, uint32(32)))
	req.NoError(g.RegisterConfigChange(6, StateChangeNotarySetSize, uint32(32)))
	req.NoError(g.RegisterConfigChange(7, StateChangeNotarySetSize, uint32(40)))
	// In local mode, state for round 6 would be ready after notified with
	// round 2.
	g.NotifyRound(2, roundLength*2)
	g.NotifyRound(3, roundLength*3)
	// In local mode, state for round 7 would be ready after notified with
	// round 6.
	g.NotifyRound(4, roundLength*4)
	// Notify governance to take a snapshot for round 7's configuration.
	g.NotifyRound(5, roundLength*5)
	req.Equal(g.Configuration(6).NotarySetSize, uint32(32))
	req.Equal(g.Configuration(7).NotarySetSize, uint32(40))
}

func (s *GovernanceTestSuite) TestProhibit() {
	round := uint64(1)
	prvKeys, genesisNodes, err := NewKeys(4)
	s.Require().NoError(err)
	gov, err := NewGovernance(NewState(
		1, genesisNodes, 100*time.Millisecond, &common.NullLogger{}, true), 2)
	s.Require().NoError(err)
	// Test MPK.
	proposeMPK := func(k crypto.PrivateKey) {
		signer := utils.NewSigner(k)
		_, pubShare := dkg.NewPrivateKeyShares(utils.GetDKGThreshold(
			gov.Configuration(round)))
		mpk := &typesDKG.MasterPublicKey{
			Round:           round,
			DKGID:           typesDKG.NewID(types.NewNodeID(k.PublicKey())),
			PublicKeyShares: *pubShare,
		}
		s.Require().NoError(signer.SignDKGMasterPublicKey(mpk))
		gov.AddDKGMasterPublicKey(mpk)
	}
	proposeMPK(prvKeys[0])
	s.Require().Len(gov.DKGMasterPublicKeys(round), 1)
	gov.Prohibit(StateAddDKGMasterPublicKey)
	proposeMPK(prvKeys[1])
	s.Require().Len(gov.DKGMasterPublicKeys(round), 1)
	gov.Unprohibit(StateAddDKGMasterPublicKey)
	proposeMPK(prvKeys[1])
	s.Require().Len(gov.DKGMasterPublicKeys(round), 2)
	// Test Complaint.
	proposeComplaint := func(k crypto.PrivateKey) {
		signer := utils.NewSigner(k)
		comp := &typesDKG.Complaint{
			ProposerID: types.NewNodeID(k.PublicKey()),
			Round:      round,
		}
		s.Require().NoError(signer.SignDKGComplaint(comp))
		gov.AddDKGComplaint(comp)
	}
	proposeComplaint(prvKeys[0])
	s.Require().Len(gov.DKGComplaints(round), 1)
	gov.Prohibit(StateAddDKGComplaint)
	proposeComplaint(prvKeys[1])
	s.Require().Len(gov.DKGComplaints(round), 1)
	gov.Unprohibit(StateAddDKGComplaint)
	proposeComplaint(prvKeys[1])
	s.Require().Len(gov.DKGComplaints(round), 2)
	// Test DKG Final.
	proposeFinal := func(k crypto.PrivateKey) {
		signer := utils.NewSigner(k)
		final := &typesDKG.Finalize{
			Round:      round,
			ProposerID: types.NewNodeID(k.PublicKey()),
		}
		s.Require().NoError(signer.SignDKGFinalize(final))
		gov.AddDKGFinalize(final)
	}
	gov.Prohibit(StateAddDKGFinal)
	for _, k := range prvKeys {
		proposeFinal(k)
	}
	s.Require().False(gov.IsDKGFinal(round))
	gov.Unprohibit(StateAddDKGFinal)
	for _, k := range prvKeys {
		proposeFinal(k)
	}
	s.Require().True(gov.IsDKGFinal(round))
}

func TestGovernance(t *testing.T) {
	suite.Run(t, new(GovernanceTestSuite))
}