/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package pluggable

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"
	"time"

	docker "github.com/fsouza/go-dockerclient"
	"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/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("EndToEnd", func() {
	var (
		testDir                     string
		client                      *docker.Client
		network                     *nwo.Network
		chaincode                   nwo.Chaincode
		ordererRunner               *ginkgomon.Runner
		ordererProcess, peerProcess ifrit.Process

		endorsementPluginPath string
		validationPluginPath  string
	)

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

		// Compile plugins
		endorsementPluginPath = compilePlugin("endorsement")
		validationPluginPath = compilePlugin("validation")

		// Create directories for endorsement and validation activation
		dir := filepath.Join(testDir, "endorsement")
		err = os.Mkdir(dir, 0o700)
		Expect(err).NotTo(HaveOccurred())
		SetEndorsementPluginActivationFolder(dir)

		dir = filepath.Join(testDir, "validation")
		err = os.Mkdir(dir, 0o700)
		Expect(err).NotTo(HaveOccurred())
		SetValidationPluginActivationFolder(dir)

		basicEtcdRaftConfig := nwo.BasicEtcdRaft()
		Expect(basicEtcdRaftConfig.Peers).To(HaveLen(2))

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

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

		// modify config
		configurePlugins(network, endorsementPluginPath, validationPluginPath)

		// generate network config
		network.Bootstrap()
		// Start all the fabric processes
		ordererRunner, ordererProcess, peerProcess = network.StartSingleOrdererNetwork("orderer")

		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','Org2MSP.member')`,
			Sequence:        "1",
			InitRequired:    true,
			Label:           "my_prebuilt_chaincode",
		}
		orderer := network.Orderer("orderer")
		channelparticipation.JoinOrdererJoinPeersAppChannel(network, "testchannel", orderer, ordererRunner)
		nwo.EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, network.Peer("Org1", "peer0"), network.Peer("Org2", "peer0"))
		nwo.DeployChaincode(network, "testchannel", orderer, chaincode)
	})

	AfterEach(func() {
		// stop the network
		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())
		}

		// cleanup the network artifacts
		network.Cleanup()
		os.RemoveAll(testDir)

		// cleanup the compiled plugins
		os.Remove(endorsementPluginPath)
		os.Remove(validationPluginPath)
	})

	It("executes a basic etcdraft network with specified plugins", func() {
		// Make sure plugins activated
		peerCount := len(network.Peers)
		activations := CountEndorsementPluginActivations()
		Expect(activations).To(Equal(peerCount))
		activations = CountValidationPluginActivations()
		Expect(activations).To(Equal(peerCount))

		RunQueryInvokeQuery(network, network.Orderer("orderer"), network.Peer("Org1", "peer0"))
	})
})

// compilePlugin compiles the plugin of the given type and returns the path for
// the plugin file.
func compilePlugin(pluginType string) string {
	// Building plugins can take quite a while as the build caches won't be
	// populated for the 'dynlink' install suffix. While a minute or two is
	// usually enough, timeouts have occurred at 2 minutes on slow machines.
	const buildTimeout = 5 * time.Minute

	pluginFilePath := filepath.Join("testdata", "plugins", pluginType, "plugin.so")
	cmd := exec.Command(
		"go", "build",
		"-x", // print build commands while running
		"-buildmode=plugin",
		"-tags=generic", // workaround to ensure github.com/kilic/bls12-381 dependency compiles on amd64 in plugin mode
		"-o", pluginFilePath,
		fmt.Sprintf("github.com/hyperledger/fabric/integration/pluggable/testdata/plugins/%s", pluginType),
	)
	pw := gexec.NewPrefixedWriter(fmt.Sprintf("[build-plugin-%s] ", pluginType), GinkgoWriter)
	sess, err := gexec.Start(cmd, pw, pw)
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, buildTimeout).Should(gexec.Exit(0))

	Expect(pluginFilePath).To(BeARegularFile())
	return pluginFilePath
}

func configurePlugins(network *nwo.Network, endorsement, validation string) {
	for _, p := range network.Peers {
		core := network.ReadPeerConfig(p)
		core.Peer.Handlers.Endorsers = fabricconfig.HandlerMap{
			"escc": fabricconfig.Handler{Name: "plugin-escc", Library: endorsement},
		}
		core.Peer.Handlers.Validators = fabricconfig.HandlerMap{
			"vscc": fabricconfig.Handler{Name: "plugin-vscc", Library: validation},
		}
		network.WritePeerConfig(p, core)
	}
}

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

	sess, err = n.PeerUserSession(peer, "User1", commands.ChaincodeInvoke{
		ChannelID: "testchannel",
		Orderer:   n.OrdererAddress(orderer, nwo.ListenPort),
		Name:      "mycc",
		Ctor:      `{"Args":["invoke","a","b","10"]}`,
		PeerAddresses: []string{
			n.PeerAddress(n.Peer("Org1", "peer0"), nwo.ListenPort),
			n.PeerAddress(n.Peer("Org2", "peer0"), 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"))

	sess, err = n.PeerUserSession(peer, "User1", commands.ChaincodeQuery{
		ChannelID: "testchannel",
		Name:      "mycc",
		Ctor:      `{"Args":["query","a"]}`,
	})
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, n.EventuallyTimeout).Should(gexec.Exit(0))
	Expect(sess).To(gbytes.Say("90"))
}
