/*
Copyright 2021 IBM All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

import (
	"context"
	"testing"
	"time"

	cp "github.com/hyperledger/fabric-protos-go-apiv2/common"
	dp "github.com/hyperledger/fabric-protos-go-apiv2/discovery"
	pb "github.com/hyperledger/fabric-protos-go-apiv2/gateway"
	"github.com/hyperledger/fabric-protos-go-apiv2/gossip"
	"github.com/hyperledger/fabric-protos-go-apiv2/msp"
	ab "github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric/common/channelconfig"
	"github.com/hyperledger/fabric/common/crypto/tlsgen"
	"github.com/hyperledger/fabric/common/deliverclient/orderers"
	"github.com/hyperledger/fabric/common/ledger"
	"github.com/hyperledger/fabric/gossip/api"
	"github.com/hyperledger/fabric/gossip/common"
	gdiscovery "github.com/hyperledger/fabric/gossip/discovery"
	"github.com/hyperledger/fabric/internal/pkg/comm"
	"github.com/hyperledger/fabric/internal/pkg/gateway/commit"
	"github.com/hyperledger/fabric/internal/pkg/gateway/config"
	ledgermocks "github.com/hyperledger/fabric/internal/pkg/gateway/ledger/mocks"
	"github.com/hyperledger/fabric/internal/pkg/gateway/mocks"
	idmocks "github.com/hyperledger/fabric/internal/pkg/identity/mocks"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

// The following private interfaces are here purely to prevent counterfeiter creating an import cycle in the unit test
//
//go:generate counterfeiter -o mocks/endorserclient.go --fake-name EndorserClient . endorserClient
type endorserClient interface {
	peer.EndorserClient
}

//go:generate counterfeiter -o mocks/discovery.go --fake-name Discovery . discovery
type discovery interface {
	Discovery
}

//go:generate counterfeiter -o mocks/abclient.go --fake-name ABClient . abClient
type abClient interface {
	ab.AtomicBroadcastClient
}

//go:generate counterfeiter -o mocks/abbclient.go --fake-name ABBClient . abbClient
type abbClient interface {
	ab.AtomicBroadcast_BroadcastClient
}

//go:generate counterfeiter -o mocks/commitfinder.go --fake-name CommitFinder . commitFinder
type commitFinder interface {
	CommitFinder
}

//go:generate counterfeiter -o mocks/chaincodeeventsserver.go --fake-name ChaincodeEventsServer github.com/hyperledger/fabric-protos-go-apiv2/gateway.Gateway_ChaincodeEventsServer

//go:generate counterfeiter -o mocks/aclchecker.go --fake-name ACLChecker . aclChecker
type aclChecker interface {
	ACLChecker
}

//go:generate counterfeiter -o mocks/resultsiterator.go --fake-name ResultsIterator . mockResultsIterator
type mockResultsIterator interface {
	ledger.ResultsIterator
}

//go:generate counterfeiter -o mocks/resources.go --fake-name Resources . ccResources

type ccResources interface {
	channelconfig.Resources
}

//go:generate counterfeiter -o mocks/orderer.go --fake-name Orderer . ccOrderer

type ccOrderer interface {
	channelconfig.Orderer
}

//go:generate counterfeiter -o mocks/channel.go --fake-name Channel . ccChannel

type ccChannel interface {
	channelconfig.Channel
}

//go:generate counterfeiter -o mocks/channel_capabilities.go --fake-name ChannelCapabilities . ccChannelCapabilities

type ccChannelCapabilities interface {
	channelconfig.ChannelCapabilities
}

type (
	endorsementPlan   map[string][]endorserState
	endorsementLayout map[string]uint32
)

type networkMember struct {
	id       string
	endpoint string
	mspid    string
	height   uint64
}

type endpointDef struct {
	proposalResponseValue   string
	proposalResponseStatus  int32
	proposalResponseMessage string
	proposalError           error
	ordererResponse         string
	ordererStatus           int32
	ordererBroadcastError   error
	ordererSendError        error
	ordererRecvError        error
}

var defaultEndpointDef = &endpointDef{
	proposalResponseValue:  "mock_response",
	proposalResponseStatus: 200,
	ordererResponse:        "mock_orderer_response",
	ordererStatus:          200,
}

const (
	testChannel        = "test_channel"
	testChaincode      = "test_chaincode"
	endorsementTimeout = -1 * time.Second
	broadcastTimeout   = 100 * time.Millisecond
)

type testDef struct {
	name                     string
	plan                     endorsementPlan
	layouts                  []endorsementLayout
	members                  []networkMember
	config                   *dp.ConfigResult
	identity                 []byte
	localResponse            string
	errString                string
	errCode                  codes.Code
	errDetails               []*pb.ErrorDetail
	endpointDefinition       *endpointDef
	endorsingOrgs            []string
	postSetup                func(t *testing.T, def *preparedTest)
	postTest                 func(t *testing.T, def *preparedTest)
	expectedEndorsers        []string
	finderStatus             *commit.Status
	finderErr                error
	eventErr                 error
	policyErr                error
	expectedResponse         proto.Message
	expectedResponses        []proto.Message
	transientData            map[string][]byte
	interest                 *peer.ChaincodeInterest
	blocks                   []*cp.Block
	startPosition            *ab.SeekPosition
	afterTxID                string
	ordererEndpointOverrides map[string]*orderers.Endpoint
	isBFT                    bool
	localLedgerHeight        uint64
}

type preparedTest struct {
	server         *Server
	ctx            context.Context
	cancel         context.CancelFunc
	signedProposal *peer.SignedProposal
	localEndorser  *mocks.EndorserClient
	discovery      *mocks.Discovery
	dialer         *mocks.Dialer
	finder         *mocks.CommitFinder
	eventsServer   *mocks.ChaincodeEventsServer
	policy         *mocks.ACLChecker
	ledgerProvider *ledgermocks.Provider
	ledger         *ledgermocks.Ledger
	blockIterator  *mocks.ResultsIterator
	logLevel       string
	logFields      []string
}

type contextKey string

var (
	localhostMock    = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("id1"), address: "localhost:7051", mspid: "msp1"}} // Must have PKI ID of "id1" to match hard-coded local endorser value in tests
	peer1Mock        = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("1"), address: "peer1:8051", mspid: "msp1"}}
	peer2Mock        = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("2"), address: "peer2:9051", mspid: "msp2"}}
	peer3Mock        = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("3"), address: "peer3:10051", mspid: "msp2"}}
	peer4Mock        = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("4"), address: "peer4:11051", mspid: "msp3"}}
	unavailable1Mock = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("5"), address: "unavailable1:12051", mspid: "msp1"}}
	unavailable2Mock = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("6"), address: "unavailable2:13051", mspid: "msp1"}}
	unavailable3Mock = &endorser{endpointConfig: &endpointConfig{pkiid: []byte("7"), address: "unavailable3:14051", mspid: "msp1"}}
	endorsers        = map[string]*endorser{
		localhostMock.address: localhostMock,
		peer1Mock.address:     peer1Mock,
		peer2Mock.address:     peer2Mock,
		peer3Mock.address:     peer3Mock,
		peer4Mock.address:     peer4Mock,
	}
	orderer1Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o1"), address: "orderer1:7050", mspid: "msp1"}}
	orderer2Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o2"), address: "orderer2:7050", mspid: "msp1"}}
	orderer3Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o3"), address: "orderer3:7050", mspid: "msp1"}}
	orderer4Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o4"), address: "orderer4:7050", mspid: "msp1"}}
	orderer5Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o5"), address: "orderer5:7050", mspid: "msp1"}}
	orderer6Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o6"), address: "orderer6:7050", mspid: "msp1"}}
	orderer7Mock = &orderer{endpointConfig: &endpointConfig{pkiid: []byte("o7"), address: "orderer7:7050", mspid: "msp1"}}
	ordererMocks = map[string]*orderer{
		orderer1Mock.address: orderer1Mock,
		orderer2Mock.address: orderer2Mock,
		orderer3Mock.address: orderer3Mock,
		orderer4Mock.address: orderer4Mock,
		orderer5Mock.address: orderer5Mock,
		orderer6Mock.address: orderer6Mock,
		orderer7Mock.address: orderer7Mock,
	}
)

func channelToSlice(ch chan string) []string {
	slice := []string{}
	for {
		select {
		case value, ok := <-ch:
			if ok {
				slice = append(slice, value)
			} else {
				return slice
			}
		default:
			return slice
		}
	}
}

func TestNilArgs(t *testing.T) {
	server := newServer(
		&mocks.EndorserClient{},
		&mocks.Discovery{},
		&mocks.CommitFinder{},
		&mocks.ACLChecker{},
		&ledgermocks.Provider{},
		gdiscovery.NetworkMember{
			PKIid:    common.PKIidType("id1"),
			Endpoint: "localhost:7051",
		},
		"msp1",
		&comm.SecureOptions{},
		config.GetOptions(viper.New()),
		nil,
		nil,
		nil,
	)
	ctx := context.Background()

	_, err := server.Evaluate(ctx, nil)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "an evaluate request is required"))

	_, err = server.Evaluate(ctx, &pb.EvaluateRequest{ProposedTransaction: nil})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "failed to unpack transaction proposal: a signed proposal is required"))

	_, err = server.Evaluate(ctx, &pb.EvaluateRequest{ProposedTransaction: &peer.SignedProposal{}})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "failed to unpack transaction proposal: a signed proposal is required"))

	_, err = server.Evaluate(ctx, &pb.EvaluateRequest{ProposedTransaction: &peer.SignedProposal{ProposalBytes: []byte("jibberish")}})
	require.ErrorContains(t, err, "failed to unpack transaction proposal: error unmarshalling Proposal")

	request := &pb.EvaluateRequest{ProposedTransaction: &peer.SignedProposal{
		ProposalBytes: protoutil.MarshalOrPanic(&peer.Proposal{
			Header: protoutil.MarshalOrPanic(&cp.Header{}),
			Payload: protoutil.MarshalOrPanic(&peer.ChaincodeActionPayload{
				ChaincodeProposalPayload: protoutil.MarshalOrPanic(&peer.ChaincodeProposalPayload{
					Input: protoutil.MarshalOrPanic(&peer.ChaincodeInvocationSpec{
						ChaincodeSpec: &peer.ChaincodeSpec{
							ChaincodeId: &peer.ChaincodeID{
								Name: "testChaincode",
							},
						},
					}),
				}),
			}),
		}),
	}}
	require.True(t, len(request.GetProposedTransaction().GetProposalBytes()) != 0)
	_, err = server.Evaluate(ctx, request)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "failed to unpack transaction proposal: no channel id provided"))

	_, err = server.Evaluate(ctx, &pb.EvaluateRequest{ProposedTransaction: &peer.SignedProposal{
		ProposalBytes: protoutil.MarshalOrPanic(&peer.Proposal{
			Header: protoutil.MarshalOrPanic(&cp.Header{
				ChannelHeader: protoutil.MarshalOrPanic(&cp.ChannelHeader{
					ChannelId: "test",
				}),
			}),
			Payload: protoutil.MarshalOrPanic(&peer.ChaincodeActionPayload{
				ChaincodeProposalPayload: protoutil.MarshalOrPanic(&peer.ChaincodeProposalPayload{
					Input: nil,
				}),
			}),
		}),
	}})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "failed to unpack transaction proposal: no chaincode spec is provided, channel id [test]"))

	_, err = server.Evaluate(ctx, &pb.EvaluateRequest{ProposedTransaction: &peer.SignedProposal{
		ProposalBytes: protoutil.MarshalOrPanic(&peer.Proposal{
			Header: protoutil.MarshalOrPanic(&cp.Header{
				ChannelHeader: protoutil.MarshalOrPanic(&cp.ChannelHeader{
					ChannelId: "test",
				}),
			}),
			Payload: protoutil.MarshalOrPanic(&peer.ChaincodeActionPayload{
				ChaincodeProposalPayload: protoutil.MarshalOrPanic(&peer.ChaincodeProposalPayload{
					Input: protoutil.MarshalOrPanic(&peer.ChaincodeSpec{
						ChaincodeId: &peer.ChaincodeID{
							Name: "",
						},
					}),
				}),
			}),
		}),
	}})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "failed to unpack transaction proposal: no chaincode name is provided, channel id [test]"))

	_, err = server.Endorse(ctx, nil)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "an endorse request is required"))

	_, err = server.Endorse(ctx, &pb.EndorseRequest{ProposedTransaction: nil})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "the proposed transaction must contain a signed proposal"))

	_, err = server.Endorse(ctx, &pb.EndorseRequest{ProposedTransaction: &peer.SignedProposal{}})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "the proposed transaction must contain a signed proposal"))

	_, err = server.Endorse(ctx, &pb.EndorseRequest{ProposedTransaction: &peer.SignedProposal{ProposalBytes: []byte("jibberish")}})
	require.ErrorContains(t, err, "rpc error: code = InvalidArgument desc = error unmarshalling Proposal")

	_, err = server.Submit(ctx, nil)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a submit request is required"))

	_, err = server.Submit(ctx, &pb.SubmitRequest{})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a prepared transaction is required"))

	_, err = server.CommitStatus(ctx, nil)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a commit status request is required"))

	_, err = server.CommitStatus(ctx, &pb.SignedCommitStatusRequest{})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a commit status request is required"))

	err = server.ChaincodeEvents(nil, &mocks.ChaincodeEventsServer{})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a chaincode events request is required"))

	err = server.ChaincodeEvents(&pb.SignedChaincodeEventsRequest{}, &mocks.ChaincodeEventsServer{})
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "a chaincode events request is required"))
}

func prepareTest(t *testing.T, tt *testDef) *preparedTest {
	localEndorser := &mocks.EndorserClient{}
	localResponse := tt.localResponse
	if localResponse == "" {
		localResponse = "mock_response"
	}
	epDef := tt.endpointDefinition
	if epDef == nil {
		epDef = defaultEndpointDef
	}
	if epDef.proposalError != nil {
		localEndorser.ProcessProposalReturns(createErrorResponse(t, 500, epDef.proposalError.Error(), nil), nil)
	} else {
		localEndorser.ProcessProposalReturns(createProposalResponseWithInterest(t, localhostMock.address, localResponse, epDef.proposalResponseStatus, epDef.proposalResponseMessage, tt.interest), nil)
	}

	for _, e := range endorsers {
		e.client = &mocks.EndorserClient{}
		if epDef.proposalError != nil {
			e.client.(*mocks.EndorserClient).ProcessProposalReturns(createErrorResponse(t, 500, epDef.proposalError.Error(), nil), nil)
		} else {
			e.client.(*mocks.EndorserClient).ProcessProposalReturns(createProposalResponseWithInterest(t, e.address, epDef.proposalResponseValue, epDef.proposalResponseStatus, epDef.proposalResponseMessage, tt.interest), nil)
		}
	}

	for _, o := range ordererMocks {
		o.client = &mocks.ABClient{}
		if epDef.ordererBroadcastError != nil {
			o.client.(*mocks.ABClient).BroadcastReturns(nil, epDef.ordererBroadcastError)
			continue
		}
		abbc := &mocks.ABBClient{}
		abbc.SendReturns(epDef.ordererSendError)
		abbc.RecvReturns(&ab.BroadcastResponse{
			Info:   epDef.ordererResponse,
			Status: cp.Status(epDef.ordererStatus),
		}, epDef.ordererRecvError)
		o.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
	}

	mockSigner := &idmocks.SignerSerializer{}
	mockSigner.SignReturns([]byte("my_signature"), nil)

	mockFinder := &mocks.CommitFinder{}
	mockFinder.TransactionStatusReturns(tt.finderStatus, tt.finderErr)

	mockPolicy := &mocks.ACLChecker{}
	mockPolicy.CheckACLReturns(tt.policyErr)

	mockBlockIterator := &mocks.ResultsIterator{}
	blockChannel := make(chan *cp.Block, len(tt.blocks))
	for _, block := range tt.blocks {
		blockChannel <- block
	}
	close(blockChannel)
	mockBlockIterator.NextCalls(func() (ledger.QueryResult, error) {
		if tt.eventErr != nil {
			return nil, tt.eventErr
		}

		block := <-blockChannel
		if block == nil {
			return nil, errors.New("NO_MORE_BLOCKS")
		}

		return block, nil
	})

	mockLedger := &ledgermocks.Ledger{}
	ledgerInfo := &cp.BlockchainInfo{
		Height: tt.localLedgerHeight,
	}
	mockLedger.GetBlockchainInfoReturns(ledgerInfo, nil)
	mockLedger.GetBlocksIteratorReturns(mockBlockIterator, nil)

	mockLedgerProvider := &ledgermocks.Provider{}
	mockLedgerProvider.LedgerReturns(mockLedger, nil)

	validProposal := createProposal(t, testChannel, testChaincode, tt.transientData)
	validSignedProposal, err := protoutil.GetSignedProposal(validProposal, mockSigner)
	require.NoError(t, err)

	ca, err := tlsgen.NewCA()
	require.NoError(t, err)
	configResult := &dp.ConfigResult{
		Orderers: map[string]*dp.Endpoints{
			"msp1": {
				Endpoint: []*dp.Endpoint{
					{Host: "orderer1", Port: 7050},
				},
			},
		},
		Msps: map[string]*msp.FabricMSPConfig{
			"msp1": {
				TlsRootCerts: [][]byte{ca.CertBytes()},
			},
		},
	}

	if tt.config != nil {
		configResult = tt.config
	}

	members := []networkMember{
		{"id1", "localhost:7051", "msp1", 0},
		{"id2", "peer1:8051", "msp1", 0},
		{"id3", "peer2:9051", "msp2", 0},
		{"id4", "peer3:10051", "msp2", 0},
		{"id5", "peer4:11051", "msp3", 0},
	}

	if tt.members != nil {
		members = tt.members
	}

	disc := mockDiscovery(t, tt.plan, tt.layouts, members, configResult)

	options := config.Options{
		Enabled:            true,
		EndorsementTimeout: endorsementTimeout,
		BroadcastTimeout:   broadcastTimeout,
	}

	member := gdiscovery.NetworkMember{
		PKIid:    common.PKIidType("id1"),
		Endpoint: "localhost:7051",
	}

	getChannelConfig := func(channel string) channelconfig.Resources {
		cap := &mocks.ChannelCapabilities{}
		cap.ConsensusTypeBFTReturns(tt.isBFT)
		c := &mocks.Channel{}
		c.CapabilitiesReturns(cap)
		res := &mocks.Resources{}
		res.ChannelConfigReturns(c)
		val := &mocks.Orderer{}
		if tt.isBFT {
			val.ConsensusTypeReturns("BFT")
		} else {
			val.ConsensusTypeReturns("etcdraft")
		}
		res.OrdererConfigReturns(val, true)
		return res
	}

	server := newServer(localEndorser, disc, mockFinder, mockPolicy, mockLedgerProvider, member, "msp1", &comm.SecureOptions{}, options, nil, tt.ordererEndpointOverrides, getChannelConfig)

	dialer := &mocks.Dialer{}
	dialer.Returns(nil, nil)
	server.registry.endpointFactory = createEndpointFactory(t, epDef, dialer.Spy, tt.ordererEndpointOverrides)

	ctx := context.WithValue(context.Background(), contextKey("orange"), "apples")

	pt := &preparedTest{
		server:         server,
		ctx:            ctx,
		signedProposal: validSignedProposal,
		localEndorser:  localEndorser,
		discovery:      disc,
		dialer:         dialer,
		finder:         mockFinder,
		eventsServer:   &mocks.ChaincodeEventsServer{},
		policy:         mockPolicy,
		ledgerProvider: mockLedgerProvider,
		ledger:         mockLedger,
		blockIterator:  mockBlockIterator,
	}
	if tt.postSetup != nil {
		tt.postSetup(t, pt)
	}
	return pt
}

func checkError(t *testing.T, tt *testDef, err error) (checked bool) {
	stringCheck := tt.errString != ""
	codeCheck := tt.errCode != codes.OK
	detailsCheck := len(tt.errDetails) > 0

	checked = stringCheck || codeCheck || detailsCheck
	if !checked {
		return
	}

	require.NotNil(t, err, "error")

	if stringCheck {
		require.ErrorContains(t, err, tt.errString, "error string")
	}

	s, ok := status.FromError(err)
	if !ok {
		s = status.FromContextError(err)
	}

	if codeCheck {
		require.Equal(t, tt.errCode.String(), s.Code().String(), "error status code")
	}

	if detailsCheck {
		require.Len(t, s.Details(), len(tt.errDetails))
	external:
		for _, detail := range s.Details() {
			for _, errDetail := range tt.errDetails {
				if proto.Equal(errDetail, detail.(*pb.ErrorDetail)) {
					continue external
				}
			}
			require.Fail(t, "error match detail", "error details, expected: %v", tt.errDetails)
		}
	}

	return
}

func checkEndorsers(t *testing.T, endorsers []string, test *preparedTest) {
	// check the correct endorsers (mock) were called with the right parameters
	if endorsers == nil {
		endorsers = []string{"localhost:7051"}
	}
	for _, e := range endorsers {
		var ec *mocks.EndorserClient
		if e == test.server.registry.localEndorser.address {
			ec = test.localEndorser
		} else {
			ec = test.server.registry.remoteEndorsers[e].client.(*mocks.EndorserClient)
		}
		require.Equal(t, 1, ec.ProcessProposalCallCount(), "Expected ProcessProposal() to be invoked on %s", e)
		ectx, prop, _ := ec.ProcessProposalArgsForCall(0)
		require.Equal(t, test.signedProposal, prop)
		require.Equal(t, "apples", ectx.Value(contextKey("orange")))
		// context timeout was set to -1s, so deadline should be in the past
		deadline, ok := ectx.Deadline()
		require.True(t, ok)
		require.Negative(t, time.Until(deadline))
	}
}

func mockDiscovery(t *testing.T, plan endorsementPlan, layouts []endorsementLayout, members []networkMember, config *dp.ConfigResult) *mocks.Discovery {
	discovery := &mocks.Discovery{}

	var peers []gdiscovery.NetworkMember
	var infoset []api.PeerIdentityInfo
	for _, member := range members {
		peers = append(peers, gdiscovery.NetworkMember{
			Endpoint:   member.endpoint,
			PKIid:      []byte(member.id),
			Properties: &gossip.Properties{Chaincodes: []*gossip.Chaincode{{Name: testChaincode}}, LedgerHeight: member.height},
		})
		infoset = append(infoset, api.PeerIdentityInfo{Organization: []byte(member.mspid), PKIId: []byte(member.id)})
	}
	ed := createMockEndorsementDescriptor(t, plan, layouts)
	discovery.PeersForEndorsementReturns(ed, nil)
	discovery.PeersOfChannelReturns(peers)
	discovery.IdentityInfoReturns(infoset)
	discovery.ConfigReturns(config, nil)
	return discovery
}

func createMockEndorsementDescriptor(t *testing.T, plan endorsementPlan, layouts []endorsementLayout) *dp.EndorsementDescriptor {
	quantitiesByGroup := map[string]uint32{}
	endorsersByGroups := map[string]*dp.Peers{}
	for group, endorsers := range plan {
		quantitiesByGroup[group] = 1 // for now
		var peers []*dp.Peer
		for _, endorser := range endorsers {
			peers = append(peers, createMockPeer(t, &endorser))
		}
		endorsersByGroups[group] = &dp.Peers{Peers: peers}
	}
	var layoutDef []*dp.Layout
	if layouts != nil {
		for _, layout := range layouts {
			layoutDef = append(layoutDef, &dp.Layout{QuantitiesByGroup: layout})
		}
	} else {
		// default single layout - one from each group
		layoutDef = []*dp.Layout{{QuantitiesByGroup: quantitiesByGroup}}
	}
	descriptor := &dp.EndorsementDescriptor{
		Chaincode:         "my_channel",
		Layouts:           layoutDef,
		EndorsersByGroups: endorsersByGroups,
	}
	return descriptor
}

func createMockPeer(t *testing.T, endorser *endorserState) *dp.Peer {
	aliveMsgBytes, err := proto.Marshal(
		&gossip.GossipMessage{
			Content: &gossip.GossipMessage_AliveMsg{
				AliveMsg: &gossip.AliveMessage{
					Membership: &gossip.Member{Endpoint: endorser.endorser.address},
				},
			},
		})

	require.NoError(t, err)

	stateInfoBytes, err := proto.Marshal(
		&gossip.GossipMessage{
			Content: &gossip.GossipMessage_StateInfo{
				StateInfo: &gossip.StateInfo{
					Properties: &gossip.Properties{
						LedgerHeight: endorser.height,
					},
				},
			},
		})

	require.NoError(t, err)

	return &dp.Peer{
		StateInfo: &gossip.Envelope{
			Payload: stateInfoBytes,
		},
		MembershipInfo: &gossip.Envelope{
			Payload: aliveMsgBytes,
		},
		Identity: marshal(&msp.SerializedIdentity{
			IdBytes: []byte(endorser.endorser.address),
			Mspid:   endorser.endorser.mspid,
		}, t),
	}
}

func createEndpointFactory(t *testing.T, definition *endpointDef, dialer dialer, ordererEndpointOverrides map[string]*orderers.Endpoint) *endpointFactory {
	var endpoint string
	ca, err := tlsgen.NewCA()
	require.NoError(t, err, "failed to create CA")
	pair, err := ca.NewClientCertKeyPair()
	require.NoError(t, err, "failed to create client key pair")
	return &endpointFactory{
		timeout: 5 * time.Second,
		connectEndorser: func(conn grpc.ClientConnInterface) peer.EndorserClient {
			if ep, ok := endorsers[endpoint]; ok && ep.client != nil {
				return ep.client
			}
			return nil
		},
		connectOrderer: func(conn grpc.ClientConnInterface) ab.AtomicBroadcastClient {
			if ep, ok := ordererMocks[endpoint]; ok && ep.client != nil {
				return ep.client
			}
			return nil
		},
		dialer: func(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
			endpoint = target
			for from, to := range ordererEndpointOverrides {
				if to.Address == target {
					endpoint = from
				}
			}
			return dialer(ctx, target, opts...)
		},
		clientKey:                pair.Key,
		clientCert:               pair.Cert,
		ordererEndpointOverrides: ordererEndpointOverrides,
	}
}

func createProposal(t *testing.T, channel string, chaincode string, transient map[string][]byte, args ...[]byte) *peer.Proposal {
	invocationSpec := &peer.ChaincodeInvocationSpec{
		ChaincodeSpec: &peer.ChaincodeSpec{
			Type:        peer.ChaincodeSpec_NODE,
			ChaincodeId: &peer.ChaincodeID{Name: chaincode},
			Input:       &peer.ChaincodeInput{Args: args},
		},
	}

	proposal, _, err := protoutil.CreateChaincodeProposalWithTransient(
		cp.HeaderType_ENDORSER_TRANSACTION,
		channel,
		invocationSpec,
		[]byte{},
		transient,
	)

	require.NoError(t, err, "Failed to create the proposal")

	return proposal
}

func createProposalResponse(t *testing.T, endorser, value string, status int32, errMessage string) *peer.ProposalResponse {
	response := &peer.Response{
		Status:  status,
		Payload: []byte(value),
		Message: errMessage,
	}
	action := &peer.ChaincodeAction{
		Response: response,
	}
	payload := &peer.ProposalResponsePayload{
		ProposalHash: []byte{},
		Extension:    marshal(action, t),
	}
	endorsement := &peer.Endorsement{
		Endorser: []byte(endorser),
	}

	return &peer.ProposalResponse{
		Payload:     marshal(payload, t),
		Response:    response,
		Endorsement: endorsement,
	}
}

func createProposalResponseWithInterest(t *testing.T, endorser, value string, status int32, errMessage string, interest *peer.ChaincodeInterest) *peer.ProposalResponse {
	response := createProposalResponse(t, endorser, value, status, errMessage)
	if interest != nil {
		response.Interest = interest
	}
	return response
}

func createErrorResponse(t *testing.T, status int32, errMessage string, payload []byte) *peer.ProposalResponse {
	return &peer.ProposalResponse{
		Response: &peer.Response{
			Status:  status,
			Payload: payload,
			Message: errMessage,
		},
	}
}

func marshal(msg proto.Message, t *testing.T) []byte {
	buf, err := proto.Marshal(msg)
	require.NoError(t, err, "Failed to marshal message")
	return buf
}

func TestRpcErrorWithBadDetails(t *testing.T) {
	err := newRpcError(codes.InvalidArgument, "terrible error", nil)
	require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "terrible error"))
}
