/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package protoutil_test

import (
	"encoding/hex"
	"errors"
	"strconv"
	"testing"

	"github.com/golang/protobuf/proto"
	cb "github.com/hyperledger/fabric-protos-go/common"
	pb "github.com/hyperledger/fabric-protos-go/peer"

	"github.com/hyperledger/fabric/protoutil"
	"github.com/hyperledger/fabric/protoutil/fakes"
	"github.com/stretchr/testify/require"
)

func TestGetPayloads(t *testing.T) {
	var txAction *pb.TransactionAction
	var err error

	// good
	ccActionBytes, _ := proto.Marshal(&pb.ChaincodeAction{
		Results: []byte("results"),
	})
	proposalResponsePayload := &pb.ProposalResponsePayload{
		Extension: ccActionBytes,
	}
	proposalResponseBytes, err := proto.Marshal(proposalResponsePayload)
	require.NoError(t, err)
	ccActionPayload := &pb.ChaincodeActionPayload{
		Action: &pb.ChaincodeEndorsedAction{
			ProposalResponsePayload: proposalResponseBytes,
		},
	}
	ccActionPayloadBytes, _ := proto.Marshal(ccActionPayload)
	txAction = &pb.TransactionAction{
		Payload: ccActionPayloadBytes,
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.NoError(t, err, "Unexpected error getting payload bytes")
	t.Logf("error1 [%s]", err)

	// nil proposal response extension
	proposalResponseBytes, err = proto.Marshal(&pb.ProposalResponsePayload{
		Extension: nil,
	})
	require.NoError(t, err)
	ccActionPayloadBytes, _ = proto.Marshal(&pb.ChaincodeActionPayload{
		Action: &pb.ChaincodeEndorsedAction{
			ProposalResponsePayload: proposalResponseBytes,
		},
	})
	txAction = &pb.TransactionAction{
		Payload: ccActionPayloadBytes,
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.Error(t, err, "Expected error with nil proposal response extension")
	t.Logf("error2 [%s]", err)

	// malformed proposal response payload
	ccActionPayloadBytes, _ = proto.Marshal(&pb.ChaincodeActionPayload{
		Action: &pb.ChaincodeEndorsedAction{
			ProposalResponsePayload: []byte("bad payload"),
		},
	})
	txAction = &pb.TransactionAction{
		Payload: ccActionPayloadBytes,
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.Error(t, err, "Expected error with malformed proposal response payload")
	t.Logf("error3 [%s]", err)

	// malformed proposal response payload extension
	proposalResponseBytes, _ = proto.Marshal(&pb.ProposalResponsePayload{
		Extension: []byte("bad extension"),
	})
	ccActionPayloadBytes, _ = proto.Marshal(&pb.ChaincodeActionPayload{
		Action: &pb.ChaincodeEndorsedAction{
			ProposalResponsePayload: proposalResponseBytes,
		},
	})
	txAction = &pb.TransactionAction{
		Payload: ccActionPayloadBytes,
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.Error(t, err, "Expected error with malformed proposal response extension")
	t.Logf("error4 [%s]", err)

	// nil proposal response payload extension
	proposalResponseBytes, _ = proto.Marshal(&pb.ProposalResponsePayload{
		ProposalHash: []byte("hash"),
	})
	ccActionPayloadBytes, _ = proto.Marshal(&pb.ChaincodeActionPayload{
		Action: &pb.ChaincodeEndorsedAction{
			ProposalResponsePayload: proposalResponseBytes,
		},
	})
	txAction = &pb.TransactionAction{
		Payload: ccActionPayloadBytes,
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.Error(t, err, "Expected error with nil proposal response extension")
	t.Logf("error5 [%s]", err)

	// malformed transaction action payload
	txAction = &pb.TransactionAction{
		Payload: []byte("bad payload"),
	}
	_, _, err = protoutil.GetPayloads(txAction)
	require.Error(t, err, "Expected error with malformed transaction action payload")
	t.Logf("error6 [%s]", err)

}

func TestCreateSignedTx(t *testing.T) {
	var err error
	prop := &pb.Proposal{}

	signID := &fakes.SignerSerializer{}
	signID.SerializeReturns([]byte("signer"), nil)
	signerBytes, err := signID.Serialize()
	require.NoError(t, err, "Unexpected error serializing signing identity")

	ccHeaderExtensionBytes := protoutil.MarshalOrPanic(&pb.ChaincodeHeaderExtension{})
	chdrBytes := protoutil.MarshalOrPanic(&cb.ChannelHeader{
		Extension: ccHeaderExtensionBytes,
	})
	shdrBytes := protoutil.MarshalOrPanic(&cb.SignatureHeader{
		Creator: signerBytes,
	})
	responses := []*pb.ProposalResponse{{}}

	// malformed signature header
	headerBytes := protoutil.MarshalOrPanic(&cb.Header{
		SignatureHeader: []byte("bad signature header"),
	})
	prop.Header = headerBytes
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with malformed signature header")

	// set up the header bytes for the remaining tests
	headerBytes, _ = proto.Marshal(&cb.Header{
		ChannelHeader:   chdrBytes,
		SignatureHeader: shdrBytes,
	})
	prop.Header = headerBytes

	nonMatchingTests := []struct {
		responses     []*pb.ProposalResponse
		expectedError string
	}{
		// good responses, but different payloads
		{
			[]*pb.ProposalResponse{
				{Payload: []byte("payload"), Response: &pb.Response{Status: int32(200)}},
				{Payload: []byte("payload2"), Response: &pb.Response{Status: int32(200)}},
			},
			"ProposalResponsePayloads do not match",
		},
		// good response followed by bad response
		{
			[]*pb.ProposalResponse{
				{Payload: []byte("payload"), Response: &pb.Response{Status: int32(200)}},
				{Payload: []byte{}, Response: &pb.Response{Status: int32(500), Message: "failed to endorse"}},
			},
			"proposal response was not successful, error code 500, msg failed to endorse",
		},
		// bad response followed by good response
		{
			[]*pb.ProposalResponse{
				{Payload: []byte{}, Response: &pb.Response{Status: int32(500), Message: "failed to endorse"}},
				{Payload: []byte("payload"), Response: &pb.Response{Status: int32(200)}},
			},
			"proposal response was not successful, error code 500, msg failed to endorse",
		},
	}
	for i, nonMatchingTest := range nonMatchingTests {
		_, err = protoutil.CreateSignedTx(prop, signID, nonMatchingTest.responses...)
		require.EqualErrorf(t, err, nonMatchingTest.expectedError, "Expected non-matching response error '%v' for test %d", nonMatchingTest.expectedError, i)
	}

	// no endorsement
	responses = []*pb.ProposalResponse{{
		Payload: []byte("payload"),
		Response: &pb.Response{
			Status: int32(200),
		},
	}}
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with no endorsements")

	// success
	responses = []*pb.ProposalResponse{{
		Payload:     []byte("payload"),
		Endorsement: &pb.Endorsement{},
		Response: &pb.Response{
			Status: int32(200),
		},
	}}
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.NoError(t, err, "Unexpected error creating signed transaction")
	t.Logf("error: [%s]", err)

	//
	//
	// additional failure cases
	prop = &pb.Proposal{}
	responses = []*pb.ProposalResponse{}
	// no proposal responses
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with no proposal responses")

	// missing proposal header
	responses = append(responses, &pb.ProposalResponse{})
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with no proposal header")

	// bad proposal payload
	prop.Payload = []byte("bad payload")
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with malformed proposal payload")

	// bad payload header
	prop.Header = []byte("bad header")
	_, err = protoutil.CreateSignedTx(prop, signID, responses...)
	require.Error(t, err, "Expected error with malformed proposal header")

}

func TestCreateSignedTxStatus(t *testing.T) {
	serializedExtension, err := proto.Marshal(&pb.ChaincodeHeaderExtension{})
	require.NoError(t, err)
	serializedChannelHeader, err := proto.Marshal(&cb.ChannelHeader{
		Extension: serializedExtension,
	})
	require.NoError(t, err)

	signingID := &fakes.SignerSerializer{}
	signingID.SerializeReturns([]byte("signer"), nil)
	serializedSigningID, err := signingID.Serialize()
	require.NoError(t, err)
	serializedSignatureHeader, err := proto.Marshal(&cb.SignatureHeader{
		Creator: serializedSigningID,
	})
	require.NoError(t, err)

	header := &cb.Header{
		ChannelHeader:   serializedChannelHeader,
		SignatureHeader: serializedSignatureHeader,
	}

	serializedHeader, err := proto.Marshal(header)
	require.NoError(t, err)

	proposal := &pb.Proposal{
		Header: serializedHeader,
	}

	tests := []struct {
		status      int32
		expectedErr string
	}{
		{status: 0, expectedErr: "proposal response was not successful, error code 0, msg response-message"},
		{status: 199, expectedErr: "proposal response was not successful, error code 199, msg response-message"},
		{status: 200, expectedErr: ""},
		{status: 201, expectedErr: ""},
		{status: 399, expectedErr: ""},
		{status: 400, expectedErr: "proposal response was not successful, error code 400, msg response-message"},
	}
	for _, tc := range tests {
		t.Run(strconv.Itoa(int(tc.status)), func(t *testing.T) {
			response := &pb.ProposalResponse{
				Payload:     []byte("payload"),
				Endorsement: &pb.Endorsement{},
				Response: &pb.Response{
					Status:  tc.status,
					Message: "response-message",
				},
			}

			_, err := protoutil.CreateSignedTx(proposal, signingID, response)
			if tc.expectedErr == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, tc.expectedErr)
			}
		})
	}
}

func TestCreateSignedEnvelope(t *testing.T) {
	var env *cb.Envelope
	channelID := "mychannelID"
	msg := &cb.ConfigEnvelope{}

	id := &fakes.SignerSerializer{}
	id.SignReturnsOnCall(0, []byte("goodsig"), nil)
	id.SignReturnsOnCall(1, nil, errors.New("bad signature"))
	env, err := protoutil.CreateSignedEnvelope(cb.HeaderType_CONFIG, channelID,
		id, msg, int32(1), uint64(1))
	require.NoError(t, err, "Unexpected error creating signed envelope")
	require.NotNil(t, env, "Envelope should not be nil")
	// mock sign returns the bytes to be signed
	require.Equal(t, []byte("goodsig"), env.Signature, "Unexpected signature returned")
	payload := &cb.Payload{}
	err = proto.Unmarshal(env.Payload, payload)
	require.NoError(t, err, "Failed to unmarshal payload")
	data := &cb.ConfigEnvelope{}
	err = proto.Unmarshal(payload.Data, data)
	require.NoError(t, err, "Expected payload data to be a config envelope")
	require.Equal(t, msg, data, "Payload data does not match expected value")

	_, err = protoutil.CreateSignedEnvelope(cb.HeaderType_CONFIG, channelID,
		id, &cb.ConfigEnvelope{}, int32(1), uint64(1))
	require.Error(t, err, "Expected sign error")
}

func TestCreateSignedEnvelopeNilSigner(t *testing.T) {
	var env *cb.Envelope
	channelID := "mychannelID"
	msg := &cb.ConfigEnvelope{}

	env, err := protoutil.CreateSignedEnvelope(cb.HeaderType_CONFIG, channelID,
		nil, msg, int32(1), uint64(1))
	require.NoError(t, err, "Unexpected error creating signed envelope")
	require.NotNil(t, env, "Envelope should not be nil")
	require.Empty(t, env.Signature, "Signature should have been empty")
	payload := &cb.Payload{}
	err = proto.Unmarshal(env.Payload, payload)
	require.NoError(t, err, "Failed to unmarshal payload")
	data := &cb.ConfigEnvelope{}
	err = proto.Unmarshal(payload.Data, data)
	require.NoError(t, err, "Expected payload data to be a config envelope")
	require.Equal(t, msg, data, "Payload data does not match expected value")
}

func TestGetSignedProposal(t *testing.T) {
	var signedProp *pb.SignedProposal
	var err error

	sig := []byte("signature")

	signID := &fakes.SignerSerializer{}
	signID.SignReturns(sig, nil)

	prop := &pb.Proposal{}
	propBytes, _ := proto.Marshal(prop)
	signedProp, err = protoutil.GetSignedProposal(prop, signID)
	require.NoError(t, err, "Unexpected error getting signed proposal")
	require.Equal(t, propBytes, signedProp.ProposalBytes,
		"Proposal bytes did not match expected value")
	require.Equal(t, sig, signedProp.Signature,
		"Signature did not match expected value")

	_, err = protoutil.GetSignedProposal(nil, signID)
	require.Error(t, err, "Expected error with nil proposal")
	_, err = protoutil.GetSignedProposal(prop, nil)
	require.Error(t, err, "Expected error with nil signing identity")

}

func TestMockSignedEndorserProposalOrPanic(t *testing.T) {
	var prop *pb.Proposal
	var signedProp *pb.SignedProposal

	ccProposal := &pb.ChaincodeProposalPayload{}
	cis := &pb.ChaincodeInvocationSpec{}
	chainID := "testchannelid"
	sig := []byte("signature")
	creator := []byte("creator")
	cs := &pb.ChaincodeSpec{
		ChaincodeId: &pb.ChaincodeID{
			Name: "mychaincode",
		},
	}

	signedProp, prop = protoutil.MockSignedEndorserProposalOrPanic(chainID, cs,
		creator, sig)
	require.Equal(t, sig, signedProp.Signature,
		"Signature did not match expected result")
	propBytes, _ := proto.Marshal(prop)
	require.Equal(t, propBytes, signedProp.ProposalBytes,
		"Proposal bytes do not match expected value")
	err := proto.Unmarshal(prop.Payload, ccProposal)
	require.NoError(t, err, "Expected ChaincodeProposalPayload")
	err = proto.Unmarshal(ccProposal.Input, cis)
	require.NoError(t, err, "Expected ChaincodeInvocationSpec")
	require.Equal(t, cs.ChaincodeId.Name, cis.ChaincodeSpec.ChaincodeId.Name,
		"Chaincode name did not match expected value")
}

func TestMockSignedEndorserProposal2OrPanic(t *testing.T) {
	var prop *pb.Proposal
	var signedProp *pb.SignedProposal

	ccProposal := &pb.ChaincodeProposalPayload{}
	cis := &pb.ChaincodeInvocationSpec{}
	chainID := "testchannelid"
	sig := []byte("signature")
	signID := &fakes.SignerSerializer{}
	signID.SignReturns(sig, nil)

	signedProp, prop = protoutil.MockSignedEndorserProposal2OrPanic(chainID,
		&pb.ChaincodeSpec{}, signID)
	require.Equal(t, sig, signedProp.Signature,
		"Signature did not match expected result")
	propBytes, _ := proto.Marshal(prop)
	require.Equal(t, propBytes, signedProp.ProposalBytes,
		"Proposal bytes do not match expected value")
	err := proto.Unmarshal(prop.Payload, ccProposal)
	require.NoError(t, err, "Expected ChaincodeProposalPayload")
	err = proto.Unmarshal(ccProposal.Input, cis)
	require.NoError(t, err, "Expected ChaincodeInvocationSpec")
}

func TestGetBytesProposalPayloadForTx(t *testing.T) {
	input := &pb.ChaincodeProposalPayload{
		Input:        []byte("input"),
		TransientMap: make(map[string][]byte),
	}
	expected, _ := proto.Marshal(&pb.ChaincodeProposalPayload{
		Input: []byte("input"),
	})

	result, err := protoutil.GetBytesProposalPayloadForTx(input)
	require.NoError(t, err, "Unexpected error getting proposal payload")
	require.Equal(t, expected, result, "Payload does not match expected value")

	_, err = protoutil.GetBytesProposalPayloadForTx(nil)
	require.Error(t, err, "Expected error with nil proposal payload")
}

func TestGetProposalHash2(t *testing.T) {
	expectedHashHex := "7b622ef4e1ab9b7093ec3bbfbca17d5d6f14a437914a6839319978a7034f7960"
	expectedHash, _ := hex.DecodeString(expectedHashHex)
	hdr := &cb.Header{
		ChannelHeader:   []byte("chdr"),
		SignatureHeader: []byte("shdr"),
	}
	propHash, err := protoutil.GetProposalHash2(hdr, []byte("ccproppayload"))
	require.NoError(t, err, "Unexpected error getting hash2 for proposal")
	require.Equal(t, expectedHash, propHash, "Proposal hash did not match expected hash")

	_, err = protoutil.GetProposalHash2(&cb.Header{}, []byte("ccproppayload"))
	require.Error(t, err, "Expected error with nil arguments")
}

func TestGetProposalHash1(t *testing.T) {
	expectedHashHex := "d4c1e3cac2105da5fddc2cfe776d6ec28e4598cf1e6fa51122c7f70d8076437b"
	expectedHash, _ := hex.DecodeString(expectedHashHex)
	hdr := &cb.Header{
		ChannelHeader:   []byte("chdr"),
		SignatureHeader: []byte("shdr"),
	}

	ccProposal, _ := proto.Marshal(&pb.ChaincodeProposalPayload{})

	propHash, err := protoutil.GetProposalHash1(hdr, ccProposal)
	require.NoError(t, err, "Unexpected error getting hash for proposal")
	require.Equal(t, expectedHash, propHash, "Proposal hash did not match expected hash")

	_, err = protoutil.GetProposalHash1(hdr, []byte("ccproppayload"))
	require.Error(t, err, "Expected error with malformed chaincode proposal payload")

	_, err = protoutil.GetProposalHash1(&cb.Header{}, []byte("ccproppayload"))
	require.Error(t, err, "Expected error with nil arguments")
}

func TestCreateProposalResponseFailure(t *testing.T) {
	// create a proposal from a ChaincodeInvocationSpec
	prop, _, err := protoutil.CreateChaincodeProposal(cb.HeaderType_ENDORSER_TRANSACTION, testChannelID, createCIS(), signerSerialized)
	if err != nil {
		t.Fatalf("Could not create chaincode proposal, err %s\n", err)
		return
	}

	response := &pb.Response{Status: 502, Payload: []byte("Invalid function name")}
	result := []byte("res")

	prespFailure, err := protoutil.CreateProposalResponseFailure(prop.Header, prop.Payload, response, result, nil, "foo")
	if err != nil {
		t.Fatalf("Could not create proposal response failure, err %s\n", err)
		return
	}

	require.Equal(t, int32(502), prespFailure.Response.Status)
	// drilldown into the response to find the chaincode response
	pRespPayload, err := protoutil.UnmarshalProposalResponsePayload(prespFailure.Payload)
	require.NoError(t, err, "Error while unmarshaling proposal response payload: %s", err)
	ca, err := protoutil.UnmarshalChaincodeAction(pRespPayload.Extension)
	require.NoError(t, err, "Error while unmarshaling chaincode action: %s", err)

	require.Equal(t, int32(502), ca.Response.Status)
	require.Equal(t, "Invalid function name", string(ca.Response.Payload))
}

func TestGetorComputeTxIDFromEnvelope(t *testing.T) {
	t.Run("txID is present in the envelope", func(t *testing.T) {
		txID := "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492"
		envelopeBytes := createSampleTxEnvelopeBytes(txID)
		actualTxID, err := protoutil.GetOrComputeTxIDFromEnvelope(envelopeBytes)
		require.Nil(t, err)
		require.Equal(t, "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492", actualTxID)
	})

	t.Run("txID is not present in the envelope", func(t *testing.T) {
		txID := ""
		envelopeBytes := createSampleTxEnvelopeBytes(txID)
		actualTxID, err := protoutil.GetOrComputeTxIDFromEnvelope(envelopeBytes)
		require.Nil(t, err)
		require.Equal(t, "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492", actualTxID)

	})
}

func createSampleTxEnvelopeBytes(txID string) []byte {
	chdr := &cb.ChannelHeader{
		TxId: "709184f9d24f6ade8fcd4d6521a6eef295fef6c2e67216c58b68ac15e8946492",
	}
	chdrBytes := protoutil.MarshalOrPanic(chdr)

	shdr := &cb.SignatureHeader{
		Nonce:   []byte("nonce"),
		Creator: []byte("creator"),
	}
	shdrBytes := protoutil.MarshalOrPanic(shdr)

	hdr := &cb.Header{
		ChannelHeader:   chdrBytes,
		SignatureHeader: shdrBytes,
	}

	payload := &cb.Payload{
		Header: hdr,
	}
	payloadBytes := protoutil.MarshalOrPanic(payload)

	envelope := &cb.Envelope{
		Payload: payloadBytes,
	}
	return protoutil.MarshalOrPanic(envelope)

}
