/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package chaincode_test

import (
	"encoding/json"
	"fmt"

	"github.com/hyperledger/fabric-lib-go/bccsp/sw"
	pb "github.com/hyperledger/fabric-protos-go-apiv2/peer"
	lb "github.com/hyperledger/fabric-protos-go-apiv2/peer/lifecycle"
	"github.com/hyperledger/fabric/internal/peer/lifecycle/chaincode"
	"github.com/hyperledger/fabric/internal/peer/lifecycle/chaincode/mock"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"google.golang.org/protobuf/proto"
)

var _ = Describe("QueryCommitted", func() {
	Describe("CommittedQuerier", func() {
		var (
			mockProposalResponse *pb.ProposalResponse
			mockEndorserClient   *mock.EndorserClient
			mockSigner           *mock.Signer
			input                *chaincode.CommittedQueryInput
			committedQuerier     *chaincode.CommittedQuerier
		)

		BeforeEach(func() {
			mockResult := &lb.QueryChaincodeDefinitionsResult{
				ChaincodeDefinitions: []*lb.QueryChaincodeDefinitionsResult_ChaincodeDefinition{
					{
						Name:              "woohoo",
						Sequence:          93,
						Version:           "a-version",
						EndorsementPlugin: "e-plugin",
						ValidationPlugin:  "v-plugin",
					},
					{
						Name:              "yahoo",
						Sequence:          20,
						Version:           "another-version",
						EndorsementPlugin: "e-plugin",
						ValidationPlugin:  "v-plugin",
					},
				},
			}

			mockResultBytes, err := proto.Marshal(mockResult)
			Expect(err).NotTo(HaveOccurred())
			Expect(mockResultBytes).NotTo(BeNil())
			mockProposalResponse = &pb.ProposalResponse{
				Response: &pb.Response{
					Status:  200,
					Payload: mockResultBytes,
				},
			}

			mockEndorserClient = &mock.EndorserClient{}
			mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)

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

			input = &chaincode.CommittedQueryInput{
				ChannelID: "test-channel",
			}

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

		It("queries committed chaincodes and writes the output as human readable plain-text", func() {
			err := committedQuerier.Query()
			Expect(err).NotTo(HaveOccurred())
			Eventually(committedQuerier.Writer).Should(gbytes.Say("Committed chaincode definitions on channel 'test-channel':\n"))
			Eventually(committedQuerier.Writer).Should(gbytes.Say("Name: woohoo, Version: a-version, Sequence: 93, Endorsement Plugin: e-plugin, Validation Plugin: v-plugin\n"))
			Eventually(committedQuerier.Writer).Should(gbytes.Say("Name: yahoo, Version: another-version, Sequence: 20, Endorsement Plugin: e-plugin, Validation Plugin: v-plugin\n"))
		})

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

			It("queries committed chaincodes and writes the output as JSON", func() {
				err := committedQuerier.Query()
				Expect(err).NotTo(HaveOccurred())
				expectedOutput := &lb.QueryChaincodeDefinitionsResult{
					ChaincodeDefinitions: []*lb.QueryChaincodeDefinitionsResult_ChaincodeDefinition{
						{
							Name:              "woohoo",
							Sequence:          93,
							Version:           "a-version",
							EndorsementPlugin: "e-plugin",
							ValidationPlugin:  "v-plugin",
						},
						{
							Name:              "yahoo",
							Sequence:          20,
							Version:           "another-version",
							EndorsementPlugin: "e-plugin",
							ValidationPlugin:  "v-plugin",
						},
					},
				}
				json, err := json.MarshalIndent(expectedOutput, "", "\t")
				Expect(err).NotTo(HaveOccurred())
				Eventually(committedQuerier.Writer).Should(gbytes.Say(fmt.Sprintf(`\Q%s\E`, string(json))))
			})
		})

		Context("when a single chaincode definition is requested", func() {
			BeforeEach(func() {
				input.Name = "test-cc"

				mockResult := &lb.QueryChaincodeDefinitionResult{
					Sequence:          93,
					Version:           "a-version",
					EndorsementPlugin: "e-plugin",
					ValidationPlugin:  "v-plugin",
					Approvals: map[string]bool{
						"whatkindoforgisthis": true,
						"nowaydoiapprove":     false,
					},
				}

				mockResultBytes, err := proto.Marshal(mockResult)
				Expect(err).NotTo(HaveOccurred())
				Expect(mockResultBytes).NotTo(BeNil())
				mockProposalResponse = &pb.ProposalResponse{
					Response: &pb.Response{
						Status:  200,
						Payload: mockResultBytes,
					},
				}

				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

			It("queries the committed chaincode and writes the output as human readable plain-text", func() {
				err := committedQuerier.Query()
				Expect(err).NotTo(HaveOccurred())
				Eventually(committedQuerier.Writer).Should(gbytes.Say("Committed chaincode definition for chaincode 'test-cc' on channel 'test-channel'"))
				Eventually(committedQuerier.Writer).Should(gbytes.Say(`\QVersion: a-version, Sequence: 93, Endorsement Plugin: e-plugin, Validation Plugin: v-plugin, Approvals: [nowaydoiapprove: false, whatkindoforgisthis: true]\E`))
			})

			Context("when the payload contains bytes that aren't a QueryChaincodeDefinitionResult", func() {
				BeforeEach(func() {
					mockProposalResponse.Response = &pb.Response{
						Payload: []byte("badpayloadbadpayload"),
						Status:  200,
					}
					mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
				})

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

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

				It("queries the committed chaincodes and writes the output as JSON", func() {
					err := committedQuerier.Query()
					Expect(err).NotTo(HaveOccurred())
					expectedOutput := &lb.QueryChaincodeDefinitionResult{
						Sequence:          93,
						Version:           "a-version",
						EndorsementPlugin: "e-plugin",
						ValidationPlugin:  "v-plugin",
						Approvals: map[string]bool{
							"whatkindoforgisthis": true,
							"nowaydoiapprove":     false,
						},
					}
					json, err := json.MarshalIndent(expectedOutput, "", "\t")
					Expect(err).NotTo(HaveOccurred())
					Eventually(committedQuerier.Writer).Should(gbytes.Say(fmt.Sprintf(`\Q%s\E`, string(json))))
				})
			})
		})

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

			It("returns an error", func() {
				err := committedQuerier.Query()
				Expect(err).To(MatchError("channel name must be specified"))
			})
		})

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

			It("returns an error", func() {
				err := committedQuerier.Query()
				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 := committedQuerier.Query()
				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 := committedQuerier.Query()
				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 := committedQuerier.Query()
				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 := committedQuerier.Query()
				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 := committedQuerier.Query()
				Expect(err).To(MatchError("query failed with status: 500 - capuccino"))
			})
		})

		Context("when the payload contains bytes that aren't a QueryChaincodeDefinitionsResult", func() {
			BeforeEach(func() {
				mockProposalResponse.Response = &pb.Response{
					Payload: []byte("badpayloadbadpayload"),
					Status:  200,
				}
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
			})

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

		Context("when the payload contains bytes that aren't a QueryChaincodeDefinitionsResult and JSON-output is requested", func() {
			BeforeEach(func() {
				mockProposalResponse.Response = &pb.Response{
					Payload: []byte("badpayloadbadpayload"),
					Status:  200,
				}
				mockEndorserClient.ProcessProposalReturns(mockProposalResponse, nil)
				committedQuerier.Input.OutputFormat = "json"
			})

			It("returns an error", func() {
				err := committedQuerier.Query()
				Expect(err).To(MatchError(ContainSubstring("failed to unmarshal proposal response's response payload as type *lifecycle.QueryChaincodeDefinitionsResult")))
			})
		})
	})

	Describe("QueryCommittedCmd", func() {
		var queryCommittedCmd *cobra.Command

		BeforeEach(func() {
			cryptoProvider, err := sw.NewDefaultSecurityLevelWithKeystore(sw.NewDummyKeyStore())
			Expect(err).To(BeNil())
			queryCommittedCmd = chaincode.QueryCommittedCmd(nil, cryptoProvider)
			queryCommittedCmd.SilenceErrors = true
			queryCommittedCmd.SilenceUsage = true
			queryCommittedCmd.SetArgs([]string{
				"--name=testcc",
				"--channelID=testchannel",
				"--peerAddresses=querycommittedpeer1",
				"--tlsRootCertFiles=tls1",
			})
		})

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

		It("attempts to connect to the endorser", func() {
			err := queryCommittedCmd.Execute()
			Expect(err).To(MatchError(ContainSubstring("failed to retrieve endorser client")))
		})

		Context("when more than one peer address is provided", func() {
			BeforeEach(func() {
				queryCommittedCmd.SetArgs([]string{
					"--name=testcc",
					"--channelID=testchannel",
					"--peerAddresses=querycommittedpeer1",
					"--tlsRootCertFiles=tls1",
					"--peerAddresses=querycommittedpeer2",
					"--tlsRootCertFiles=tls2",
				})
			})

			It("returns an error", func() {
				err := queryCommittedCmd.Execute()
				Expect(err).To(MatchError(ContainSubstring("failed to validate peer connection parameters")))
			})
		})
	})
})
