/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"

	"github.com/hyperledger/fabric-lib-go/bccsp"
	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/cmd/osnadmin/mocks"
	"github.com/hyperledger/fabric/common/crypto/tlsgen"
	"github.com/hyperledger/fabric/orderer/common/channelparticipation"
	"github.com/hyperledger/fabric/orderer/common/localconfig"
	"github.com/hyperledger/fabric/orderer/common/types"
	"github.com/hyperledger/fabric/protoutil"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"google.golang.org/protobuf/proto"
)

var _ = Describe("osnadmin", func() {
	var (
		tempDir               string
		ordererCACert         string
		clientCert            string
		clientKey             string
		mockChannelManagement *mocks.ChannelManagement
		testServer            *httptest.Server
		tlsConfig             *tls.Config
		ordererURL            string
		channelID             string
	)

	BeforeEach(func() {
		var err error
		tempDir, err = os.MkdirTemp("", "osnadmin")
		Expect(err).NotTo(HaveOccurred())

		generateCertificates(tempDir)

		ordererCACert = filepath.Join(tempDir, "server-ca.pem")
		clientCert = filepath.Join(tempDir, "client-cert.pem")
		clientKey = filepath.Join(tempDir, "client-key.pem")

		channelID = "testing123"

		config := localconfig.ChannelParticipation{
			Enabled:            true,
			MaxRequestBodySize: 1024 * 1024,
		}
		mockChannelManagement = &mocks.ChannelManagement{}

		h := channelparticipation.NewHTTPHandler(config, mockChannelManagement)
		Expect(h).NotTo(BeNil())
		testServer = httptest.NewUnstartedServer(h)

		cert, err := tls.LoadX509KeyPair(
			filepath.Join(tempDir, "server-cert.pem"),
			filepath.Join(tempDir, "server-key.pem"),
		)
		Expect(err).NotTo(HaveOccurred())

		caCertPool := x509.NewCertPool()
		clientCAPem, err := os.ReadFile(filepath.Join(tempDir, "client-ca.pem"))
		Expect(err).NotTo(HaveOccurred())
		caCertPool.AppendCertsFromPEM(clientCAPem)

		tlsConfig = &tls.Config{
			Certificates: []tls.Certificate{cert},
			ClientCAs:    caCertPool,
			ClientAuth:   tls.RequireAndVerifyClientCert,
		}
	})

	JustBeforeEach(func() {
		if tlsConfig != nil {
			testServer.TLS = tlsConfig
			testServer.StartTLS()
		} else {
			testServer.Start()
		}

		u, err := url.Parse(testServer.URL)
		Expect(err).NotTo(HaveOccurred())
		ordererURL = u.Host
	})

	AfterEach(func() {
		os.RemoveAll(tempDir)
		testServer.Close()
	})

	Describe("List", func() {
		BeforeEach(func() {
			mockChannelManagement.ChannelListReturns(types.ChannelList{
				Channels: []types.ChannelInfoShort{
					{
						Name: "participation-trophy",
					},
					{
						Name: "another-participation-trophy",
					},
				},
				SystemChannel: &types.ChannelInfoShort{
					Name: "fight-the-system",
				},
			})

			mockChannelManagement.ChannelInfoReturns(types.ChannelInfo{
				Name:              "asparagus",
				ConsensusRelation: "broccoli",
				Status:            "carrot",
				Height:            987,
			}, nil)
		})

		It("uses the channel participation API to list all application channels and the system channel (when it exists)", func() {
			args := []string{
				"channel",
				"list",
				"--orderer-address", ordererURL,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			expectedOutput := types.ChannelList{
				Channels: []types.ChannelInfoShort{
					{
						Name: "participation-trophy",
						URL:  "/participation/v1/channels/participation-trophy",
					},
					{
						Name: "another-participation-trophy",
						URL:  "/participation/v1/channels/another-participation-trophy",
					},
				},
				SystemChannel: &types.ChannelInfoShort{
					Name: "fight-the-system",
					URL:  "/participation/v1/channels/fight-the-system",
				},
			}
			checkStatusOutput(output, exit, err, 200, expectedOutput)
		})

		It("uses the channel participation API to list the details of a single channel", func() {
			args := []string{
				"channel",
				"list",
				"--orderer-address", ordererURL,
				"--channelID", "tell-me-your-secrets",
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			expectedOutput := types.ChannelInfo{
				Name:              "asparagus",
				URL:               "/participation/v1/channels/asparagus",
				ConsensusRelation: "broccoli",
				Status:            "carrot",
				Height:            987,
			}
			checkStatusOutput(output, exit, err, 200, expectedOutput)
		})

		Context("when the channel does not exist", func() {
			BeforeEach(func() {
				mockChannelManagement.ChannelInfoReturns(types.ChannelInfo{}, errors.New("eat-your-peas"))
			})

			It("returns 404 not found", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
					"--channelID", "tell-me-your-secrets",
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				expectedOutput := types.ErrorResponse{
					Error: "eat-your-peas",
				}
				checkStatusOutput(output, exit, err, 404, expectedOutput)
			})
		})

		Context("when TLS is disabled", func() {
			BeforeEach(func() {
				tlsConfig = nil
			})

			It("uses the channel participation API to list all channels", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
				}
				output, exit, err := executeForArgs(args)
				Expect(err).NotTo(HaveOccurred())
				Expect(exit).To(Equal(0))

				expectedOutput := types.ChannelList{
					Channels: []types.ChannelInfoShort{
						{
							Name: "participation-trophy",
							URL:  "/participation/v1/channels/participation-trophy",
						},
						{
							Name: "another-participation-trophy",
							URL:  "/participation/v1/channels/another-participation-trophy",
						},
					},
					SystemChannel: &types.ChannelInfoShort{
						Name: "fight-the-system",
						URL:  "/participation/v1/channels/fight-the-system",
					},
				}
				checkStatusOutput(output, exit, err, 200, expectedOutput)
			})

			It("uses the channel participation API to list the details of a single channel", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
					"--channelID", "tell-me-your-secrets",
				}
				output, exit, err := executeForArgs(args)
				Expect(err).NotTo(HaveOccurred())
				Expect(exit).To(Equal(0))

				expectedOutput := types.ChannelInfo{
					Name:              "asparagus",
					URL:               "/participation/v1/channels/asparagus",
					ConsensusRelation: "broccoli",
					Status:            "carrot",
					Height:            987,
				}
				checkStatusOutput(output, exit, err, 200, expectedOutput)
			})
		})
	})

	Describe("Remove", func() {
		It("uses the channel participation API to remove a channel", func() {
			args := []string{
				"channel",
				"remove",
				"--orderer-address", ordererURL,
				"--channelID", channelID,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			Expect(err).NotTo(HaveOccurred())
			Expect(exit).To(Equal(0))
			Expect(output).To(Equal("Status: 204\n"))
		})

		It("uses the channel participation API to remove a channel (without status)", func() {
			args := []string{
				"channel",
				"remove",
				"--orderer-address", ordererURL,
				"--channelID", channelID,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
				"--no-status",
			}
			output, exit, err := executeForArgs(args)
			Expect(err).NotTo(HaveOccurred())
			Expect(exit).To(Equal(0))
			Expect(output).To(BeEmpty())
		})

		Context("when the channel does not exist", func() {
			BeforeEach(func() {
				mockChannelManagement.RemoveChannelReturns(types.ErrChannelNotExist)
			})

			It("returns 404 not found", func() {
				args := []string{
					"channel",
					"remove",
					"--ca-file", ordererCACert,
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				expectedOutput := types.ErrorResponse{
					Error: "cannot remove: channel does not exist",
				}
				checkStatusOutput(output, exit, err, 404, expectedOutput)
			})
		})

		Context("when TLS is disabled", func() {
			BeforeEach(func() {
				tlsConfig = nil
			})

			It("uses the channel participation API to remove a channel", func() {
				args := []string{
					"channel",
					"remove",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
				}
				output, exit, err := executeForArgs(args)
				Expect(err).NotTo(HaveOccurred())
				Expect(exit).To(Equal(0))
				Expect(output).To(Equal("Status: 204\n"))
			})
		})
	})

	Describe("Join", func() {
		var blockPath string

		BeforeEach(func() {
			configBlock := blockWithGroups(
				map[string]*cb.ConfigGroup{
					"Application": {},
				},
				"testing123",
			)
			blockPath = createBlockFile(tempDir, configBlock)

			mockChannelManagement.JoinChannelReturns(types.ChannelInfo{
				Name:              "apple",
				ConsensusRelation: "banana",
				Status:            "orange",
				Height:            123,
			}, nil)
		})

		It("uses the channel participation API to join a channel", func() {
			args := []string{
				"channel",
				"join",
				"--orderer-address", ordererURL,
				"--channelID", channelID,
				"--config-block", blockPath,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			expectedOutput := types.ChannelInfo{
				Name:              "apple",
				URL:               "/participation/v1/channels/apple",
				ConsensusRelation: "banana",
				Status:            "orange",
				Height:            123,
			}
			checkStatusOutput(output, exit, err, 201, expectedOutput)
		})

		Context("when the block is empty", func() {
			BeforeEach(func() {
				blockPath = createBlockFile(tempDir, &cb.Block{})
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)

				checkFlagError(output, exit, err, "failed to retrieve channel id - block is empty")
			})
		})

		Context("when the --channelID does not match the channel ID in the block", func() {
			BeforeEach(func() {
				channelID = "not-the-channel-youre-looking-for"
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)

				checkFlagError(output, exit, err, "specified --channelID not-the-channel-youre-looking-for does not match channel ID testing123 in config block")
			})
		})

		Context("when the block isn't a valid config block", func() {
			BeforeEach(func() {
				block := &cb.Block{
					Data: &cb.BlockData{
						Data: [][]byte{
							protoutil.MarshalOrPanic(&cb.Envelope{
								Payload: protoutil.MarshalOrPanic(&cb.Payload{
									Header: &cb.Header{
										ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
											Type:      int32(cb.HeaderType_ENDORSER_TRANSACTION),
											ChannelId: channelID,
										}),
									},
								}),
							}),
						},
					},
				}
				blockPath = createBlockFile(tempDir, block)
			})

			It("returns 405 bad request", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				Expect(err).NotTo(HaveOccurred())
				Expect(exit).To(Equal(0))

				expectedOutput := types.ErrorResponse{
					Error: "invalid join block: block is not a config block",
				}
				checkStatusOutput(output, exit, err, 400, expectedOutput)
			})
		})

		Context("when joining the channel fails", func() {
			BeforeEach(func() {
				mockChannelManagement.JoinChannelReturns(types.ChannelInfo{}, types.ErrChannelAlreadyExists)
			})

			It("returns 405 not allowed", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				expectedOutput := types.ErrorResponse{
					Error: "cannot join: channel already exists",
				}
				checkStatusOutput(output, exit, err, 405, expectedOutput)
			})

			It("returns 405 not allowed (without status)", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
					"--no-status",
				}
				output, exit, err := executeForArgs(args)
				expectedOutput := types.ErrorResponse{
					Error: "cannot join: channel already exists",
				}
				checkOutput(output, exit, err, expectedOutput)
			})
		})

		Context("when TLS is disabled", func() {
			BeforeEach(func() {
				tlsConfig = nil
			})

			It("uses the channel participation API to join a channel", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--config-block", blockPath,
				}
				output, exit, err := executeForArgs(args)
				expectedOutput := types.ChannelInfo{
					Name:              "apple",
					URL:               "/participation/v1/channels/apple",
					ConsensusRelation: "banana",
					Status:            "orange",
					Height:            123,
				}
				checkStatusOutput(output, exit, err, 201, expectedOutput)
			})
		})
	})

	Describe("Flags", func() {
		It("accepts short versions of the --orderer-address, --channelID, and --config-block flags", func() {
			configBlock := blockWithGroups(
				map[string]*cb.ConfigGroup{
					"Application": {},
				},
				"testing123",
			)
			blockPath := createBlockFile(tempDir, configBlock)
			mockChannelManagement.JoinChannelReturns(types.ChannelInfo{
				Name:              "apple",
				ConsensusRelation: "banana",
				Status:            "orange",
				Height:            123,
			}, nil)

			args := []string{
				"channel",
				"join",
				"-o", ordererURL,
				"-c", channelID,
				"-b", blockPath,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			expectedOutput := types.ChannelInfo{
				Name:              "apple",
				URL:               "/participation/v1/channels/apple",
				ConsensusRelation: "banana",
				Status:            "orange",
				Height:            123,
			}
			checkStatusOutput(output, exit, err, 201, expectedOutput)
		})

		Context("when an unknown flag is used", func() {
			It("returns an error for long flags", func() {
				_, _, err := executeForArgs([]string{"channel", "list", "--bad-flag"})
				Expect(err).To(MatchError("unknown long flag '--bad-flag'"))
			})

			It("returns an error for short flags", func() {
				_, _, err := executeForArgs([]string{"channel", "list", "-z"})
				Expect(err).To(MatchError("unknown short flag '-z'"))
			})
		})

		Context("when the ca cert cannot be read", func() {
			BeforeEach(func() {
				ordererCACert = "not-the-ca-cert-youre-looking-for"
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				checkFlagError(output, exit, err, "reading orderer CA certificate: open not-the-ca-cert-youre-looking-for: no such file or directory")
			})
		})

		Context("when the ca-file contains a private key instead of certificate(s)", func() {
			BeforeEach(func() {
				ordererCACert = clientKey
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"remove",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				checkFlagError(output, exit, err, "failed to add ca-file PEM to cert pool")
			})
		})

		Context("when the client cert/key pair fail to load", func() {
			BeforeEach(func() {
				clientKey = "brussel-sprouts"
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				checkFlagError(output, exit, err, "loading client cert/key pair: open brussel-sprouts: no such file or directory")
			})
		})

		Context("when the config block cannot be read", func() {
			var configBlockPath string

			BeforeEach(func() {
				configBlockPath = "not-the-config-block-youre-looking-for"
			})

			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"join",
					"--orderer-address", ordererURL,
					"--channelID", channelID,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
					"--config-block", configBlockPath,
				}
				output, exit, err := executeForArgs(args)
				checkFlagError(output, exit, err, "reading config block: open not-the-config-block-youre-looking-for: no such file or directory")
			})
		})
	})

	Describe("Server using intermediate CA", func() {
		BeforeEach(func() {
			cert, err := tls.LoadX509KeyPair(
				filepath.Join(tempDir, "server-intermediate-cert.pem"),
				filepath.Join(tempDir, "server-intermediate-key.pem"),
			)
			Expect(err).NotTo(HaveOccurred())
			tlsConfig.Certificates = []tls.Certificate{cert}

			ordererCACert = filepath.Join(tempDir, "server-ca+intermediate-ca.pem")
		})

		It("uses the channel participation API to list all application and the system channel (when it exists)", func() {
			args := []string{
				"channel",
				"list",
				"--orderer-address", ordererURL,
				"--ca-file", ordererCACert,
				"--client-cert", clientCert,
				"--client-key", clientKey,
			}
			output, exit, err := executeForArgs(args)
			expectedOutput := types.ChannelList{
				Channels:      nil,
				SystemChannel: nil,
			}
			checkStatusOutput(output, exit, err, 200, expectedOutput)
		})

		Context("when the ca-file does not include the intermediate CA", func() {
			BeforeEach(func() {
				ordererCACert = filepath.Join(tempDir, "server-ca.pem")
			})
			It("returns with exit code 1 and prints the error", func() {
				args := []string{
					"channel",
					"list",
					"--orderer-address", ordererURL,
					"--ca-file", ordererCACert,
					"--client-cert", clientCert,
					"--client-key", clientKey,
				}
				output, exit, err := executeForArgs(args)
				checkCLIError(output, exit, err, fmt.Sprintf("Get \"%s/participation/v1/channels\": tls: failed to verify certificate: x509: certificate signed by unknown authority", testServer.URL))
			})
		})
	})
})

func checkStatusOutput(output string, exit int, err error, expectedStatus int, expectedOutput interface{}) {
	Expect(err).NotTo(HaveOccurred())
	Expect(exit).To(Equal(0))
	json, err := json.MarshalIndent(expectedOutput, "", "\t")
	Expect(err).NotTo(HaveOccurred())
	Expect(output).To(Equal(fmt.Sprintf("Status: %d\n%s\n", expectedStatus, string(json))))
}

func checkOutput(output string, exit int, err error, expectedOutput interface{}) {
	Expect(err).NotTo(HaveOccurred())
	Expect(exit).To(Equal(0))
	json, err := json.MarshalIndent(expectedOutput, "", "\t")
	Expect(err).NotTo(HaveOccurred())
	Expect(output).To(Equal(string(json) + "\n"))
}

func checkFlagError(output string, exit int, err error, expectedError string) {
	Expect(err).To(MatchError(ContainSubstring(expectedError)))
	Expect(exit).To(Equal(1))
	Expect(output).To(BeEmpty())
}

func checkCLIError(output string, exit int, err error, expectedError string) {
	Expect(err).NotTo(HaveOccurred())
	Expect(exit).To(Equal(1))
	Expect(output).To(Equal(fmt.Sprintf("Error: %s\n", expectedError)))
}

func generateCertificates(tempDir string) {
	serverCA, err := tlsgen.NewCA()
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-ca.pem"), serverCA.CertBytes(), 0o640)
	Expect(err).NotTo(HaveOccurred())
	serverKeyPair, err := serverCA.NewServerCertKeyPair("127.0.0.1")
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-cert.pem"), serverKeyPair.Cert, 0o640)
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-key.pem"), serverKeyPair.Key, 0o640)
	Expect(err).NotTo(HaveOccurred())

	serverIntermediateCA, err := serverCA.NewIntermediateCA()
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-intermediate-ca.pem"), serverIntermediateCA.CertBytes(), 0o640)
	Expect(err).NotTo(HaveOccurred())
	serverIntermediateKeyPair, err := serverIntermediateCA.NewServerCertKeyPair("127.0.0.1")
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-intermediate-cert.pem"), serverIntermediateKeyPair.Cert, 0o640)
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "server-intermediate-key.pem"), serverIntermediateKeyPair.Key, 0o640)
	Expect(err).NotTo(HaveOccurred())

	serverAndIntermediateCABytes := append(serverCA.CertBytes(), serverIntermediateCA.CertBytes()...)
	err = os.WriteFile(filepath.Join(tempDir, "server-ca+intermediate-ca.pem"), serverAndIntermediateCABytes, 0o640)
	Expect(err).NotTo(HaveOccurred())

	clientCA, err := tlsgen.NewCA()
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "client-ca.pem"), clientCA.CertBytes(), 0o640)
	Expect(err).NotTo(HaveOccurred())
	clientKeyPair, err := clientCA.NewClientCertKeyPair()
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "client-cert.pem"), clientKeyPair.Cert, 0o640)
	Expect(err).NotTo(HaveOccurred())
	err = os.WriteFile(filepath.Join(tempDir, "client-key.pem"), clientKeyPair.Key, 0o640)
	Expect(err).NotTo(HaveOccurred())
}

func blockWithGroups(groups map[string]*cb.ConfigGroup, channelID string) *cb.Block {
	block := protoutil.NewBlock(0, []byte{})
	block.Data = &cb.BlockData{
		Data: [][]byte{
			protoutil.MarshalOrPanic(&cb.Envelope{
				Payload: protoutil.MarshalOrPanic(&cb.Payload{
					Data: protoutil.MarshalOrPanic(&cb.ConfigEnvelope{
						Config: &cb.Config{
							ChannelGroup: &cb.ConfigGroup{
								Groups: groups,
								Values: map[string]*cb.ConfigValue{
									"HashingAlgorithm": {
										Value: protoutil.MarshalOrPanic(&cb.HashingAlgorithm{
											Name: bccsp.SHA256,
										}),
									},
									"BlockDataHashingStructure": {
										Value: protoutil.MarshalOrPanic(&cb.BlockDataHashingStructure{
											Width: math.MaxUint32,
										}),
									},
									"OrdererAddresses": {
										Value: protoutil.MarshalOrPanic(&cb.OrdererAddresses{
											Addresses: []string{"localhost"},
										}),
									},
								},
							},
						},
					}),
					Header: &cb.Header{
						ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
							Type:      int32(cb.HeaderType_CONFIG),
							ChannelId: channelID,
						}),
					},
				}),
			}),
		},
	}
	block.Header.DataHash, _ = protoutil.BlockDataHash(block.Data)
	protoutil.InitBlockMetadata(block)

	return block
}

func createBlockFile(tempDir string, configBlock *cb.Block) string {
	blockBytes, err := proto.Marshal(configBlock)
	Expect(err).NotTo(HaveOccurred())
	blockPath := filepath.Join(tempDir, "block.pb")
	err = os.WriteFile(blockPath, blockBytes, 0o644)
	Expect(err).NotTo(HaveOccurred())
	return blockPath
}
