/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package chaincode_test

import (
	"encoding/json"

	"github.com/golang/protobuf/proto"
	pb "github.com/hyperledger/fabric-protos-go/peer"
	lb "github.com/hyperledger/fabric-protos-go/peer/lifecycle"
	"github.com/hyperledger/fabric/bccsp/sw"
	"github.com/hyperledger/fabric/internal/peer/lifecycle/chaincode"
	"github.com/hyperledger/fabric/internal/peer/lifecycle/chaincode/mock"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
)

var _ = Describe("CheckCommitReadiness", func() {
	Describe("CommitReadinessChecker", func() {
		var (
			mockProposalResponse   *pb.ProposalResponse
			mockEndorserClient     *mock.EndorserClient
			mockSigner             *mock.Signer
			input                  *chaincode.CommitReadinessCheckInput
			commitReadinessChecker *chaincode.CommitReadinessChecker
		)

		BeforeEach(func() {
			mockEndorserClient = &mock.EndorserClient{}
			mockResult := &lb.CheckCommitReadinessResult{
				Approvals: map[string]bool{
					"seemsfinetome":  true,
					"well...ok":      true,
					"absolutely-not": false,
				},
			}
			mockResultBytes, err := proto.Marshal(mockResult)
			Expect(err).NotTo(HaveOccurred())
			Expect(mockResultBytes).NotTo(BeNil())
			mockProposalResponse = &pb.ProposalResponse{
				Response: &pb.Response{
					Status:  200,
					Payload: mockResultBytes,
				},
				Endorsement: &pb.Endorsement{},
			}
			mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)

			input = &chaincode.CommitReadinessCheckInput{
				ChannelID: "testchannel",
				Name:      "testcc",
				Version:   "1.0",
				Sequence:  1,
			}

			mockSigner = &mock.Signer{}
			buffer := gbytes.NewBuffer()

			commitReadinessChecker = &chaincode.CommitReadinessChecker{
				Input:          input,
				EndorserClient: mockEndorserClient,
				Signer:         mockSigner,
				Writer:         buffer,
			}
		})

		It("checks whether a chaincode definition is ready to commit and writes the output as human readable plain-text", func() {
			err := commitReadinessChecker.ReadinessCheck()
			Expect(err).NotTo(HaveOccurred())
			Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("Chaincode definition for chaincode 'testcc', version '1.0', sequence '1' on channel 'testchannel' approval status by org"))
			Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("absolutely-not: false"))
			Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("seemsfinetome: true"))
			Eventually(commitReadinessChecker.Writer).Should(gbytes.Say("well...ok: true"))
		})

		Context("when JSON-formatted output is requested", func() {
			BeforeEach(func() {
				commitReadinessChecker.Input.OutputFormat = "json"
			})

			It("checks whether a chaincode definition is ready to commit and writes the output as JSON", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).NotTo(HaveOccurred())
				expectedOutput := &lb.CheckCommitReadinessResult{
					Approvals: map[string]bool{
						"absolutely-not": false,
						"well...ok":      true,
						"seemsfinetome":  true,
					},
				}
				json, err := json.MarshalIndent(expectedOutput, "", "\t")
				Expect(err).NotTo(HaveOccurred())
				Eventually(commitReadinessChecker.Writer).Should(gbytes.Say(string(json)))
			})
		})

		Context("when the channel name is not provided", func() {
			BeforeEach(func() {
				commitReadinessChecker.Input.ChannelID = ""
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("The required parameter 'channelID' is empty. Rerun the command with -C flag"))
			})
		})

		Context("when the chaincode name is not provided", func() {
			BeforeEach(func() {
				commitReadinessChecker.Input.Name = ""
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("The required parameter 'name' is empty. Rerun the command with -n flag"))
			})
		})

		Context("when the chaincode version is not provided", func() {
			BeforeEach(func() {
				commitReadinessChecker.Input.Version = ""
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("The required parameter 'version' is empty. Rerun the command with -v flag"))
			})
		})

		Context("when the sequence is not provided", func() {
			BeforeEach(func() {
				commitReadinessChecker.Input.Sequence = 0
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("The required parameter 'sequence' is empty. Rerun the command with --sequence flag"))
			})
		})

		Context("when the signer cannot be serialized", func() {
			BeforeEach(func() {
				mockSigner.SerializeReturns(nil, errors.New("cafe"))
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("failed to create proposal: failed to serialize identity: cafe"))
			})
		})

		Context("when the signer fails to sign the proposal", func() {
			BeforeEach(func() {
				mockSigner.SignReturns(nil, errors.New("tea"))
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("failed to create signed proposal: tea"))
			})
		})

		Context("when the endorser fails to endorse the proposal", func() {
			BeforeEach(func() {
				mockEndorserClient.ProcessProposalReturns(nil, errors.New("latte"))
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("failed to endorse proposal: latte"))
			})
		})

		Context("when the endorser returns a nil proposal response", func() {
			BeforeEach(func() {
				mockProposalResponse = nil
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("received nil proposal response"))
			})
		})

		Context("when the endorser returns a proposal response with a nil response", func() {
			BeforeEach(func() {
				mockProposalResponse.Response = nil
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("received proposal response with nil response"))
			})
		})

		Context("when the endorser returns a non-success status", func() {
			BeforeEach(func() {
				mockProposalResponse.Response = &pb.Response{
					Status:  500,
					Message: "capuccino",
				}
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError("query failed with status: 500 - capuccino"))
			})
		})

		Context("when the endorser returns an unexpected result", func() {
			BeforeEach(func() {
				mockProposalResponse.Response = &pb.Response{
					Status:  200,
					Payload: []byte("jibberish"),
				}
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

			It("returns an error", func() {
				err := commitReadinessChecker.ReadinessCheck()
				Expect(err).To(MatchError(ContainSubstring("failed to unmarshal proposal response's response payload")))
			})
		})
	})

	Describe("CheckCommitReadinessCmd", func() {
		var checkCommitReadinessCmd *cobra.Command

		BeforeEach(func() {
			cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore())
			Expect(err).To(BeNil())
			checkCommitReadinessCmd = chaincode.CheckCommitReadinessCmd(nil, cryptoProvider)
			checkCommitReadinessCmd.SilenceErrors = true
			checkCommitReadinessCmd.SilenceUsage = true
			checkCommitReadinessCmd.SetArgs([]string{
				"--channelID=testchannel",
				"--name=testcc",
				"--version=testversion",
				"--sequence=1",
				"--peerAddresses=querypeer1",
				"--tlsRootCertFiles=tls1",
				"--signature-policy=AND ('Org1MSP.member','Org2MSP.member')",
			})
		})

		AfterEach(func() {
			chaincode.ResetFlags()
		})

		It("sets up the commit readiness checker and checks whether the chaincode definition is ready to commit", func() {
			err := checkCommitReadinessCmd.Execute()
			Expect(err).To(MatchError(ContainSubstring("failed to retrieve endorser client")))
		})

		Context("when the policy is invalid", func() {
			BeforeEach(func() {
				checkCommitReadinessCmd.SetArgs([]string{
					"--signature-policy=notapolicy",
					"--channelID=testchannel",
					"--name=testcc",
					"--version=testversion",
					"--sequence=1",
					"--peerAddresses=querypeer1",
					"--tlsRootCertFiles=tls1",
				})
			})

			It("returns an error", func() {
				err := checkCommitReadinessCmd.Execute()
				Expect(err).To(MatchError("invalid signature policy: notapolicy"))
			})
		})

		Context("when the collections config is invalid", func() {
			BeforeEach(func() {
				checkCommitReadinessCmd.SetArgs([]string{
					"--collections-config=idontexist.json",
					"--channelID=testchannel",
					"--name=testcc",
					"--version=testversion",
					"--sequence=1",
					"--peerAddresses=querypeer1",
					"--tlsRootCertFiles=tls1",
				})
			})

			It("returns an error", func() {
				err := checkCommitReadinessCmd.Execute()
				Expect(err).To(MatchError("invalid collection configuration in file idontexist.json: could not read file 'idontexist.json': open idontexist.json: no such file or directory"))
			})
		})
	})
})
