/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package smartbft_test

import (
	"sync/atomic"
	"testing"

	"github.com/hyperledger-labs/SmartBFT/pkg/types"
	"github.com/hyperledger/fabric-lib-go/common/flogging"
	"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/require"
)

var (
	configTx    = makeTx(int32(common.HeaderType_CONFIG))
	nonConfigTx = makeTx(int32(common.HeaderType_ENDORSER_TRANSACTION))
)

func TestAssembler(t *testing.T) {
	lastBlock := makeNonConfigBlock(19, 10)
	lastHash := protoutil.BlockHeaderHash(lastBlock.Header)
	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)

	for _, testCase := range []struct {
		name             string
		panicVal         string
		requests         [][]byte
		expectedProposal types.Proposal
	}{
		{
			name:     "Must contain at least one request",
			panicVal: "Programming error, no requests in proposal",
		},
		{
			name: "Must not contain an invalid request",
			panicVal: "Programming error, received bad envelope but should have" +
				" validated it: error unmarshalling Envelope: proto: cannot parse invalid wire-format data",
			requests: [][]byte{{1, 2, 3}},
		},
		{
			name:             "Config transaction is first in the batch",
			requests:         [][]byte{configTx, nonConfigTx},
			expectedProposal: proposalFromRequests(10, 20, 20, lastHash, []byte("metadata"), configTx),
		},
		{
			name:             "Config transaction is in the middle of the batch",
			requests:         [][]byte{nonConfigTx, configTx, nonConfigTx},
			expectedProposal: proposalFromRequests(10, 20, 10, lastHash, []byte("metadata"), nonConfigTx),
		},
		{
			name:             "Config transaction is at the end of the batch",
			requests:         [][]byte{nonConfigTx, nonConfigTx, configTx},
			expectedProposal: proposalFromRequests(10, 20, 10, lastHash, []byte("metadata"), nonConfigTx, nonConfigTx),
		},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			logger := flogging.MustGetLogger("test")

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

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

			if testCase.panicVal != "" {

				require.Panics(t, func() {
					assembler.AssembleProposal([]byte{1, 2, 3}, testCase.requests)
				})
				return
			}

			prop := assembler.AssembleProposal([]byte("metadata"), testCase.requests)
			require.Equal(t, testCase.expectedProposal, prop)
		})
	}
}

func makeTx(headerType int32) []byte {
	return protoutil.MarshalOrPanic(&common.Envelope{
		Signature: []byte{1, 2, 3},
		Payload: protoutil.MarshalOrPanic(&common.Payload{
			Header: &common.Header{
				ChannelHeader: protoutil.MarshalOrPanic(&common.ChannelHeader{
					Type:      headerType,
					ChannelId: "test-chain",
				}),
			},
		}),
	})
}

func makeNonConfigBlock(seq, lastConfigSeq uint64) *common.Block {
	return &common.Block{
		Header: &common.BlockHeader{
			Number: seq,
		},
		Data: &common.BlockData{
			Data: [][]byte{nonConfigTx},
		},
		Metadata: &common.BlockMetadata{
			Metadata: [][]byte{
				{},
				protoutil.MarshalOrPanic(&common.Metadata{
					Value: protoutil.MarshalOrPanic(&common.LastConfig{Index: lastConfigSeq}),
				}),
			},
		},
	}
}

func makeConfigBlock(seq uint64) *common.Block {
	return &common.Block{
		Header: &common.BlockHeader{
			Number: seq,
		},
		Data: &common.BlockData{
			Data: [][]byte{protoutil.MarshalOrPanic(&common.Envelope{
				Signature: []byte{1, 2, 3},
				Payload: protoutil.MarshalOrPanic(&common.Payload{
					Data: protoutil.MarshalOrPanic(&common.ConfigEnvelope{
						Config: &common.Config{
							Sequence: 10,
						},
					}),
				}),
			})},
		},
		Metadata: &common.BlockMetadata{
			Metadata: [][]byte{
				{},
				protoutil.MarshalOrPanic(&common.Metadata{
					Value: protoutil.MarshalOrPanic(&common.LastConfig{Index: 666}),
				}),
			},
		},
	}
}

func proposalFromRequests(verificationSeq, seq, lastConfigSeq uint64, lastBlockHash, metadata []byte, requests ...[]byte) types.Proposal {
	block := protoutil.NewBlock(seq, nil)
	block.Data = &common.BlockData{Data: requests}
	block.Header.DataHash = protoutil.ComputeBlockDataHash(block.Data)
	block.Header.PreviousHash = lastBlockHash
	block.Metadata.Metadata[common.BlockMetadataIndex_LAST_CONFIG] = protoutil.MarshalOrPanic(&common.Metadata{
		Value: protoutil.MarshalOrPanic(&common.LastConfig{Index: lastConfigSeq}),
	})

	block.Metadata.Metadata[common.BlockMetadataIndex_SIGNATURES] = protoutil.MarshalOrPanic(&common.Metadata{
		Value: protoutil.MarshalOrPanic(&common.OrdererBlockMetadata{
			ConsenterMetadata: metadata,
			LastConfig: &common.LastConfig{
				Index: lastConfigSeq,
			},
		}),
	})

	tuple := &smartbft.ByteBufferTuple{
		A: protoutil.MarshalOrPanic(block.Data),
		B: protoutil.MarshalOrPanic(block.Metadata),
	}

	return types.Proposal{
		Header:               protoutil.BlockHeaderBytes(block.Header),
		Payload:              tuple.ToBytes(),
		Metadata:             metadata,
		VerificationSequence: int64(verificationSeq),
	}
}
