/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package lifecycle

import (
	"bytes"
	"os"
	"path/filepath"
	"syscall"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/hyperledger/fabric-config/protolator"
	"github.com/hyperledger/fabric-config/protolator/protoext/peerext"
	"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/hyperledger/fabric/integration/nwo/fabricconfig"
	"github.com/hyperledger/fabric/integration/nwo/runner"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
	. "github.com/onsi/gomega/gstruct"
	"github.com/tedsuo/ifrit"
	ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2"
	"google.golang.org/protobuf/proto"
)

var _ = Describe("Lifecycle", func() {
	var (
		client                      *docker.Client
		testDir                     string
		network                     *nwo.Network
		processes                   = map[string]ifrit.Process{}
		ordererRunner               *ginkgomon.Runner
		ordererProcess, peerProcess ifrit.Process

		termFiles []string
	)

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

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

		network = nwo.New(nwo.BasicEtcdRaft(), testDir, client, StartPort(), components)

		// Generate config
		network.GenerateConfigTree()

		// configure only one of four peers (Org1, peer0) to use couchdb.
		// Note that we do not support a channel with mixed DBs.
		// However, for testing, it would be fine to use couchdb for one
		// peer. We're using couchdb here to ensure all supported character
		// classes in chaincode names/versions work on the supported db types.
		couchDB := &runner.CouchDB{}
		couchProcess := ifrit.Invoke(couchDB)
		Eventually(couchProcess.Ready(), runner.DefaultStartTimeout).Should(BeClosed())
		Consistently(couchProcess.Wait()).ShouldNot(Receive())
		couchAddr := couchDB.Address()
		peer := network.Peer("Org1", "peer0")
		core := network.ReadPeerConfig(peer)
		core.Ledger.State.StateDatabase = "CouchDB"
		core.Ledger.State.CouchDBConfig.CouchDBAddress = couchAddr
		processes[couchDB.Name] = couchProcess
		setTermFileEnvForBinaryExternalBuilder := func(envKey, termFile string, externalBuilders []fabricconfig.ExternalBuilder) {
			os.Setenv(envKey, termFile)
			for i, e := range externalBuilders {
				if e.Name == "binary" {
					e.PropagateEnvironment = append(e.PropagateEnvironment, envKey)
					externalBuilders[i] = e
				}
			}
		}
		org1TermFile := filepath.Join(testDir, "org1-term-file")
		setTermFileEnvForBinaryExternalBuilder("ORG1_TERM_FILE", org1TermFile, core.Chaincode.ExternalBuilders)
		network.WritePeerConfig(peer, core)

		peer = network.Peer("Org2", "peer0")
		core = network.ReadPeerConfig(peer)
		org2TermFile := filepath.Join(testDir, "org2-term-file")
		setTermFileEnvForBinaryExternalBuilder("ORG2_TERM_FILE", org2TermFile, core.Chaincode.ExternalBuilders)
		network.WritePeerConfig(peer, core)

		termFiles = []string{org1TermFile, org2TermFile}

		// bootstrap the network
		network.Bootstrap()

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

	AfterEach(func() {
		// Shutdown processes and cleanup
		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())
		}

		for _, p := range processes {
			p.Signal(syscall.SIGTERM)
			Eventually(p.Wait(), network.EventuallyTimeout).Should(Receive())
		}
		network.Cleanup()

		os.RemoveAll(testDir)
	})

	It("deploys and executes chaincode using _lifecycle and upgrades it", func() {
		orderer := network.Orderer("orderer")
		testPeers := network.PeersWithChannel("testchannel")
		org1peer0 := network.Peer("Org1", "peer0")

		chaincodePath := components.Build("github.com/hyperledger/fabric/integration/chaincode/simple/cmd")
		chaincode := nwo.Chaincode{
			Name:                "My_1st-Chaincode",
			Version:             "Version-0.0",
			Path:                chaincodePath,
			Lang:                "binary",
			PackageFile:         filepath.Join(testDir, "simplecc.tar.gz"),
			Ctor:                `{"Args":["init","a","100","b","200"]}`,
			ChannelConfigPolicy: "/Channel/Application/Endorsement",
			Sequence:            "1",
			InitRequired:        true,
			Label:               "my_simple_chaincode",
		}

		By("setting up the channel")
		channelparticipation.JoinOrdererJoinPeersAppChannel(network, "testchannel", orderer, ordererRunner)
		network.VerifyMembership(network.PeersWithChannel("testchannel"), "testchannel")
		nwo.EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, network.Peer("Org1", "peer0"), network.Peer("Org2", "peer0"))

		By("deploying the chaincode")
		nwo.PackageChaincodeBinary(chaincode)
		chaincode.SetPackageIDFromPackageFile()

		nwo.InstallChaincode(network, chaincode, testPeers...)

		By("verifying the installed chaincode package matches the one that was submitted")
		sess, err := network.PeerAdminSession(testPeers[0], commands.ChaincodeGetInstalledPackage{
			PackageID:       chaincode.PackageID,
			OutputDirectory: testDir,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(0))
		fileBytes, err := os.ReadFile(chaincode.PackageFile)
		Expect(err).NotTo(HaveOccurred())
		fileBytesFromPeer, err := os.ReadFile(filepath.Join(network.RootDir, chaincode.PackageID+".tar.gz"))
		Expect(err).NotTo(HaveOccurred())
		Expect(fileBytesFromPeer).To(Equal(fileBytes))

		nwo.ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, testPeers...)

		nwo.CheckCommitReadinessUntilReady(network, "testchannel", chaincode, network.PeerOrgs(), testPeers...)
		nwo.CommitChaincode(network, "testchannel", orderer, chaincode, testPeers[0], testPeers...)
		nwo.InitChaincode(network, "testchannel", orderer, chaincode, testPeers...)

		By("ensuring the chaincode can be invoked and queried")
		endorsers := []*nwo.Peer{
			network.Peer("Org1", "peer0"),
			network.Peer("Org2", "peer0"),
		}
		RunQueryInvokeQuery(network, orderer, "My_1st-Chaincode", 100, endorsers...)

		By("setting a bad package ID to temporarily disable endorsements on org1")
		savedPackageID := chaincode.PackageID
		// note that in theory it should be sufficient to set it to an
		// empty string, but the ApproveChaincodeForMyOrg
		// function fills the packageID field if empty
		chaincode.PackageID = "bad"
		nwo.ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, org1peer0)

		By("querying the chaincode and expecting the invocation to fail")
		sess, err = network.PeerUserSession(org1peer0, "User1", commands.ChaincodeQuery{
			ChannelID: "testchannel",
			Name:      "My_1st-Chaincode",
			Ctor:      `{"Args":["query","a"]}`,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(1))
		Expect(sess.Err).To(gbytes.Say("Error: endorsement failure during query. response: status:500 "))
		Expect(sess.Err).To(gbytes.Say("message:\"make sure the chaincode My_1st-Chaincode has been successfully defined on channel testchannel and try " +
			"again: chaincode definition for 'My_1st-Chaincode' exists, but chaincode is not installed\""))

		By("setting the correct package ID to restore the chaincode")
		chaincode.PackageID = savedPackageID
		nwo.ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, org1peer0)

		By("querying the chaincode and expecting the invocation to succeed")
		sess, err = network.PeerUserSession(org1peer0, "User1", commands.ChaincodeQuery{
			ChannelID: "testchannel",
			Name:      "My_1st-Chaincode",
			Ctor:      `{"Args":["query","a"]}`,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(0))
		Expect(sess).To(gbytes.Say("90"))

		By("upgrading the chaincode to sequence 2 using a new chaincode package")
		previousLabel := chaincode.Label
		previousPackageID := chaincode.PackageID
		chaincode.Label = "my_simple_chaincode_updated"
		chaincode.PackageFile = filepath.Join(testDir, "simplecc-updated.tar.gz")
		chaincode.Sequence = "2"
		nwo.PackageChaincodeBinary(chaincode)
		chaincode.SetPackageIDFromPackageFile()
		nwo.InstallChaincode(network, chaincode, testPeers...)

		nwo.ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, testPeers...)

		nwo.CheckCommitReadinessUntilReady(network, "testchannel", chaincode, network.PeerOrgs(), testPeers...)
		nwo.CommitChaincode(network, "testchannel", orderer, chaincode, testPeers[0], testPeers...)

		By("listing the installed chaincodes and verifying the channel/chaincode definitions that are using the chaincode package")
		nwo.QueryInstalledReferences(network, "testchannel", chaincode.Label, chaincode.PackageID, network.Peer("Org2", "peer0"), []string{"My_1st-Chaincode", "Version-0.0"})

		By("checking for term files that are written when the chaincode is stopped via SIGTERM")
		for _, termFile := range termFiles {
			Expect(termFile).To(BeARegularFile())
		}

		By("ensuring the previous chaincode package is no longer referenced by a chaincode definition on the channel")
		Expect(nwo.QueryInstalled(network, network.Peer("Org2", "peer0"))()).To(
			ContainElement(MatchFields(IgnoreExtras,
				Fields{
					"Label":      Equal(previousLabel),
					"PackageId":  Equal(previousPackageID),
					"References": Not(HaveKey("testchannel")),
				},
			)),
		)

		By("ensuring the chaincode can still be invoked and queried")
		RunQueryInvokeQuery(network, orderer, "My_1st-Chaincode", 90, endorsers...)

		By("deploying another chaincode using the same chaincode package")
		anotherChaincode := nwo.Chaincode{
			Name:                "Your_Chaincode",
			Version:             "Version+0_0",
			Path:                chaincodePath,
			Lang:                "binary",
			PackageFile:         filepath.Join(testDir, "simplecc.tar.gz"),
			Ctor:                `{"Args":["init","a","100","b","200"]}`,
			ChannelConfigPolicy: "/Channel/Application/Endorsement",
			Sequence:            "1",
			InitRequired:        true,
			Label:               "my_simple_chaincode",
		}
		nwo.DeployChaincode(network, "testchannel", orderer, anotherChaincode)

		By("listing the installed chaincodes and verifying the channel/chaincode definitions that are using the chaincode package")
		anotherChaincode.SetPackageIDFromPackageFile()
		nwo.QueryInstalledReferences(network, "testchannel", anotherChaincode.Label, anotherChaincode.PackageID, network.Peer("Org2", "peer0"), []string{"Your_Chaincode", "Version+0_0"})

		By("adding a new org")
		org3 := &nwo.Organization{
			MSPID:         "Org3MSP",
			Name:          "Org3",
			Domain:        "org3.example.com",
			EnableNodeOUs: true,
			Users:         2,
			CA: &nwo.CA{
				Hostname: "ca",
			},
		}

		org3peer0 := &nwo.Peer{
			Name:         "peer0",
			Organization: "Org3",
			Channels:     testPeers[0].Channels,
		}

		network.AddOrg(org3, org3peer0)
		GenerateOrgUpdateMaterials(network, org3peer0)

		By("starting the org3 peer")
		pr := network.PeerRunner(org3peer0)
		org3Process := ifrit.Invoke(pr)
		processes[org3peer0.ID()] = org3Process
		Eventually(org3Process.Ready(), network.EventuallyTimeout).Should(BeClosed())

		By("updating the channel config to include org3")
		// get the current channel config
		currentConfig := nwo.GetConfig(network, testPeers[0], orderer, "testchannel")
		updatedConfig := proto.Clone(currentConfig).(*common.Config)

		// get the configtx info for org3
		sess, err = network.ConfigTxGen(commands.PrintOrg{
			ConfigPath: network.RootDir,
			PrintOrg:   "Org3",
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(0))
		org3Group := &peerext.DynamicApplicationOrgGroup{ConfigGroup: &common.ConfigGroup{}}
		err = protolator.DeepUnmarshalJSON(bytes.NewBuffer(sess.Out.Contents()), org3Group)
		Expect(err).NotTo(HaveOccurred())

		// update the channel config to include org3
		updatedConfig.ChannelGroup.Groups["Application"].Groups["Org3"] = org3Group.ConfigGroup
		nwo.UpdateConfig(network, orderer, "testchannel", currentConfig, updatedConfig, true, testPeers[0], nil, testPeers...)

		By("joining the org3 peers to the channel")
		network.JoinChannel("testchannel", orderer, org3peer0)

		// update testPeers now that org3 has joined
		testPeers = network.PeersWithChannel("testchannel")

		// wait until all peers, particularly those in org3, have received the block
		// containing the updated config
		maxLedgerHeight := nwo.GetMaxLedgerHeight(network, "testchannel", testPeers...)
		nwo.WaitUntilEqualLedgerHeight(network, "testchannel", maxLedgerHeight, testPeers...)

		By("querying definitions by org3 before performing any chaincode actions")
		sess, err = network.PeerAdminSession(network.Peer("Org2", "peer0"), commands.ChaincodeListCommitted{
			ChannelID: "testchannel",
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(0))

		By("installing the chaincode to the org3 peers")
		nwo.InstallChaincode(network, chaincode, org3peer0)

		By("ensuring org3 peers do not execute the chaincode before approving the definition")
		org3AndOrg1PeerAddresses := []string{
			network.PeerAddress(org3peer0, nwo.ListenPort),
			network.PeerAddress(org1peer0, nwo.ListenPort),
		}

		sess, err = network.PeerUserSession(org3peer0, "User1", commands.ChaincodeInvoke{
			ChannelID:     "testchannel",
			Orderer:       network.OrdererAddress(orderer, nwo.ListenPort),
			Name:          "My_1st-Chaincode",
			Ctor:          `{"Args":["invoke","a","b","10"]}`,
			PeerAddresses: org3AndOrg1PeerAddresses,
			WaitForEvent:  true,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(1))
		Expect(sess.Err).To(gbytes.Say("chaincode definition for 'My_1st-Chaincode' at sequence 2 on channel 'testchannel' has not yet been approved by this org"))

		By("org3 approving the chaincode definition")
		nwo.ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, network.PeersInOrg("Org3")...)
		nwo.EnsureChaincodeCommitted(network, "testchannel", chaincode.Name, chaincode.Version, chaincode.Sequence, []*nwo.Organization{network.Organization("Org1"), network.Organization("Org2"), network.Organization("Org3")}, org3peer0)

		By("ensuring chaincode can be invoked and queried by org3")
		org3andOrg1Endorsers := []*nwo.Peer{
			network.Peer("Org3", "peer0"),
			network.Peer("Org1", "peer0"),
		}
		RunQueryInvokeQuery(network, orderer, "My_1st-Chaincode", 80, org3andOrg1Endorsers...)

		By("deploying a chaincode without an endorsement policy specified")
		chaincode = nwo.Chaincode{
			Name:         "defaultpolicycc",
			Version:      "0.0",
			Path:         chaincodePath,
			Lang:         "binary",
			PackageFile:  filepath.Join(testDir, "modulecc.tar.gz"),
			Ctor:         `{"Args":["init","a","100","b","200"]}`,
			Sequence:     "1",
			InitRequired: true,
			Label:        "my_simple_chaincode",
		}

		nwo.DeployChaincode(network, "testchannel", orderer, chaincode)

		By("attempting to invoke the chaincode without a majority")
		sess, err = network.PeerUserSession(org3peer0, "User1", commands.ChaincodeInvoke{
			ChannelID:    "testchannel",
			Orderer:      network.OrdererAddress(orderer, nwo.ListenPort),
			Name:         "defaultpolicycc",
			Ctor:         `{"Args":["invoke","a","b","10"]}`,
			WaitForEvent: true,
		})
		Expect(err).ToNot(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(1))
		Expect(sess.Err).To(gbytes.Say(`\QError: transaction invalidated with status (ENDORSEMENT_POLICY_FAILURE)\E`))

		By("attempting to invoke the chaincode with a majority")
		sess, err = network.PeerUserSession(org3peer0, "User1", commands.ChaincodeInvoke{
			ChannelID:     "testchannel",
			Orderer:       network.OrdererAddress(orderer, nwo.ListenPort),
			Name:          "defaultpolicycc",
			Ctor:          `{"Args":["invoke","a","b","10"]}`,
			PeerAddresses: org3AndOrg1PeerAddresses,
			WaitForEvent:  true,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(0))
		Expect(sess.Err).To(gbytes.Say(`\Qcommitted with status (VALID)\E`))
		Expect(sess.Err).To(gbytes.Say(`Chaincode invoke successful. result: status:200`))
	})
})
