/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package devmode

import (
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"syscall"
	"time"

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

var _ = Describe("Devmode", func() {
	var (
		testDir          string
		client           *docker.Client
		network          *nwo.Network
		process          ifrit.Process
		chaincode        nwo.Chaincode
		legacyChaincode  nwo.Chaincode
		chaincodeRunner  *ginkgomon.Runner
		chaincodeProcess ifrit.Process
		channelName      string
	)

	BeforeEach(func() {
		var err error
		channelName = "testchannel"
		testDir, err = ioutil.TempDir("", "devmode")
		Expect(err).NotTo(HaveOccurred())

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

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

		network.TLSEnabled = false
		network.Peer("Org1", "peer0").DevMode = true

		network.GenerateConfigTree()
		network.Bootstrap()

		networkRunner := network.NetworkGroupRunner()
		process = ifrit.Invoke(networkRunner)
		Eventually(process.Ready(), network.EventuallyTimeout).Should(BeClosed())
	})

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

		if chaincodeProcess != nil {
			chaincodeProcess.Signal(syscall.SIGTERM)
		}

		if network != nil {
			network.Cleanup()
		}

		os.RemoveAll(testDir)
	})

	It("executes chaincode in dev mode using legacy lifecycle", func() {
		legacyChaincode = nwo.Chaincode{
			Name:    "mycc",
			Version: "0.0",
			Path:    "github.com/hyperledger/fabric/integration/chaincode/simple/cmd",
			Ctor:    `{"Args":["init","a","100","b","200"]}`,
			Policy:  `OR ('Org1MSP.member')`,
		}

		org1peer0 := network.Peer("Org1", "peer0")
		orderer := network.Orderer("orderer")

		By("setting up the channel")
		network.CreateAndJoinChannel(orderer, channelName)

		By("building chaincode")
		chaincodeExecutePath := components.Build(legacyChaincode.Path)

		By("running the chaincode")
		legacyChaincodeID := legacyChaincode.Name + ":" + legacyChaincode.Version
		peerChaincodeAddress := network.PeerAddress(org1peer0, nwo.ChaincodePort)
		envs := []string{
			"CORE_PEER_TLS_ENABLED=false",
			"CORE_CHAINCODE_ID_NAME=" + legacyChaincodeID,
			"DEVMODE_ENABLED=true",
		}
		cmd := exec.Command(chaincodeExecutePath, "-peer.address", peerChaincodeAddress)
		cmd.Env = append(cmd.Env, envs...)
		chaincodeRunner = ginkgomon.New(ginkgomon.Config{
			Name:              "chaincode",
			Command:           cmd,
			StartCheckTimeout: 15 * time.Second,
			StartCheck:        "starting up in devmode...",
		})
		chaincodeProcess = ifrit.Invoke(chaincodeRunner)
		Eventually(chaincodeProcess.Ready(), network.EventuallyTimeout).Should(BeClosed())

		By("installing the chaincode")
		nwo.InstallChaincodeLegacy(network, legacyChaincode, org1peer0)

		By("instantiating the chaincode")
		nwo.InstantiateChaincodeLegacy(network, channelName, orderer, legacyChaincode, org1peer0, org1peer0)

		By("querying and invoking the chaincode")
		RunQueryInvokeQuery(network, orderer, org1peer0, channelName, 100)
		Eventually(chaincodeRunner).Should(gbytes.Say("invoking in devmode"))
	})

	It("executes chaincode in dev mode", func() {
		chaincode = nwo.Chaincode{
			Name:            "mycc",
			Version:         "0.0",
			Path:            components.Build("github.com/hyperledger/fabric/integration/chaincode/simple/cmd"),
			Lang:            "binary",
			PackageFile:     filepath.Join(testDir, "simplecc.tar.gz"),
			Ctor:            `{"Args":["init","a","100","b","200"]}`,
			SignaturePolicy: `OR ('Org1MSP.member')`,
			Sequence:        "1",
			InitRequired:    true,
			Label:           "my_prebuilt_chaincode",
		}

		org1peer0 := network.Peer("Org1", "peer0")
		orderer := network.Orderer("orderer")

		By("setting up the channel")
		network.CreateAndJoinChannel(orderer, channelName)

		By("enabling V2_0 application capabilities")
		nwo.EnableCapabilities(network, channelName, "Application", "V2_0", orderer, org1peer0)

		By("running the chaincode")
		chaincodeID := chaincode.Name + ":" + chaincode.Version
		peerChaincodeAddress := network.PeerAddress(org1peer0, nwo.ChaincodePort)
		envs := []string{
			"CORE_PEER_TLS_ENABLED=false",
			"CORE_PEER_ADDRESS=" + peerChaincodeAddress,
			"CORE_CHAINCODE_ID_NAME=" + chaincodeID,
			"DEVMODE_ENABLED=true",
		}

		cmd := exec.Command(chaincode.Path, "-peer.address", peerChaincodeAddress)
		cmd.Env = append(cmd.Env, envs...)
		chaincodeRunner = ginkgomon.New(ginkgomon.Config{
			Name:              "chaincode",
			Command:           cmd,
			StartCheckTimeout: 15 * time.Second,
			StartCheck:        "starting up in devmode...",
		})
		chaincodeProcess = ifrit.Invoke(chaincodeRunner)
		Eventually(chaincodeProcess.Ready(), network.EventuallyTimeout).Should(BeClosed())

		By("approving chaincode for orgs")
		ApproveChaincodeForMyOrg(network, channelName, orderer, chaincode, org1peer0)
		By("committing the chaincode definition")
		nwo.CheckCommitReadinessUntilReady(network, channelName, chaincode, network.PeerOrgs(), org1peer0)
		nwo.CommitChaincode(network, channelName, orderer, chaincode, org1peer0, org1peer0)
		By("initializing chaincode if required")
		if chaincode.InitRequired {
			nwo.InitChaincode(network, channelName, orderer, chaincode, org1peer0)
		}

		By("querying and invoking the chaincode")
		RunQueryInvokeQuery(network, orderer, org1peer0, channelName, 100)
		Eventually(chaincodeRunner.Buffer()).Should(gbytes.Say("invoking in devmode"))

		By("killing chaincode process")
		chaincodeProcess.Signal(syscall.SIGKILL)
		Eventually(chaincodeProcess.Wait(), network.EventuallyTimeout).Should(Receive())

		By("invoking chaincode after it has been killed, expecting it to fail")
		sess, err := network.PeerUserSession(org1peer0, "User1", commands.ChaincodeInvoke{
			ChannelID: channelName,
			Orderer:   network.OrdererAddress(orderer, nwo.ListenPort),
			Name:      "mycc",
			Ctor:      `{"Args":["invoke","a","b","10"]}`,
			PeerAddresses: []string{
				network.PeerAddress(network.Peer("Org1", "peer0"), nwo.ListenPort),
			},
			WaitForEvent: true,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network.EventuallyTimeout).Should(gexec.Exit(1))

		By("restarting the chaincode process")
		cmd = exec.Command(chaincode.Path, []string{"-peer.address", peerChaincodeAddress}...)
		cmd.Env = append(cmd.Env, envs...)
		chaincodeRunner = ginkgomon.New(ginkgomon.Config{
			Name:              "chaincode",
			Command:           cmd,
			StartCheckTimeout: 15 * time.Second,
			StartCheck:        "starting up in devmode...",
		})
		chaincodeProcess = ifrit.Invoke(chaincodeRunner)
		Eventually(chaincodeProcess.Ready(), network.EventuallyTimeout).Should(BeClosed())

		By("querying and invoking the chaincode")
		RunQueryInvokeQuery(network, orderer, org1peer0, channelName, 90)
		Eventually(chaincodeRunner).Should(gbytes.Say("invoking in devmode"))
	})
})

func RunQueryInvokeQuery(n *nwo.Network, orderer *nwo.Orderer, peer *nwo.Peer, channel string, queryValue int) {
	By("querying the chaincode")
	sess, err := n.PeerUserSession(peer, "User1", commands.ChaincodeQuery{
		ChannelID: channel,
		Name:      "mycc",
		Ctor:      `{"Args":["query","a"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say(strconv.Itoa(queryValue)))

	By("invoking the chaincode")
	sess, err = n.PeerUserSession(peer, "User1", commands.ChaincodeInvoke{
		ChannelID: channel,
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["invoke","a","b","10"]}`,
		PeerAddresses: []string{
			n.PeerAddress(peer, nwo.ListenPort),
		},
		WaitForEvent: true,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess.Err).To(gbytes.Say("Chaincode invoke successful. result: status:200"))

	By("querying the chaincode again")
	sess, err = n.PeerUserSession(peer, "User1", commands.ChaincodeQuery{
		ChannelID: channel,
		Name:      "mycc",
		Ctor:      `{"Args":["query","a"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say(strconv.Itoa(queryValue - 10)))
}

// ApproveChaincodeForMyOrg should only be used when devmode is enabled.
// It does not set PackageID form ChaincodeApproveForMyOrg command.
func ApproveChaincodeForMyOrg(n *nwo.Network, channel string, orderer *nwo.Orderer, chaincode nwo.Chaincode, peers ...*nwo.Peer) {
	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, nwo.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,
				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`))
		}
	}
}

var devModeSolo = &nwo.Config{
	Organizations: []*nwo.Organization{{
		Name:          "OrdererOrg",
		MSPID:         "OrdererMSP",
		Domain:        "example.com",
		EnableNodeOUs: false,
		Users:         0,
		CA:            &nwo.CA{Hostname: "ca"},
	}, {
		Name:          "Org1",
		MSPID:         "Org1MSP",
		Domain:        "org1.example.com",
		EnableNodeOUs: true,
		Users:         2,
		CA:            &nwo.CA{Hostname: "ca"},
	}},
	Consortiums: []*nwo.Consortium{{
		Name: "SampleConsortium",
		Organizations: []string{
			"Org1",
		},
	}},
	Consensus: &nwo.Consensus{
		Type:            "solo",
		BootstrapMethod: "file",
	},
	SystemChannel: &nwo.SystemChannel{
		Name:    "systemchannel",
		Profile: "OneOrgOrdererGenesis",
	},
	Orderers: []*nwo.Orderer{
		{Name: "orderer", Organization: "OrdererOrg"},
	},
	Channels: []*nwo.Channel{
		{Name: "testchannel", Profile: "OneOrgChannel"},
	},
	Peers: []*nwo.Peer{{
		Name:         "peer0",
		Organization: "Org1",
		Channels: []*nwo.PeerChannel{
			{Name: "testchannel", Anchor: true},
		},
	}},
	Profiles: []*nwo.Profile{{
		Name:     "OneOrgOrdererGenesis",
		Orderers: []string{"orderer"},
	}, {
		Name:          "OneOrgChannel",
		Consortium:    "SampleConsortium",
		Organizations: []string{"Org1"},
	}},
}
