/*
Copyright 2021 IBM All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

import (
	"context"
	"fmt"
	"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/msp"
	ab "github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric/common/deliverclient/orderers"
	"github.com/hyperledger/fabric/internal/pkg/gateway/mocks"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

func TestSubmit(t *testing.T) {
	tests := []testDef{
		{
			name: "two endorsers",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock, height: 3}},
				"g2": {{endorser: peer1Mock, height: 3}},
			},
		},
		{
			name: "discovery fails",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.discovery.ConfigReturnsOnCall(1, nil, fmt.Errorf("jabberwocky"))
			},
			errCode:   codes.FailedPrecondition,
			errString: "failed to get config for channel [test_channel]: jabberwocky",
		},
		{
			name: "no orderers",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.discovery.ConfigReturns(&dp.ConfigResult{
					Orderers: map[string]*dp.Endpoints{},
					Msps:     map[string]*msp.FabricMSPConfig{},
				}, nil)
			},
			errCode:   codes.Unavailable,
			errString: "no orderer nodes available",
		},
		{
			name: "orderer broadcast fails",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererBroadcastError:  status.Error(codes.FailedPrecondition, "Orderer not listening!"),
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
			errDetails: []*pb.ErrorDetail{{
				Address: "orderer1:7050",
				MspId:   "msp1",
				Message: "rpc error: code = FailedPrecondition desc = Orderer not listening!",
			}},
		},
		{
			name: "send to orderer fails",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererSendError:       status.Error(codes.Internal, "Orderer says no!"),
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
			errDetails: []*pb.ErrorDetail{{
				Address: "orderer1:7050",
				MspId:   "msp1",
				Message: "rpc error: code = Internal desc = Orderer says no!",
			}},
		},
		{
			name: "receive from orderer fails",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererRecvError:       status.Error(codes.FailedPrecondition, "Orderer not happy!"),
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
			errDetails: []*pb.ErrorDetail{{
				Address: "orderer1:7050",
				MspId:   "msp1",
				Message: "rpc error: code = FailedPrecondition desc = Orderer not happy!",
			}},
		},
		{
			name: "orderer Recv() returns nil",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				abbc := &mocks.ABBClient{}
				abbc.RecvReturns(nil, nil)
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
		},
		{
			name: "orderer returns unsuccessful response",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				abbc := &mocks.ABBClient{}
				response := &ab.BroadcastResponse{
					Status: cp.Status_BAD_REQUEST,
					Info:   "err-info",
				}
				abbc.RecvReturns(response, nil)
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
			errCode:   codes.Aborted,
			errString: fmt.Sprintf("received unsuccessful response from orderer: status=%s, info=err-info", cp.Status_name[int32(cp.Status_BAD_REQUEST)]),
		},
		{
			name: "dialing orderer endpoint fails",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.dialer.Calls(func(_ context.Context, target string, _ ...grpc.DialOption) (*grpc.ClientConn, error) {
					if target == "orderer1:7050" {
						return nil, fmt.Errorf("orderer not answering")
					}
					return nil, nil
				})
			},
			errCode:   codes.Unavailable,
			errString: "no orderer nodes available",
		},
		{
			name: "multiple non-BFT orderers - only one needs to succeed",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			postTest: func(t *testing.T, def *preparedTest) {
				// only one orderer is invoked
				var invoked int
				for _, o := range ordererMocks {
					invoked += o.client.(*mocks.ABClient).BroadcastCallCount()
				}
				require.Equal(t, invoked, 1)
			},
		},
		{
			name: "orderer retry",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				abbc := &mocks.ABBClient{}
				abbc.SendReturnsOnCall(0, status.Error(codes.Unavailable, "First orderer error"))
				abbc.SendReturnsOnCall(1, status.Error(codes.Unavailable, "Second orderer error"))
				abbc.SendReturnsOnCall(2, nil) // third time lucky
				abbc.RecvReturns(&ab.BroadcastResponse{
					Info:   "success",
					Status: cp.Status(200),
				}, nil)
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer2Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer3Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
		},
		{
			name: "orderer bad response retry",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				abbc := &mocks.ABBClient{}
				abbc.SendReturns(nil)
				abbc.RecvReturnsOnCall(0, &ab.BroadcastResponse{
					Info:   "internal error",
					Status: cp.Status(500),
				}, nil)
				abbc.RecvReturnsOnCall(1, &ab.BroadcastResponse{
					Info:   "success",
					Status: cp.Status(200),
				}, nil)
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer2Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer3Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
		},
		{
			name: "orderer timeout - retry",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.ctx, def.cancel = context.WithTimeout(def.ctx, 300*time.Millisecond)
				broadcastTime := 200 * time.Millisecond // first invocation exceeds BroadcastTimeout

				abbc := &mocks.ABBClient{}
				abbc.SendReturns(nil)
				abbc.RecvReturns(&ab.BroadcastResponse{
					Info:   "success",
					Status: cp.Status(200),
				}, nil)
				bs := func(ctx context.Context, co ...grpc.CallOption) (ab.AtomicBroadcast_BroadcastClient, error) {
					defer func() {
						broadcastTime = time.Millisecond // subsequent invocations will not timeout
					}()
					select {
					case <-time.After(broadcastTime):
						return abbc, nil
					case <-ctx.Done():
						return nil, ctx.Err()
					}
				}
				orderer1Mock.client.(*mocks.ABClient).BroadcastStub = bs
				orderer2Mock.client.(*mocks.ABClient).BroadcastStub = bs
				orderer3Mock.client.(*mocks.ABClient).BroadcastStub = bs
			},
			postTest: func(t *testing.T, def *preparedTest) {
				def.cancel()
			},
		},
		{
			name: "submit timeout",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.ctx, def.cancel = context.WithTimeout(def.ctx, 50*time.Millisecond)
				broadcastTime := 200 * time.Millisecond // invocation exceeds BroadcastTimeout

				abbc := &mocks.ABBClient{}
				abbc.SendReturns(nil)
				abbc.RecvReturns(&ab.BroadcastResponse{
					Info:   "success",
					Status: cp.Status(200),
				}, nil)
				bs := func(ctx context.Context, co ...grpc.CallOption) (ab.AtomicBroadcast_BroadcastClient, error) {
					select {
					case <-time.After(broadcastTime):
						return abbc, nil
					case <-ctx.Done():
						return nil, ctx.Err()
					}
				}
				orderer1Mock.client.(*mocks.ABClient).BroadcastStub = bs
				orderer2Mock.client.(*mocks.ABClient).BroadcastStub = bs
				orderer3Mock.client.(*mocks.ABClient).BroadcastStub = bs
			},
			postTest: func(t *testing.T, def *preparedTest) {
				def.cancel()
			},
			errCode:   codes.DeadlineExceeded,
			errString: "submit timeout expired while broadcasting to ordering service",
		},
		{
			name: "multiple orderers all fail",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererBroadcastError:  status.Error(codes.Unavailable, "Orderer not listening!"),
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
			errDetails: []*pb.ErrorDetail{
				{
					Address: "orderer1:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
				{
					Address: "orderer2:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
				{
					Address: "orderer3:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
			},
		},
		{
			name: "orderer endpoint overrides",
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			ordererEndpointOverrides: map[string]*orderers.Endpoint{
				"orderer1:7050": {Address: "override1:1234"},
				"orderer3:7050": {Address: "override3:4321"},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererBroadcastError:  status.Error(codes.Unavailable, "Orderer not listening!"),
			},
			errCode:   codes.Unavailable,
			errString: "no orderers could successfully process transaction",
			errDetails: []*pb.ErrorDetail{
				{
					Address: "override1:1234 (mapped from orderer1:7050)",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
				{
					Address: "orderer2:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
				{
					Address: "override3:4321 (mapped from orderer3:7050)",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = Orderer not listening!",
				},
			},
			postTest: func(t *testing.T, def *preparedTest) {
				var addresses []string
				for i := 0; i < def.dialer.CallCount(); i++ {
					_, address, _ := def.dialer.ArgsForCall(i)
					addresses = append(addresses, address)
				}
				require.Contains(t, addresses, "override1:1234")
				require.NotContains(t, addresses, "orderer1:7050")
				require.Contains(t, addresses, "orderer2:7050")
				require.Contains(t, addresses, "override3:4321")
				require.NotContains(t, addresses, "orderer3:7050")
			},
		},
		{
			name:  "multiple BFT orderers are all invoked",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postTest: func(t *testing.T, def *preparedTest) {
				require.Eventually(t, func() bool {
					return orderer1Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer2Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer3Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer4Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1
				}, broadcastTimeout, 10*time.Millisecond)
			},
		},
		{
			name:  "multiple BFT orderers all fail",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererBroadcastError:  status.Error(codes.Unavailable, "Orderer not listening!"),
			},
			errCode:   codes.Unavailable,
			errString: "insufficient number of orderers could successfully process transaction to satisfy quorum requirement",
		},
		{
			name:  "7 BFT orderers can tolerate 2 faults",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
							{Host: "orderer5", Port: 7050},
							{Host: "orderer6", Port: 7050},
							{Host: "orderer7", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(nil, errors.New("orderer1 fails"))
				orderer4Mock.client.(*mocks.ABClient).BroadcastReturns(nil, errors.New("orderer4 fails"))
			},
		},
		{
			name:  "7 BFT orderers cannot tolerate 3 faults",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
							{Host: "orderer5", Port: 7050},
							{Host: "orderer6", Port: 7050},
							{Host: "orderer7", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				abbc := &mocks.ABBClient{}
				response := &ab.BroadcastResponse{
					Status: cp.Status_BAD_REQUEST,
					Info:   "extra details",
				}
				abbc.RecvReturns(response, nil)
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(nil, status.Error(codes.Unavailable, "orderer1 fails"))
				orderer4Mock.client.(*mocks.ABClient).BroadcastReturns(nil, status.Error(codes.Unavailable, "orderer4 fails"))
				orderer7Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
			errCode:   codes.Unavailable,
			errString: "insufficient number of orderers could successfully process transaction to satisfy quorum requirement",
			errDetails: []*pb.ErrorDetail{
				{
					Address: "orderer1:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = orderer1 fails",
				},
				{
					Address: "orderer4:7050",
					MspId:   "msp1",
					Message: "rpc error: code = Unavailable desc = orderer4 fails",
				},
				{
					Address: "orderer7:7050",
					MspId:   "msp1",
					Message: "received unsuccessful response from orderer: status=BAD_REQUEST, info=extra details",
				},
			},
		},
		{
			name:  "all BFT orderers exceed timeout",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.ctx, def.cancel = context.WithTimeout(def.ctx, 50*time.Millisecond)
				abbc := &mocks.ABBClient{}
				abbc.SendReturns(nil)
				abbc.RecvStub = func() (*ab.BroadcastResponse, error) {
					time.Sleep(100 * time.Millisecond)
					return nil, nil
				}
				orderer1Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer2Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
				orderer3Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
			postTest: func(t *testing.T, def *preparedTest) {
				def.cancel()
				require.Equal(t, 1, orderer1Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 1, orderer2Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 1, orderer3Mock.client.(*mocks.ABClient).BroadcastCallCount())
			},
			errCode:   codes.DeadlineExceeded,
			errString: "submit timeout expired while broadcasting to ordering service",
		},
		{
			name:  "one BFT orderer exceeds timeout",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.ctx, def.cancel = context.WithTimeout(def.ctx, 50*time.Millisecond)
				abbc := &mocks.ABBClient{}
				abbc.SendReturns(nil)
				abbc.RecvStub = func() (*ab.BroadcastResponse, error) {
					time.Sleep(100 * time.Millisecond)
					return nil, nil
				}
				orderer2Mock.client.(*mocks.ABClient).BroadcastReturns(abbc, nil)
			},
			postTest: func(t *testing.T, def *preparedTest) {
				def.cancel()
				require.Eventually(t, func() bool {
					return orderer1Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer2Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer3Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1 &&
						orderer4Mock.client.(*mocks.ABClient).BroadcastCallCount() == 1
				}, broadcastTimeout, 10*time.Millisecond)
			},
		},
		{
			name:  "one BFT orderer cannot connect - quorum satisfied",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.dialer.Calls(func(_ context.Context, target string, _ ...grpc.DialOption) (*grpc.ClientConn, error) {
					if target == "orderer2:7050" {
						return nil, fmt.Errorf("orderer not answering")
					}
					return nil, nil
				})
			},
			postTest: func(t *testing.T, def *preparedTest) {
				require.Equal(t, 1, orderer1Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 0, orderer2Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 1, orderer3Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 1, orderer4Mock.client.(*mocks.ABClient).BroadcastCallCount())
			},
		},
		{
			name:  "two BFT orderers cannot connect - quorum not satisfied",
			isBFT: true,
			plan: endorsementPlan{
				"g1": {{endorser: localhostMock}},
			},
			config: &dp.ConfigResult{
				Orderers: map[string]*dp.Endpoints{
					"msp1": {
						Endpoint: []*dp.Endpoint{
							{Host: "orderer1", Port: 7050},
							{Host: "orderer2", Port: 7050},
							{Host: "orderer3", Port: 7050},
							{Host: "orderer4", Port: 7050},
						},
					},
				},
				Msps: map[string]*msp.FabricMSPConfig{
					"msp1": {
						TlsRootCerts: [][]byte{},
					},
				},
			},
			endpointDefinition: &endpointDef{
				proposalResponseStatus: 200,
				ordererStatus:          200,
			},
			postSetup: func(t *testing.T, def *preparedTest) {
				def.dialer.Calls(func(_ context.Context, target string, _ ...grpc.DialOption) (*grpc.ClientConn, error) {
					if target == "orderer2:7050" || target == "orderer3:7050" {
						return nil, fmt.Errorf("orderer not answering")
					}
					return nil, nil
				})
			},
			postTest: func(t *testing.T, def *preparedTest) {
				require.Equal(t, 1, orderer1Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 0, orderer2Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 0, orderer3Mock.client.(*mocks.ABClient).BroadcastCallCount())
				require.Equal(t, 1, orderer4Mock.client.(*mocks.ABClient).BroadcastCallCount())
			},
			errCode:   codes.Unavailable,
			errString: "insufficient number of orderers could successfully process transaction to satisfy quorum requirement",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			test := prepareTest(t, &tt)

			// first call endorse to prepare the tx
			endorseResponse, err := test.server.Endorse(test.ctx, &pb.EndorseRequest{ProposedTransaction: test.signedProposal})
			require.NoError(t, err)

			preparedTx := endorseResponse.GetPreparedTransaction()

			// sign the envelope
			preparedTx.Signature = []byte("mysignature")

			// submit
			submitResponse, err := test.server.Submit(test.ctx, &pb.SubmitRequest{PreparedTransaction: preparedTx, ChannelId: testChannel})

			if checkError(t, &tt, err) {
				require.Nil(t, submitResponse, "response on error")
				if tt.postTest != nil {
					tt.postTest(t, test)
				}
				return
			}

			require.NoError(t, err)
			require.True(t, proto.Equal(&pb.SubmitResponse{}, submitResponse), "Incorrect response")

			if tt.postTest != nil {
				tt.postTest(t, test)
			}
		})
	}
}

func TestSubmitUnsigned(t *testing.T) {
	server := &Server{}
	req := &pb.SubmitRequest{
		TransactionId:       "transaction-id",
		ChannelId:           "channel-id",
		PreparedTransaction: &cp.Envelope{},
	}
	_, err := server.Submit(context.Background(), req)
	require.Error(t, err)
	require.Equal(t, err, status.Error(codes.InvalidArgument, "prepared transaction must be signed"))
}

// copied from the smartbft library...
func TestComputeBFTQuorum(t *testing.T) {
	// Ensure that quorum size is as expected.

	type quorum struct {
		N uint64
		F int
		Q int
	}

	quorums := []quorum{
		{4, 1, 3},
		{5, 1, 4},
		{6, 1, 4},
		{7, 2, 5},
		{8, 2, 6},
		{9, 2, 6},
		{10, 3, 7},
		{11, 3, 8},
		{12, 3, 8},
	}

	for _, testCase := range quorums {
		t.Run(fmt.Sprintf("%d nodes", testCase.N), func(t *testing.T) {
			Q, F := computeBFTQuorum(testCase.N)
			require.Equal(t, testCase.Q, Q)
			require.Equal(t, testCase.F, F)
		})
	}
}
