/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

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

	docker "github.com/fsouza/go-dockerclient"
	"github.com/hyperledger/fabric-protos-go-apiv2/gateway"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric/integration/channelparticipation"
	"github.com/hyperledger/fabric/integration/nwo"
	"github.com/hyperledger/fabric/protoutil"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/tedsuo/ifrit"
	ginkgomon "github.com/tedsuo/ifrit/ginkgomon_v2"
	"google.golang.org/protobuf/proto"
)

var _ = Describe("GatewayService with endorser discovery", func() {
	var (
		testDir                     string
		network                     *nwo.Network
		orderer                     *nwo.Orderer
		org1Peer0                   *nwo.Peer
		org2Peer0                   *nwo.Peer
		org3Peer0                   *nwo.Peer
		ordererRunner               *ginkgomon.Runner
		ordererProcess, peerProcess ifrit.Process
		peerCerts                   map[string]string
	)

	loadPeerCert := func(peer *nwo.Peer) string {
		peerCert, err := os.ReadFile(network.PeerCert(peer))
		Expect(err).NotTo(HaveOccurred())
		return string(peerCert)
	}

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

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

		config := nwo.ThreeOrgEtcdRaft()
		network = nwo.New(config, testDir, client, StartPort(), components)

		network.GenerateConfigTree()
		network.Bootstrap()

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

		orderer = network.Orderer("orderer")
		channelparticipation.JoinOrdererJoinPeersAppChannel(network, "testchannel", orderer, ordererRunner)

		network.VerifyMembership(
			network.PeersWithChannel("testchannel"),
			"testchannel",
		)
		nwo.EnableCapabilities(
			network,
			"testchannel",
			"Application", "V2_0",
			orderer,
			network.PeersWithChannel("testchannel")...,
		)

		org1Peer0 = network.Peer("Org1", "peer0")
		org2Peer0 = network.Peer("Org2", "peer0")
		org3Peer0 = network.Peer("Org3", "peer0")

		peerCerts = map[string]string{
			loadPeerCert(org1Peer0): org1Peer0.ID(),
			loadPeerCert(org2Peer0): org2Peer0.ID(),
			loadPeerCert(org3Peer0): org3Peer0.ID(),
		}

		chaincode := nwo.Chaincode{
			Name:            "gatewaycc",
			Version:         "0.0",
			Path:            components.Build("github.com/hyperledger/fabric/integration/chaincode/simple/cmd"),
			Lang:            "binary",
			PackageFile:     filepath.Join(testDir, "gatewaycc.tar.gz"),
			Ctor:            `{"Args":["init","a","100","b","200"]}`,
			SignaturePolicy: `AND ('Org1MSP.peer')`,
			Sequence:        "1",
			InitRequired:    true,
			Label:           "gatewaycc_label",
		}
		nwo.DeployChaincode(network, "testchannel", orderer, chaincode)

		chaincode = nwo.Chaincode{
			Name:              "sbecc",
			Version:           "0.0",
			Path:              components.Build("github.com/hyperledger/fabric/integration/chaincode/keylevelep/cmd"),
			Ctor:              `{"Args":["init"]}`,
			Lang:              "binary",
			PackageFile:       filepath.Join(testDir, "sbecc.tar.gz"),
			SignaturePolicy:   `OR ('Org3MSP.member')`,
			Sequence:          "1",
			InitRequired:      true,
			Label:             "sbecc_label",
			CollectionsConfig: filepath.Join("testdata", "collections_config_sbe.json"),
		}
		nwo.DeployChaincode(network, "testchannel", orderer, chaincode)

		chaincode = nwo.Chaincode{
			Name:              "readpvtcc",
			Version:           "0.0",
			Path:              components.Build("github.com/hyperledger/fabric/integration/chaincode/keylevelep/cmd"),
			Ctor:              `{"Args":["init"]}`,
			Lang:              "binary",
			PackageFile:       filepath.Join(testDir, "readpvtcc.tar.gz"),
			SignaturePolicy:   `OR ('Org1MSP.member', 'Org3MSP.member')`,
			Sequence:          "1",
			InitRequired:      true,
			Label:             "readpvtcc_label",
			CollectionsConfig: filepath.Join("testdata", "collections_config_read.json"),
		}
		nwo.DeployChaincode(network, "testchannel", orderer, chaincode)
	})

	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(testDir)
	})

	verifyEndorsers := func(endorseResponse *gateway.EndorseResponse, expectedEndorsers []string) {
		preparedTransaction := endorseResponse.GetPreparedTransaction()
		payload, err := protoutil.UnmarshalPayload(preparedTransaction.GetPayload())
		Expect(err).NotTo(HaveOccurred())
		transaction, err := protoutil.UnmarshalTransaction(payload.GetData())
		Expect(err).NotTo(HaveOccurred())
		Expect(transaction.GetActions()).To(HaveLen(1))
		action, err := protoutil.UnmarshalChaincodeActionPayload(transaction.Actions[0].GetPayload())
		Expect(err).NotTo(HaveOccurred())
		endorsements := action.GetAction().GetEndorsements()

		actualEndorsers := []string{}
		for _, endorsement := range endorsements {
			id, err := protoutil.UnmarshalSerializedIdentity(endorsement.GetEndorser())
			Expect(err).NotTo(HaveOccurred())
			actualEndorsers = append(actualEndorsers, peerCerts[string(id.IdBytes)])
		}

		Expect(actualEndorsers).To(ConsistOf(expectedEndorsers))
	}

	submitTransaction := func(
		gatewayClient gateway.GatewayClient,
		signer *nwo.SigningIdentity,
		channel string,
		chaincode string,
		transactionName string,
		arguments []string,
		transientData map[string][]byte,
		expectedEndorsers []string,
	) *peer.Response {
		args := [][]byte{}
		for _, arg := range arguments {
			args = append(args, []byte(arg))
		}
		proposedTransaction, transactionID := NewProposedTransaction(
			signer,
			channel,
			chaincode,
			transactionName,
			transientData,
			args...,
		)

		endorseRequest := &gateway.EndorseRequest{
			TransactionId:       transactionID,
			ChannelId:           channel,
			ProposedTransaction: proposedTransaction,
		}

		ctx, cancel := context.WithTimeout(context.Background(), network.EventuallyTimeout)
		defer cancel()

		endorseResponse, err := gatewayClient.Endorse(ctx, endorseRequest)
		Expect(err).NotTo(HaveOccurred())

		preparedTransaction := endorseResponse.GetPreparedTransaction()
		preparedTransaction.Signature, err = signer.Sign(preparedTransaction.Payload)
		Expect(err).NotTo(HaveOccurred())

		if expectedEndorsers != nil {
			verifyEndorsers(endorseResponse, expectedEndorsers)
		}

		submitRequest := &gateway.SubmitRequest{
			TransactionId:       transactionID,
			ChannelId:           channel,
			PreparedTransaction: preparedTransaction,
		}
		_, err = gatewayClient.Submit(ctx, submitRequest)
		Expect(err).NotTo(HaveOccurred())

		idBytes, err := signer.Serialize()
		Expect(err).NotTo(HaveOccurred())

		statusRequest := &gateway.CommitStatusRequest{
			ChannelId:     "testchannel",
			Identity:      idBytes,
			TransactionId: transactionID,
		}
		statusRequestBytes, err := proto.Marshal(statusRequest)
		Expect(err).NotTo(HaveOccurred())

		signature, err := signer.Sign(statusRequestBytes)
		Expect(err).NotTo(HaveOccurred())

		signedStatusRequest := &gateway.SignedCommitStatusRequest{
			Request:   statusRequestBytes,
			Signature: signature,
		}

		statusResponse, err := gatewayClient.CommitStatus(ctx, signedStatusRequest)
		Expect(err).NotTo(HaveOccurred())
		Expect(statusResponse.Result).To(Equal(peer.TxValidationCode_VALID))

		chaincodeAction, err := protoutil.GetActionFromEnvelopeMsg(endorseResponse.GetPreparedTransaction())
		Expect(err).NotTo(HaveOccurred())

		return chaincodeAction.GetResponse()
	}

	evaluateTransaction := func(
		gatewayClient gateway.GatewayClient,
		signer *nwo.SigningIdentity,
		channel string,
		chaincode string,
		transactionName string,
		arguments []string,
		transientData map[string][]byte,
	) *peer.Response {
		args := [][]byte{}
		for _, arg := range arguments {
			args = append(args, []byte(arg))
		}
		proposedTransaction, transactionID := NewProposedTransaction(
			signer,
			channel,
			chaincode,
			transactionName,
			transientData,
			args...,
		)

		evaluateRequest := &gateway.EvaluateRequest{
			TransactionId:       transactionID,
			ChannelId:           channel,
			ProposedTransaction: proposedTransaction,
		}

		ctx, cancel := context.WithTimeout(context.Background(), network.EventuallyTimeout)
		defer cancel()

		evaluateResponse, err := gatewayClient.Evaluate(ctx, evaluateRequest)
		Expect(err).NotTo(HaveOccurred())

		return evaluateResponse.GetResult()
	}

	It("setting SBE policy should override chaincode policy", func() {
		conn := network.PeerClientConn(org1Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org1Peer0, "User1")

		// add org2 to SBE policy - requires endorsement from org3 peer (chaincode policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"addorgs",
			[]string{"pub", "Org2MSP"},
			nil,
			[]string{org3Peer0.ID()},
		)

		// add org1 to SBE policy - requires endorsement from org2 peer (SBE policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"addorgs",
			[]string{"pub", "Org1MSP"},
			nil,
			[]string{org2Peer0.ID()},
		)

		// remove org2 to SBE policy - requires endorsement from org1, org2 peers (SBE policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"delorgs",
			[]string{"pub", "Org2MSP"},
			nil,
			[]string{org1Peer0.ID(), org2Peer0.ID()},
		)
	})

	It("setting SBE policy on private key then writing to should override the collection & chaincode policies", func() {
		conn := network.PeerClientConn(org1Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org1Peer0, "User1")

		// write to private collection - requires endorsement from org2 peer (collection policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"priv", "initial private value"},
			nil,
			[]string{org2Peer0.ID()},
		)

		// check the value was set
		result := evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"priv"},
			nil,
		)
		Expect(result.Payload).To(Equal([]byte("initial private value")))

		// add org1 to SBE policy - requires endorsement from org2 peer (collection policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"addorgs",
			[]string{"priv", "Org1MSP"},
			nil,
			[]string{org2Peer0.ID()},
		)

		// write to private collection - requires endorsement from org1 peer (SBE policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"priv", "updated private value"},
			nil,
			[]string{org1Peer0.ID()},
		)

		// check the value was set
		result = evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"priv"},
			nil,
		)

		Expect(result.Payload).To(Equal([]byte("updated private value")))
	})

	It("writing to public and private keys should combine the collection & chaincode policies", func() {
		conn := network.PeerClientConn(org1Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org1Peer0, "User1")

		// write to both pub&priv - requires endorsement from org2 (collection policy) and org3 (chaincode policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"both", "initial value"},
			nil,
			[]string{org2Peer0.ID(), org3Peer0.ID()},
		)

		// check the private value was set
		result := evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"priv"},
			nil,
		)
		Expect(result.Payload).To(Equal([]byte("initial value")))

		// check the public value was set
		result = evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"pub"},
			nil,
		)
		Expect(result.Payload).To(Equal([]byte("initial value")))
	})

	It("should combine chaincode and private SBE policies", func() {
		conn := network.PeerClientConn(org1Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org1Peer0, "User1")

		// write to both public & private states - requires endorsement from org2 peer (collection policy) and org3 peer (chaincode) policy
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"both", "initial private & public value"},
			nil,
			[]string{org2Peer0.ID(), org3Peer0.ID()},
		)

		// add org1 to SBE policy for private state - requires endorsement from org2 peer (collection policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"addorgs",
			[]string{"priv", "Org1MSP"},
			nil,
			[]string{org2Peer0.ID()},
		)

		// write to both pub&priv - requires endorsement from org1 (SBE policy) and org3 (chaincode policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"both", "chaincode and SBE policies"},
			nil,
			[]string{org1Peer0.ID(), org3Peer0.ID()},
		)

		// check the private value was set
		result := evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"priv"},
			nil,
		)
		Expect(result.Payload).To(Equal([]byte("chaincode and SBE policies")))
	})

	It("should combine collection and public SBE policies", func() {
		conn := network.PeerClientConn(org3Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org3Peer0, "User1")

		// add org1 to SBE policy - requires endorsement from org3 peer (chaincode policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"addorgs",
			[]string{"pub", "Org1MSP"},
			nil,
			[]string{org3Peer0.ID()},
		)

		// write to both pub&priv - requires endorsement from org1 (SBE policy) and org2 (collection policy)
		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"setval",
			[]string{"both", "collection and SBE policies"},
			nil,
			[]string{org1Peer0.ID(), org2Peer0.ID()},
		)

		// check the private value was set
		result := evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"getval",
			[]string{"pub"},
			nil,
		)
		Expect(result.Payload).To(Equal([]byte("collection and SBE policies")))
	})

	It("should endorse chaincode on org3 and also seek endorsement from another org for cc2cc call", func() {
		conn := network.PeerClientConn(org3Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org3Peer0, "User1")

		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"cc2cc",
			[]string{"testchannel", "gatewaycc", "invoke", "a", "b", "10"},
			nil,
			[]string{org1Peer0.ID(), org3Peer0.ID()},
		)

		// check the transaction really worked - query via cc2cc call
		result := evaluateTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"sbecc",
			"cc2cc",
			[]string{"testchannel", "gatewaycc", "query", "a"},
			nil,
		)

		Expect(result.Payload).To(Equal([]byte("90")))
	})

	It("reading private data should use the collection ownership policy, not signature policy", func() {
		conn := network.PeerClientConn(org1Peer0)
		defer conn.Close()
		gatewayClient := gateway.NewGatewayClient(conn)
		signingIdentity := network.PeerUserSigner(org1Peer0, "User1")

		submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"readpvtcc",
			"setval",
			[]string{"priv", "abcd"},
			nil,
			nil, // don't care - not testing this part
		)

		result := submitTransaction(
			gatewayClient,
			signingIdentity,
			"testchannel",
			"readpvtcc",
			"getval",
			[]string{"priv"},
			nil,
			[]string{org1Peer0.ID()},
		)

		Expect(result.GetPayload()).To(Equal([]byte("abcd")))
	})
})
