/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package sbe

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/integration/channelparticipation"
	"github.com/hyperledger/fabric/integration/nwo"
	"github.com/hyperledger/fabric/integration/nwo/commands"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
	"github.com/tedsuo/ifrit"
	ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2"
)

var _ = Describe("SBE_E2E", func() {
	var (
		testDir                     string
		client                      *docker.Client
		network                     *nwo.Network
		chaincode                   nwo.Chaincode
		ordererRunner               *ginkgomon.Runner
		ordererProcess, peerProcess ifrit.Process
		tempDir                     string
	)

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

		client, err = docker.NewClientFromEnv()
		Expect(err).NotTo(HaveOccurred())

		tempDir, err = os.MkdirTemp("", "sbe")
		Expect(err).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		if ordererProcess != nil {
			ordererProcess.Signal(syscall.SIGTERM)
			Eventually(ordererProcess.Wait(), network.EventuallyTimeout).Should(Receive())
		}

		if peerProcess != nil {
			peerProcess.Signal(syscall.SIGTERM)
			Eventually(peerProcess.Wait(), network.EventuallyTimeout).Should(Receive())
		}

		if network != nil {
			network.Cleanup()
		}
		os.RemoveAll(tempDir)
		os.RemoveAll(testDir)
	})

	Describe("basic etcdraft network with 2 orgs", func() {
		BeforeEach(func() {
			network = nwo.New(nwo.BasicEtcdRaft(), testDir, client, StartPort(), components)
			network.GenerateConfigTree()
			network.Bootstrap()

			ordererRunner, ordererProcess, peerProcess = network.StartSingleOrdererNetwork("orderer")
		})

		It("executes a basic etcdraft network with 2 orgs and SBE checks with _lifecycle", func() {
			chaincode = nwo.Chaincode{
				Name:              "mycc",
				Version:           "0.0",
				Path:              "github.com/hyperledger/fabric/integration/chaincode/keylevelep/cmd",
				Lang:              "golang",
				PackageFile:       filepath.Join(tempDir, "simplecc.tar.gz"),
				Ctor:              `{"Args":["init"]}`,
				SignaturePolicy:   `OR('Org1MSP.member','Org2MSP.member')`,
				Sequence:          "1",
				InitRequired:      true,
				Label:             "my_simple_chaincode",
				CollectionsConfig: "testdata/collection_config.json",
			}

			By("getting the orderer by name")
			orderer := network.Orderer("orderer")

			By("setting up the channel")
			channelparticipation.JoinOrdererJoinPeersAppChannel(network, "testchannel", orderer, ordererRunner)
			network.VerifyMembership(network.PeersWithChannel("testchannel"), "testchannel")

			By("enabling 2.0 application capabilities")
			nwo.EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, network.Peer("Org1", "peer0"), network.Peer("Org2", "peer0"))

			By("deploying the chaincode")
			nwo.DeployChaincode(network, "testchannel", orderer, chaincode)

			By("deploying a second instance of the chaincode")
			chaincode.Name = "mycc2"
			chaincode.PackageFile = filepath.Join(tempDir, "simplecc2.tar.gz")
			chaincode.Label = "my_other_simple_chaincode"
			nwo.DeployChaincode(network, "testchannel", orderer, chaincode)

			RunSBE(network, orderer, "pub")
			RunSBE(network, orderer, "priv")
		})
	})
})

func RunSBE(n *nwo.Network, orderer *nwo.Orderer, mode string) {
	peerOrg1 := n.Peer("Org1", "peer0")
	peerOrg2 := n.Peer("Org2", "peer0")

	By("org1 initializes the key")
	sess, err := n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["setval", "` + mode + `", "foo"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	syncLedgerHeights(n, peerOrg1, peerOrg2)

	By("org2 checks that setting the value was successful by reading it")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("foo"))

	By("org1 adds org1 to the state-based ep of a key")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["addorgs", "` + mode + `", "Org1MSP"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	By("checking that the modification succeeded through listing the orgs in the ep")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["listorgs", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("Org1MSP"))

	By("org1 sets the value of the key")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["setval", "` + mode + `", "val1"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	syncLedgerHeights(n, peerOrg1, peerOrg2)

	By("org2 checks that setting the value was successful by reading it")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("val1"))

	By("org2 sets the value of the key")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["setval", "` + mode + `", "val2"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(1))
	Expect(sess.Err).To(gbytes.Say(`\Qcommitted with status (ENDORSEMENT_POLICY_FAILURE)\E`))

	By("org2 checks that setting the value was not successful by reading it")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("val1"))

	syncLedgerHeights(n, peerOrg2, peerOrg1)

	By("org1 adds org2 to the ep of the key")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["addorgs", "` + mode + `", "Org2MSP"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	By("org1 lists the orgs of the ep to check that both org1 and org2 are there")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["listorgs", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	orgs := [2]string{"Org1MSP", "Org2MSP"}
	orgsList, err := json.Marshal(orgs)
	Expect(err).NotTo(HaveOccurred())
	Expect(sess).To(gbytes.Say(string(orgsList)))

	syncLedgerHeights(n, peerOrg1, peerOrg2)

	By("org2 sets the value of the key")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["setval", "` + mode + `", "val3"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(1))
	Expect(sess.Err).To(gbytes.Say(`\Qcommitted with status (ENDORSEMENT_POLICY_FAILURE)\E`))

	By("org2 checks that seting the value was not successful by reading it")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("val1"))

	syncLedgerHeights(n, peerOrg2, peerOrg1)

	By("org1 and org2 set the value of the key")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["setval", "` + mode + `", "val4"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	By("org1 checks that setting the value was successful by reading it")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("val4"))

	syncLedgerHeights(n, peerOrg1, peerOrg2)

	By("org2 deletes org1 from the ep of the key")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["delorgs", "` + mode + `", "Org1MSP"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(1))
	Expect(sess.Err).To(gbytes.Say(`\Qcommitted with status (ENDORSEMENT_POLICY_FAILURE)\E`))

	By("org2 lists the orgs of the key to check that deleting org1 did not succeed")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["listorgs", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say(string(orgsList)))

	syncLedgerHeights(n, peerOrg2, peerOrg1)

	By("org1 and org2 delete org1 from the ep of the key")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["delorgs", "` + mode + `", "Org1MSP"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	By("org2 lists the orgs of the key's ep to check that removing org1 from the ep was successful")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["listorgs", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("Org2MSP"))

	By("org2 uses cc2cc invocation to set the value of the key")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc2",
		Ctor:      `{"Args":["cc2cc", "testchannel", "mycc", "setval", "` + mode + `", "cc2cc_org2"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg2, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

	By("org2 reads the value of the key to check that setting it was successful")
	sess, err = n.PeerUserSession(peerOrg2, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("cc2cc_org2"))

	syncLedgerHeights(n, peerOrg2, peerOrg1)

	By("org1 uses cc2cc to set the value of the key")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc2",
		Ctor:      `{"Args":["cc2cc", "testchannel", "mycc", "setval", "` + mode + `", "cc2cc_org1"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peerOrg1, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(1))
	Expect(sess.Err).To(gbytes.Say(`\Qcommitted with status (ENDORSEMENT_POLICY_FAILURE)\E`))

	By("org1 reads the value of the key to check that setting it was not successful")
	sess, err = n.PeerUserSession(peerOrg1, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["getval", "` + mode + `"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("cc2cc_org2"))
}

func getLedgerHeight(n *nwo.Network, peer *nwo.Peer, channelName string) int {
	sess, err := n.PeerUserSession(peer, "User1", commands.ChannelInfo{
		ChannelID: channelName,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))

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

func syncLedgerHeights(n *nwo.Network, peer1 *nwo.Peer, peer2 *nwo.Peer) {
	// get height from peer1
	height := getLedgerHeight(n, peer1, "testchannel")
	// wait for same height on peer2
	Eventually(func() int {
		return getLedgerHeight(n, peer2, "testchannel")
	}, n.EventuallyTimeout).Should(Equal(height))
}
