/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package nwo

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"strconv"
	"strings"

	"github.com/golang/protobuf/proto"
	"github.com/hyperledger/fabric-protos-go/common"
	"github.com/hyperledger/fabric-protos-go/peer/lifecycle"
	"github.com/hyperledger/fabric/common/util"
	"github.com/hyperledger/fabric/integration/nwo/commands"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
	. "github.com/onsi/gomega/gstruct"
)

type Chaincode struct {
	Name                string
	Version             string
	Path                string
	Ctor                string
	Policy              string // only used for legacy lifecycle. For new lifecycle use SignaturePolicy
	Lang                string
	CollectionsConfig   string // optional
	PackageFile         string
	PackageID           string            // if unspecified, chaincode won't be executable. Can use SetPackageIDFromPackageFile() to set.
	CodeFiles           map[string]string // map from paths on the filesystem to code.tar.gz paths
	Sequence            string
	EndorsementPlugin   string
	ValidationPlugin    string
	InitRequired        bool
	Label               string
	SignaturePolicy     string
	ChannelConfigPolicy string
}

func (c *Chaincode) SetPackageIDFromPackageFile() {
	fileBytes, err := ioutil.ReadFile(c.PackageFile)
	Expect(err).NotTo(HaveOccurred())
	hashStr := fmt.Sprintf("%x", util.ComputeSHA256(fileBytes))
	c.PackageID = c.Label + ":" + hashStr
}

// DeployChaincode is a helper that will install chaincode to all peers that
// are connected to the specified channel, approve the chaincode on one of the
// peers of each organization in the network, commit the chaincode definition
// on the channel using one of the peers, and wait for the chaincode commit to
// complete on all of the peers. It uses the _lifecycle implementation.
// NOTE V2_0 capabilities must be enabled for this functionality to work.
func DeployChaincode(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peers ...*Peer) {
	if len(peers) == 0 {
		peers = n.PeersWithChannel(channel)
	}
	if len(peers) == 0 {
		return
	}

	PackageAndInstallChaincode(n, chaincode, peers...)

	// approve for each org
	ApproveChaincodeForMyOrg(n, channel, orderer, chaincode, peers...)

	// commit definition
	CheckCommitReadinessUntilReady(n, channel, chaincode, n.PeerOrgs(), peers...)
	CommitChaincode(n, channel, orderer, chaincode, peers[0], peers...)

	// init the chaincode, if required
	if chaincode.InitRequired {
		InitChaincode(n, channel, orderer, chaincode, peers...)
	}
}

// DeployChaincodeLegacy is a helper that will install chaincode to all peers
// that are connected to the specified channel, instantiate the chaincode on
// one of the peers, and wait for the instantiation to complete on all of the
// peers. It uses the legacy lifecycle (lscc) implementation.
//
// NOTE: This helper should not be used to deploy the same chaincode on
// multiple channels as the install will fail on subsequent calls. Instead,
// simply use InstantiateChaincode().
func DeployChaincodeLegacy(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peers ...*Peer) {
	if len(peers) == 0 {
		peers = n.PeersWithChannel(channel)
	}
	if len(peers) == 0 {
		return
	}

	// create temp file for chaincode package if not provided
	if chaincode.PackageFile == "" {
		tempFile, err := ioutil.TempFile("", "chaincode-package")
		Expect(err).NotTo(HaveOccurred())
		tempFile.Close()
		defer os.Remove(tempFile.Name())
		chaincode.PackageFile = tempFile.Name()
	}

	// only create chaincode package if it doesn't already exist
	if fi, err := os.Stat(chaincode.PackageFile); os.IsNotExist(err) || fi.Size() == 0 {
		PackageChaincodeLegacy(n, chaincode, peers[0])
	}

	// install on all peers
	InstallChaincodeLegacy(n, chaincode, peers...)

	// instantiate on the first peer
	InstantiateChaincodeLegacy(n, channel, orderer, chaincode, peers[0], peers...)
}

func PackageAndInstallChaincode(n *Network, chaincode Chaincode, peers ...*Peer) {
	// create temp file for chaincode package if not provided
	if chaincode.PackageFile == "" {
		tempFile, err := ioutil.TempFile("", "chaincode-package")
		Expect(err).NotTo(HaveOccurred())
		tempFile.Close()
		defer os.Remove(tempFile.Name())
		chaincode.PackageFile = tempFile.Name()
	}

	// only create chaincode package if it doesn't already exist
	if _, err := os.Stat(chaincode.PackageFile); os.IsNotExist(err) {
		switch chaincode.Lang {
		case "binary":
			PackageChaincodeBinary(chaincode)
		default:
			PackageChaincode(n, chaincode, peers[0])
		}
	}

	// install on all peers
	InstallChaincode(n, chaincode, peers...)
}

func PackageChaincode(n *Network, chaincode Chaincode, peer *Peer) {
	sess, err := n.PeerAdminSession(peer, commands.ChaincodePackage{
		Path:       chaincode.Path,
		Lang:       chaincode.Lang,
		Label:      chaincode.Label,
		OutputFile: chaincode.PackageFile,
		ClientAuth: n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
}

func PackageChaincodeLegacy(n *Network, chaincode Chaincode, peer *Peer) {
	sess, err := n.PeerAdminSession(peer, commands.ChaincodePackageLegacy{
		Name:       chaincode.Name,
		Version:    chaincode.Version,
		Path:       chaincode.Path,
		Lang:       chaincode.Lang,
		OutputFile: chaincode.PackageFile,
		ClientAuth: n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
}

func InstallChaincode(n *Network, chaincode Chaincode, peers ...*Peer) {
	// Ensure 'jq' exists in path, because we need it to build chaincode
	if _, err := exec.LookPath("jq"); err != nil {
		ginkgo.Fail("'jq' is needed to build chaincode but it wasn't found in the PATH")
	}

	if chaincode.PackageID == "" {
		chaincode.SetPackageIDFromPackageFile()
	}

	for _, p := range peers {
		sess, err := n.PeerAdminSession(p, commands.ChaincodeInstall{
			PackageFile: chaincode.PackageFile,
			ClientAuth:  n.ClientAuthRequired,
		})
		ExpectWithOffset(1, err).NotTo(HaveOccurred())
		EventuallyWithOffset(1, sess, n.EventuallyTimeout).Should(gexec.Exit())

		EnsureInstalled(n, chaincode.Label, chaincode.PackageID, p)
	}
}

func InstallChaincodeLegacy(n *Network, chaincode Chaincode, peers ...*Peer) {
	// Ensure 'jq' exists in path, because we need it to build chaincode
	if _, err := exec.LookPath("jq"); err != nil {
		ginkgo.Fail("'jq' is needed to build chaincode but it wasn't found in the PATH")
	}

	for _, p := range peers {
		sess, err := n.PeerAdminSession(p, commands.ChaincodeInstallLegacy{
			Name:        chaincode.Name,
			Version:     chaincode.Version,
			Path:        chaincode.Path,
			Lang:        chaincode.Lang,
			PackageFile: chaincode.PackageFile,
			ClientAuth:  n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		EventuallyWithOffset(1, sess, n.EventuallyTimeout).Should(gexec.Exit(0))

		sess, err = n.PeerAdminSession(p, commands.ChaincodeListInstalledLegacy{
			ClientAuth: n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		EventuallyWithOffset(1, sess, n.EventuallyTimeout).Should(gexec.Exit(0))
		ExpectWithOffset(1, sess).To(gbytes.Say(fmt.Sprintf("Name: %s, Version: %s,", chaincode.Name, chaincode.Version)))
	}
}

func ApproveChaincodeForMyOrg(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peers ...*Peer) {
	if chaincode.PackageID == "" {
		chaincode.SetPackageIDFromPackageFile()
	}

	// used to ensure we only approve once per org
	approvedOrgs := map[string]bool{}
	for _, p := range peers {
		if _, ok := approvedOrgs[p.Organization]; !ok {
			sess, err := n.PeerAdminSession(p, commands.ChaincodeApproveForMyOrg{
				ChannelID:           channel,
				Orderer:             n.OrdererAddress(orderer, ListenPort),
				Name:                chaincode.Name,
				Version:             chaincode.Version,
				PackageID:           chaincode.PackageID,
				Sequence:            chaincode.Sequence,
				EndorsementPlugin:   chaincode.EndorsementPlugin,
				ValidationPlugin:    chaincode.ValidationPlugin,
				SignaturePolicy:     chaincode.SignaturePolicy,
				ChannelConfigPolicy: chaincode.ChannelConfigPolicy,
				InitRequired:        chaincode.InitRequired,
				CollectionsConfig:   chaincode.CollectionsConfig,
				ClientAuth:          n.ClientAuthRequired,
			})
			Expect(err).NotTo(HaveOccurred())
			Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
			approvedOrgs[p.Organization] = true
			Eventually(sess.Err, n.EventuallyTimeout).Should(gbytes.Say(`\Qcommitted with status (VALID)\E`))
		}
	}
}

func EnsureChaincodeApproved(n *Network, peer *Peer, channel, name, sequence string) {
	sequenceInt, err := strconv.ParseInt(sequence, 10, 64)
	Expect(err).NotTo(HaveOccurred())
	Eventually(queryApproved(n, peer, channel, name, sequence), n.EventuallyTimeout).Should(
		MatchFields(IgnoreExtras, Fields{
			"Sequence": Equal(sequenceInt),
		}),
	)
}

func CheckCommitReadinessUntilReady(n *Network, channel string, chaincode Chaincode, checkOrgs []*Organization, peers ...*Peer) {
	for _, p := range peers {
		keys := Keys{}
		for _, org := range checkOrgs {
			keys[org.MSPID] = BeTrue()
		}
		Eventually(checkCommitReadiness(n, p, channel, chaincode), n.EventuallyTimeout).Should(MatchKeys(IgnoreExtras, keys))
	}
}

func CommitChaincode(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peer *Peer, checkPeers ...*Peer) {
	// commit using one peer per org
	commitOrgs := map[string]bool{}
	var peerAddresses []string
	for _, p := range checkPeers {
		if exists := commitOrgs[p.Organization]; !exists {
			peerAddresses = append(peerAddresses, n.PeerAddress(p, ListenPort))
			commitOrgs[p.Organization] = true
		}
	}

	sess, err := n.PeerAdminSession(peer, commands.ChaincodeCommit{
		ChannelID:           channel,
		Orderer:             n.OrdererAddress(orderer, ListenPort),
		Name:                chaincode.Name,
		Version:             chaincode.Version,
		Sequence:            chaincode.Sequence,
		EndorsementPlugin:   chaincode.EndorsementPlugin,
		ValidationPlugin:    chaincode.ValidationPlugin,
		SignaturePolicy:     chaincode.SignaturePolicy,
		ChannelConfigPolicy: chaincode.ChannelConfigPolicy,
		InitRequired:        chaincode.InitRequired,
		CollectionsConfig:   chaincode.CollectionsConfig,
		PeerAddresses:       peerAddresses,
		ClientAuth:          n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	for i := 0; i < len(peerAddresses); i++ {
		Eventually(sess.Err, n.EventuallyTimeout).Should(gbytes.Say(`\Qcommitted with status (VALID)\E`))
	}
	checkOrgs := []*Organization{}
	for org := range commitOrgs {
		checkOrgs = append(checkOrgs, n.Organization(org))
	}
	EnsureChaincodeCommitted(n, channel, chaincode.Name, chaincode.Version, chaincode.Sequence, checkOrgs, checkPeers...)
}

// EnsureChaincodeCommitted polls each supplied peer until the chaincode definition
// has been committed to the peer's ledger.
func EnsureChaincodeCommitted(n *Network, channel, name, version, sequence string, checkOrgs []*Organization, peers ...*Peer) {
	for _, p := range peers {
		sequenceInt, err := strconv.ParseInt(sequence, 10, 64)
		Expect(err).NotTo(HaveOccurred())
		approvedKeys := Keys{}
		for _, org := range checkOrgs {
			approvedKeys[org.MSPID] = BeTrue()
		}
		Eventually(listCommitted(n, p, channel, name), n.EventuallyTimeout).Should(
			MatchFields(IgnoreExtras, Fields{
				"Version":   Equal(version),
				"Sequence":  Equal(sequenceInt),
				"Approvals": MatchKeys(IgnoreExtras, approvedKeys),
			}),
		)
	}
}

func InitChaincode(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peers ...*Peer) {
	// init using one peer per org
	initOrgs := map[string]bool{}
	var peerAddresses []string
	for _, p := range peers {
		if exists := initOrgs[p.Organization]; !exists {
			peerAddresses = append(peerAddresses, n.PeerAddress(p, ListenPort))
			initOrgs[p.Organization] = true
		}
	}

	sess, err := n.PeerUserSession(peers[0], "User1", commands.ChaincodeInvoke{
		ChannelID:     channel,
		Orderer:       n.OrdererAddress(orderer, ListenPort),
		Name:          chaincode.Name,
		Ctor:          chaincode.Ctor,
		PeerAddresses: peerAddresses,
		WaitForEvent:  true,
		IsInit:        true,
		ClientAuth:    n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	for i := 0; i < len(peerAddresses); i++ {
		Eventually(sess.Err, n.EventuallyTimeout).Should(gbytes.Say(`\Qcommitted with status (VALID)\E`))
	}
	Expect(sess.Err).To(gbytes.Say("Chaincode invoke successful. result: status:200"))
}

func InstantiateChaincodeLegacy(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peer *Peer, checkPeers ...*Peer) {
	sess, err := n.PeerAdminSession(peer, commands.ChaincodeInstantiateLegacy{
		ChannelID:         channel,
		Orderer:           n.OrdererAddress(orderer, ListenPort),
		Name:              chaincode.Name,
		Version:           chaincode.Version,
		Ctor:              chaincode.Ctor,
		Policy:            chaincode.Policy,
		Lang:              chaincode.Lang,
		CollectionsConfig: chaincode.CollectionsConfig,
		ClientAuth:        n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	EnsureInstantiatedLegacy(n, channel, chaincode.Name, chaincode.Version, checkPeers...)
}

func EnsureInstantiatedLegacy(n *Network, channel, name, version string, peers ...*Peer) {
	for _, p := range peers {
		Eventually(listInstantiatedLegacy(n, p, channel), n.EventuallyTimeout).Should(
			gbytes.Say(fmt.Sprintf("Name: %s, Version: %s,", name, version)),
		)
	}
}

func UpgradeChaincodeLegacy(n *Network, channel string, orderer *Orderer, chaincode Chaincode, peers ...*Peer) {
	if len(peers) == 0 {
		peers = n.PeersWithChannel(channel)
	}
	if len(peers) == 0 {
		return
	}

	// install on all peers
	InstallChaincodeLegacy(n, chaincode, peers...)

	// upgrade from the first peer
	sess, err := n.PeerAdminSession(peers[0], commands.ChaincodeUpgradeLegacy{
		ChannelID:         channel,
		Orderer:           n.OrdererAddress(orderer, ListenPort),
		Name:              chaincode.Name,
		Version:           chaincode.Version,
		Ctor:              chaincode.Ctor,
		Policy:            chaincode.Policy,
		CollectionsConfig: chaincode.CollectionsConfig,
		ClientAuth:        n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	EnsureInstantiatedLegacy(n, channel, chaincode.Name, chaincode.Version, peers...)
}

func EnsureInstalled(n *Network, label, packageID string, peers ...*Peer) {
	for _, p := range peers {
		Eventually(QueryInstalled(n, p), n.EventuallyTimeout).Should(
			ContainElement(MatchFields(IgnoreExtras,
				Fields{
					"Label":     Equal(label),
					"PackageId": Equal(packageID),
				},
			)),
		)
	}
}

func QueryInstalledReferences(n *Network, channel, label, packageID string, checkPeer *Peer, nameVersions ...[]string) {
	chaincodes := make([]*lifecycle.QueryInstalledChaincodesResult_Chaincode, len(nameVersions))
	for i, nameVersion := range nameVersions {
		chaincodes[i] = &lifecycle.QueryInstalledChaincodesResult_Chaincode{
			Name:    nameVersion[0],
			Version: nameVersion[1],
		}
	}

	Expect(QueryInstalled(n, checkPeer)()).To(
		ContainElement(MatchFields(IgnoreExtras,
			Fields{
				"Label":     Equal(label),
				"PackageId": Equal(packageID),
				"References": HaveKeyWithValue(channel, PointTo(MatchFields(IgnoreExtras,
					Fields{
						"Chaincodes": ConsistOf(chaincodes),
					},
				))),
			},
		)),
	)
}

type queryInstalledOutput struct {
	InstalledChaincodes []lifecycle.QueryInstalledChaincodesResult_InstalledChaincode `json:"installed_chaincodes"`
}

func QueryInstalled(n *Network, peer *Peer) func() []lifecycle.QueryInstalledChaincodesResult_InstalledChaincode {
	return func() []lifecycle.QueryInstalledChaincodesResult_InstalledChaincode {
		sess, err := n.PeerAdminSession(peer, commands.ChaincodeQueryInstalled{
			ClientAuth: n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
		output := &queryInstalledOutput{}
		err = json.Unmarshal(sess.Out.Contents(), output)
		Expect(err).NotTo(HaveOccurred())
		return output.InstalledChaincodes
	}
}

type checkCommitReadinessOutput struct {
	Approvals map[string]bool `json:"approvals"`
}

func checkCommitReadiness(n *Network, peer *Peer, channel string, chaincode Chaincode) func() map[string]bool {
	return func() map[string]bool {
		sess, err := n.PeerAdminSession(peer, commands.ChaincodeCheckCommitReadiness{
			ChannelID:           channel,
			Name:                chaincode.Name,
			Version:             chaincode.Version,
			Sequence:            chaincode.Sequence,
			EndorsementPlugin:   chaincode.EndorsementPlugin,
			ValidationPlugin:    chaincode.ValidationPlugin,
			SignaturePolicy:     chaincode.SignaturePolicy,
			ChannelConfigPolicy: chaincode.ChannelConfigPolicy,
			InitRequired:        chaincode.InitRequired,
			CollectionsConfig:   chaincode.CollectionsConfig,
			ClientAuth:          n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
		output := &checkCommitReadinessOutput{}
		err = json.Unmarshal(sess.Out.Contents(), output)
		Expect(err).NotTo(HaveOccurred())
		return output.Approvals
	}
}

type queryApprovedOutput struct {
	Sequence int64 `json:"sequence"`
}

// queryApproved returns the result of the queryApproved command.
// If the command fails for any reason, it will return an empty output object.
func queryApproved(n *Network, peer *Peer, channel, name, sequence string) func() queryApprovedOutput {
	return func() queryApprovedOutput {
		sess, err := n.PeerAdminSession(peer, commands.ChaincodeQueryApproved{
			ChannelID:     channel,
			Name:          name,
			Sequence:      sequence,
			PeerAddresses: []string{n.PeerAddress(peer, ListenPort)},
			ClientAuth:    n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit())
		output := &queryApprovedOutput{}
		if sess.ExitCode() == 1 {
			// don't try to unmarshal the output as JSON if the query failed
			return *output
		}
		err = json.Unmarshal(sess.Out.Contents(), output)
		Expect(err).NotTo(HaveOccurred())
		return *output
	}
}

type queryCommittedOutput struct {
	Sequence  int64           `json:"sequence"`
	Version   string          `json:"version"`
	Approvals map[string]bool `json:"approvals"`
}

// listCommitted returns the result of the queryCommitted command.
// If the command fails for any reason (e.g. namespace not defined
// or a database access issue), it will return an empty output object.
func listCommitted(n *Network, peer *Peer, channel, name string) func() queryCommittedOutput {
	return func() queryCommittedOutput {
		sess, err := n.PeerAdminSession(peer, commands.ChaincodeListCommitted{
			ChannelID:  channel,
			Name:       name,
			ClientAuth: n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit())
		output := &queryCommittedOutput{}
		if sess.ExitCode() == 1 {
			// don't try to unmarshal the output as JSON if the query failed
			return *output
		}
		err = json.Unmarshal(sess.Out.Contents(), output)
		Expect(err).NotTo(HaveOccurred())
		return *output
	}
}

func listInstantiatedLegacy(n *Network, peer *Peer, channel string) func() *gbytes.Buffer {
	return func() *gbytes.Buffer {
		sess, err := n.PeerAdminSession(peer, commands.ChaincodeListInstantiatedLegacy{
			ChannelID:  channel,
			ClientAuth: n.ClientAuthRequired,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
		return sess.Buffer()
	}
}

// EnableCapabilities enables a specific capabilities flag for a running network.
// It generates the config update using the first peer, signs the configuration
// with the subsequent peers, and then submits the config update using the
// first peer.
func EnableCapabilities(network *Network, channel, capabilitiesGroup, capabilitiesVersion string, orderer *Orderer, peers ...*Peer) {
	if len(peers) == 0 {
		return
	}

	config := GetConfig(network, peers[0], orderer, channel)
	updatedConfig := proto.Clone(config).(*common.Config)

	updatedConfig.ChannelGroup.Groups[capabilitiesGroup].Values["Capabilities"] = &common.ConfigValue{
		ModPolicy: "Admins",
		Value: protoutil.MarshalOrPanic(
			&common.Capabilities{
				Capabilities: map[string]*common.Capability{
					capabilitiesVersion: {},
				},
			},
		),
	}

	UpdateConfig(network, orderer, channel, config, updatedConfig, false, peers[0], peers...)
}

// WaitUntilEqualLedgerHeight waits until all specified peers have the
// provided ledger height on a channel
func WaitUntilEqualLedgerHeight(n *Network, channel string, height int, peers ...*Peer) {
	for _, peer := range peers {
		Eventually(func() int {
			return GetLedgerHeight(n, peer, channel)
		}, n.EventuallyTimeout).Should(Equal(height))
	}
}

// GetLedgerHeight returns the current ledger height for a peer on
// a channel
func GetLedgerHeight(n *Network, peer *Peer, channel string) int {
	sess, err := n.PeerUserSession(peer, "User1", commands.ChannelInfo{
		ChannelID:  channel,
		ClientAuth: n.ClientAuthRequired,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit())

	if sess.ExitCode() == 1 {
		// if org is not yet member of channel, peer will return error
		return -1
	}

	channelInfoStr := strings.TrimPrefix(string(sess.Buffer().Contents()[:]), "Blockchain info:")
	var channelInfo = common.BlockchainInfo{}
	json.Unmarshal([]byte(channelInfoStr), &channelInfo)
	return int(channelInfo.Height)
}

// GetMaxLedgerHeight returns the maximum ledger height for the
// peers on a channel
func GetMaxLedgerHeight(n *Network, channel string, peers ...*Peer) int {
	var maxHeight int
	for _, peer := range peers {
		peerHeight := GetLedgerHeight(n, peer, channel)
		if peerHeight > maxHeight {
			maxHeight = peerHeight
		}
	}
	return maxHeight
}
