/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package smartbft_test

import (
	"errors"
	"sync/atomic"
	"testing"

	"github.com/hyperledger-labs/SmartBFT/pkg/types"
	"github.com/hyperledger/fabric-lib-go/common/flogging"
	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/orderer/consensus/smartbft"
	"github.com/hyperledger/fabric/orderer/consensus/smartbft/mocks"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"google.golang.org/protobuf/proto"
)

func TestSigner(t *testing.T) {
	ss := &mocks.SignerSerializer{}
	s := &smartbft.Signer{
		SignerSerializer: ss,
		Logger:           flogging.MustGetLogger("test"),
		ID:               3,
	}

	t.Run("signing fails", func(t *testing.T) {
		ss.On("Sign", mock.Anything).Return(nil, errors.New("foo")).Once()
		assert.PanicsWithValue(t, "Failed signing message: foo", func() {
			s.Sign(nil)
		})
	})

	t.Run("signing succeeds", func(t *testing.T) {
		ss.On("Sign", mock.Anything).Return([]byte{1, 2, 3}, nil).Once()
		assert.Equal(t, []byte{1, 2, 3}, s.Sign(nil))
	})
}

func TestSignProposal(t *testing.T) {
	ss := &mocks.SignerSerializer{}
	ss.On("Sign", mock.Anything).Return([]byte{1, 2, 3}, nil).Once()
	ss.On("Serialize", mock.Anything).Return([]byte{0, 2, 4, 6}, nil).Once()
	ss.On("NewSignatureHeader", mock.Anything).Return(&cb.SignatureHeader{
		Creator: []byte{0, 2, 4, 6},
	}, nil)

	s := &smartbft.Signer{
		SignerSerializer: ss,
		Logger:           flogging.MustGetLogger("test"),
		ID:               3,
		LastConfigBlockNum: func(_ *cb.Block) uint64 {
			return 10
		},
	}

	lastBlock := makeNonConfigBlock(19, 10)
	lastConfigBlock := makeConfigBlock(10)
	ledger := &mocks.Ledger{}
	ledger.On("Height").Return(uint64(20))
	ledger.On("Block", uint64(19)).Return(lastBlock)
	ledger.On("Block", uint64(10)).Return(lastConfigBlock)

	logger := flogging.MustGetLogger("test")

	assembler := &smartbft.Assembler{
		VerificationSeq: func() uint64 {
			return 0
		},
		Logger:        logger,
		RuntimeConfig: &atomic.Value{},
	}

	rtc := smartbft.RuntimeConfig{
		LastBlock:       smartbft.LastBlockFromLedgerOrPanic(ledger, logger),
		LastConfigBlock: smartbft.LastConfigBlockFromLedgerOrPanic(ledger, logger),
	}

	assembler.RuntimeConfig.Store(rtc)

	env := protoutil.MarshalOrPanic(&cb.Envelope{Payload: []byte{1, 2, 3, 4, 5}})
	prop := assembler.AssembleProposal(nil, [][]byte{env})

	sig := s.SignProposal(prop, nil)
	assert.NotNil(t, sig)

	signature := smartbft.Signature{}
	signature.Unmarshal(sig.Msg)

	assert.Equal(t, s.ID, sig.ID)
	assert.Equal(t, []byte{1, 2, 3}, sig.Value)
	assert.Equal(t, prop.Header, signature.BlockHeader)
	sigHdr := &cb.SignatureHeader{}
	assert.NoError(t, proto.Unmarshal(signature.IdentifierHeader, sigHdr))
	assert.Nil(t, sigHdr.Creator)
	assert.Equal(t, signature.OrdererBlockMetadata, protoutil.MarshalOrPanic(&cb.OrdererBlockMetadata{
		LastConfig:        &cb.LastConfig{Index: 10},
		ConsenterMetadata: prop.Metadata,
	}))
}

func TestSignBadProposal(t *testing.T) {
	ss := &mocks.SignerSerializer{}
	ss.On("Sign", mock.Anything).Return([]byte{1, 2, 3}, nil).Once()
	ss.On("Serialize", mock.Anything).Return([]byte{0, 2, 4, 6}, nil)

	s := &smartbft.Signer{
		SignerSerializer: ss,
		Logger:           flogging.MustGetLogger("test"),
		ID:               3,
	}

	f := func() {
		s.SignProposal(types.Proposal{}, nil)
	}
	assert.PanicsWithValue(t, "Tried to sign bad proposal: proposal header cannot be nil", f)
}
