/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

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

	docker "github.com/fsouza/go-dockerclient"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/gateway"
	"github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer/lifecycle"
	"github.com/hyperledger/fabric/integration/channelparticipation"
	"github.com/hyperledger/fabric/integration/nwo"
	. "github.com/hyperledger/fabric/internal/test"
	"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/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

func NewProposedTransaction(signingIdentity *nwo.SigningIdentity, channelName, chaincodeName, transactionName string, transientData map[string][]byte, args ...[]byte) (*peer.SignedProposal, string) {
	proposal, transactionID := newProposalProto(signingIdentity, channelName, chaincodeName, transactionName, transientData, args...)
	signedProposal, err := protoutil.GetSignedProposal(proposal, signingIdentity)
	Expect(err).NotTo(HaveOccurred())

	return signedProposal, transactionID
}

func newProposalProto(signingIdentity *nwo.SigningIdentity, channelName, chaincodeName, transactionName string, transientData map[string][]byte, args ...[]byte) (*peer.Proposal, string) {
	creator, err := signingIdentity.Serialize()
	Expect(err).NotTo(HaveOccurred())

	invocationSpec := &peer.ChaincodeInvocationSpec{
		ChaincodeSpec: &peer.ChaincodeSpec{
			Type:        peer.ChaincodeSpec_NODE,
			ChaincodeId: &peer.ChaincodeID{Name: chaincodeName},
			Input:       &peer.ChaincodeInput{Args: chaincodeArgs(transactionName, args...)},
		},
	}

	result, transactionID, err := protoutil.CreateChaincodeProposalWithTransient(
		common.HeaderType_ENDORSER_TRANSACTION,
		channelName,
		invocationSpec,
		creator,
		transientData,
	)
	Expect(err).NotTo(HaveOccurred())

	return result, transactionID
}

func chaincodeArgs(transactionName string, args ...[]byte) [][]byte {
	result := make([][]byte, len(args)+1)

	result[0] = []byte(transactionName)
	copy(result[1:], args)

	return result
}

var _ = Describe("GatewayService basic", func() {
	var (
		testDir                     string
		network                     *nwo.Network
		org1Peer0                   *nwo.Peer
		ordererRunner               *ginkgomon.Runner
		ordererProcess, peerProcess ifrit.Process
		conn                        *grpc.ClientConn
		gatewayClient               gateway.GatewayClient
		ctx                         context.Context
		cancel                      context.CancelFunc
		signingIdentity             *nwo.SigningIdentity
	)

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

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

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

		network.GenerateConfigTree()
		network.Bootstrap()

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

		By("setting up the channel")
		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")...,
		)

		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":[]}`,
			SignaturePolicy: `AND ('Org1MSP.peer')`,
			Sequence:        "1",
			InitRequired:    false,
			Label:           "gatewaycc_label",
		}

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

		org1Peer0 = network.Peer("Org1", "peer0")

		conn = network.PeerClientConn(org1Peer0)
		gatewayClient = gateway.NewGatewayClient(conn)
		ctx, cancel = context.WithTimeout(context.Background(), network.EventuallyTimeout)

		signingIdentity = network.PeerUserSigner(org1Peer0, "User1")
	})

	AfterEach(func() {
		conn.Close()
		cancel()

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

	submitTransaction := func(transactionName string, args ...[]byte) (*peer.Response, string) {
		proposedTransaction, transactionID := NewProposedTransaction(signingIdentity, "testchannel", "gatewaycc", transactionName, nil, args...)

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

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

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

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

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

		return chaincodeAction.GetResponse(), transactionID
	}

	commitStatus := func(transactionID string, identity func() ([]byte, error), sign func(msg []byte) ([]byte, error)) (*gateway.CommitStatusResponse, error) {
		idBytes, err := identity()
		Expect(err).NotTo(HaveOccurred())

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

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

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

		return gatewayClient.CommitStatus(ctx, signedStatusRequest)
	}

	chaincodeEvents := func(
		ctx context.Context,
		startPosition *orderer.SeekPosition,
		afterTxID string,
		identity func() ([]byte, error),
		sign func(msg []byte) ([]byte, error),
	) (gateway.Gateway_ChaincodeEventsClient, error) {
		identityBytes, err := identity()
		Expect(err).NotTo(HaveOccurred())

		request := &gateway.ChaincodeEventsRequest{
			ChannelId:   "testchannel",
			ChaincodeId: "gatewaycc",
			Identity:    identityBytes,
		}
		if startPosition != nil {
			request.StartPosition = startPosition
		}
		if len(afterTxID) > 0 {
			request.AfterTransactionId = afterTxID
		}

		requestBytes, err := proto.Marshal(request)
		Expect(err).NotTo(HaveOccurred())

		signature, err := sign(requestBytes)
		Expect(err).NotTo(HaveOccurred())

		signedRequest := &gateway.SignedChaincodeEventsRequest{
			Request:   requestBytes,
			Signature: signature,
		}

		return gatewayClient.ChaincodeEvents(ctx, signedRequest)
	}

	Describe("Evaluate", func() {
		It("should respond with the expected result", func() {
			proposedTransaction, transactionID := NewProposedTransaction(signingIdentity, "testchannel", "gatewaycc", "respond", nil, []byte("200"), []byte("conga message"), []byte("conga payload"))

			request := &gateway.EvaluateRequest{
				TransactionId:       transactionID,
				ChannelId:           "testchannel",
				ProposedTransaction: proposedTransaction,
			}

			response, err := gatewayClient.Evaluate(ctx, request)
			Expect(err).NotTo(HaveOccurred())
			expectedResponse := &gateway.EvaluateResponse{
				Result: &peer.Response{
					Status:  200,
					Message: "conga message",
					Payload: []byte("conga payload"),
				},
			}
			Expect(response.Result.Payload).To(Equal(expectedResponse.Result.Payload))
			Expect(response).To(ProtoEqual(expectedResponse))
		})

		It("should respond with system chaincode result", func() {
			proposedTransaction, transactionID := NewProposedTransaction(signingIdentity, "testchannel", "qscc", "GetChainInfo", nil, []byte("testchannel"))

			request := &gateway.EvaluateRequest{
				TransactionId:       transactionID,
				ChannelId:           "testchannel",
				ProposedTransaction: proposedTransaction,
			}

			response, err := gatewayClient.Evaluate(ctx, request)
			Expect(err).NotTo(HaveOccurred())

			status := common.Status(response.GetResult().GetStatus())
			Expect(status).To(Equal(common.Status_SUCCESS))

			blockchainInfo := new(common.BlockchainInfo)
			Expect(proto.Unmarshal(response.GetResult().GetPayload(), blockchainInfo)).NotTo(HaveOccurred())
		})
	})

	Describe("Submit", func() {
		It("should respond with the expected result", func() {
			result, _ := submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			expectedResult := &peer.Response{
				Status:  200,
				Message: "conga message",
				Payload: []byte("conga payload"),
			}
			Expect(result.Payload).To(Equal(expectedResult.Payload))
			Expect(result).To(ProtoEqual(expectedResult))
		})

		It("should endorse a system chaincode transaction", func() {
			arg, err := proto.Marshal(&lifecycle.QueryInstalledChaincodesArgs{})
			Expect(err).NotTo(HaveOccurred())
			adminSigner := network.PeerUserSigner(org1Peer0, "Admin")
			proposedTransaction, transactionID := NewProposedTransaction(adminSigner, "testchannel", "_lifecycle", "QueryInstalledChaincodes", nil, arg)

			request := &gateway.EndorseRequest{
				TransactionId:          transactionID,
				ChannelId:              "testchannel",
				ProposedTransaction:    proposedTransaction,
				EndorsingOrganizations: []string{adminSigner.MSPID}, // Only use peers for our admin ID org
			}

			response, err := gatewayClient.Endorse(ctx, request)
			Expect(err).NotTo(HaveOccurred())

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

			queryResult := new(lifecycle.QueryInstalledChaincodesResult)
			Expect(proto.Unmarshal(chaincodeAction.GetResponse().GetPayload(), queryResult)).NotTo(HaveOccurred())
		})
	})

	Describe("CommitStatus", func() {
		It("should respond with status of submitted transaction", func() {
			_, transactionID := submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			statusResult, err := commitStatus(transactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			Expect(statusResult.Result).To(Equal(peer.TxValidationCode_VALID))
		})

		It("should respond with block number", func() {
			_, transactionID := submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			firstStatus, err := commitStatus(transactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			_, transactionID = submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			nextStatus, err := commitStatus(transactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			Expect(nextStatus.BlockNumber).To(Equal(firstStatus.BlockNumber + 1))
		})

		It("should fail on unauthorized identity", func() {
			_, transactionID := submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			badIdentity := network.OrdererUserSigner(network.Orderer("orderer"), "Admin")
			_, err := commitStatus(transactionID, badIdentity.Serialize, signingIdentity.Sign)
			Expect(err).To(HaveOccurred())

			grpcErr, _ := status.FromError(err)
			Expect(grpcErr.Code()).To(Equal(codes.PermissionDenied))
		})

		It("should fail on bad signature", func() {
			_, transactionID := submitTransaction("respond", []byte("200"), []byte("conga message"), []byte("conga payload"))
			badSign := func(digest []byte) ([]byte, error) {
				return signingIdentity.Sign([]byte("WRONG"))
			}
			_, err := commitStatus(transactionID, signingIdentity.Serialize, badSign)
			Expect(err).To(HaveOccurred())

			grpcErr, _ := status.FromError(err)
			Expect(grpcErr.Code()).To(Equal(codes.PermissionDenied))
		})
	})

	Describe("ChaincodeEvents", func() {
		It("should respond with emitted chaincode events", func() {
			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			startPosition := &orderer.SeekPosition{
				Type: &orderer.SeekPosition_NextCommit{
					NextCommit: &orderer.SeekNextCommit{},
				},
			}

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, "", signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			_, transactionID := submitTransaction("event", []byte("EVENT_NAME"), []byte("EVENT_PAYLOAD"))

			event, err := eventsClient.Recv()
			Expect(err).NotTo(HaveOccurred())

			Expect(event.Events).To(HaveLen(1), "number of events")
			expectedEvent := &peer.ChaincodeEvent{
				ChaincodeId: "gatewaycc",
				TxId:        transactionID,
				EventName:   "EVENT_NAME",
				Payload:     []byte("EVENT_PAYLOAD"),
			}
			Expect(event.Events[0]).To(ProtoEqual(expectedEvent))
		})

		It("should respond with replayed chaincode events", func() {
			_, transactionID := submitTransaction("event", []byte("EVENT_NAME"), []byte("EVENT_PAYLOAD"))
			statusResult, err := commitStatus(transactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			startPosition := &orderer.SeekPosition{
				Type: &orderer.SeekPosition_Specified{
					Specified: &orderer.SeekSpecified{
						Number: statusResult.BlockNumber,
					},
				},
			}

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, "", signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			event, err := eventsClient.Recv()
			Expect(err).NotTo(HaveOccurred())

			Expect(event.BlockNumber).To(Equal(statusResult.BlockNumber), "block number")
			Expect(event.Events).To(HaveLen(1), "number of events")
			expectedEvent := &peer.ChaincodeEvent{
				ChaincodeId: "gatewaycc",
				TxId:        transactionID,
				EventName:   "EVENT_NAME",
				Payload:     []byte("EVENT_PAYLOAD"),
			}
			Expect(event.Events[0]).To(ProtoEqual(expectedEvent))
		})

		It("should respond with replayed chaincode events after specified transaction ID", func() {
			_, afterTransactionID := submitTransaction("event", []byte("WRONG_EVENT_NAME"), []byte("WRONG_EVENT_PAYLOAD"))
			_, nextTransactionID := submitTransaction("event", []byte("CORRECT_EVENT_NAME"), []byte("CORRECT_EVENT_PAYLOAD"))

			statusResult, err := commitStatus(afterTransactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			_, err = commitStatus(nextTransactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			startPosition := &orderer.SeekPosition{
				Type: &orderer.SeekPosition_Specified{
					Specified: &orderer.SeekSpecified{
						Number: statusResult.BlockNumber,
					},
				},
			}

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, afterTransactionID, signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			event, err := eventsClient.Recv()
			Expect(err).NotTo(HaveOccurred())

			Expect(event.Events).To(HaveLen(1), "number of events")
			expectedEvent := &peer.ChaincodeEvent{
				ChaincodeId: "gatewaycc",
				TxId:        nextTransactionID,
				EventName:   "CORRECT_EVENT_NAME",
				Payload:     []byte("CORRECT_EVENT_PAYLOAD"),
			}
			Expect(event.Events[0]).To(ProtoEqual(expectedEvent))
		})

		It("should default to next commit if start position not specified", func() {
			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			var startPosition *orderer.SeekPosition

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, "", signingIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			_, transactionID := submitTransaction("event", []byte("EVENT_NAME"), []byte("EVENT_PAYLOAD"))

			event, err := eventsClient.Recv()
			Expect(err).NotTo(HaveOccurred())

			Expect(event.Events).To(HaveLen(1), "number of events")
			expectedEvent := &peer.ChaincodeEvent{
				ChaincodeId: "gatewaycc",
				TxId:        transactionID,
				EventName:   "EVENT_NAME",
				Payload:     []byte("EVENT_PAYLOAD"),
			}
			Expect(event.Events[0]).To(ProtoEqual(expectedEvent))
		})

		It("should fail on unauthorized identity", func() {
			badIdentity := network.OrdererUserSigner(network.Orderer("orderer"), "Admin")

			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			startPosition := &orderer.SeekPosition{
				Type: &orderer.SeekPosition_NextCommit{
					NextCommit: &orderer.SeekNextCommit{},
				},
			}

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, "", badIdentity.Serialize, signingIdentity.Sign)
			Expect(err).NotTo(HaveOccurred())

			event, err := eventsClient.Recv()
			Expect(err).To(HaveOccurred(), "expected error but got event: %v", event)

			grpcErr, _ := status.FromError(err)
			Expect(grpcErr.Code()).To(Equal(codes.PermissionDenied))
		})

		It("should fail on bad signature", func() {
			badSign := func(digest []byte) ([]byte, error) {
				return signingIdentity.Sign([]byte("WRONG"))
			}

			eventCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
			defer cancel()

			startPosition := &orderer.SeekPosition{
				Type: &orderer.SeekPosition_NextCommit{
					NextCommit: &orderer.SeekNextCommit{},
				},
			}

			eventsClient, err := chaincodeEvents(eventCtx, startPosition, "", signingIdentity.Serialize, badSign)
			Expect(err).NotTo(HaveOccurred())

			event, err := eventsClient.Recv()
			Expect(err).To(HaveOccurred(), "expected error but got event: %v", event)

			grpcErr, _ := status.FromError(err)
			Expect(grpcErr.Code()).To(Equal(codes.PermissionDenied))
		})
	})
})
