/*
Copyright IBM Corp All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package ledger

import (
	"bytes"
	"encoding/json"
	"fmt"
	"path/filepath"
	"syscall"

	"github.com/hyperledger/fabric/integration/nwo"
	"github.com/hyperledger/fabric/integration/nwo/commands"
	"github.com/hyperledger/fabric/integration/runner"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/onsi/gomega/gexec"
	"github.com/tedsuo/ifrit"
)

var _ bool = Describe("all shim APIs for non-private data", func() {
	var (
		setup     *setup
		helper    *marblesTestHelper
		chaincode nwo.Chaincode
	)

	BeforeEach(func() {
		setup = initThreeOrgsSetup()
		helper = &marblesTestHelper{
			networkHelper: &networkHelper{
				Network:   setup.network,
				orderer:   setup.orderer,
				peers:     setup.peers,
				testDir:   setup.testDir,
				channelID: setup.channelID,
			},
		}

		chaincode = nwo.Chaincode{
			Name:            "marbles",
			Version:         "0.0",
			Path:            "github.com/hyperledger/fabric/integration/chaincode/marbles/cmdwithindexspecs",
			Lang:            "golang",
			PackageFile:     filepath.Join(setup.testDir, "marbles.tar.gz"),
			Label:           "marbles",
			SignaturePolicy: `OR ('Org1MSP.member','Org2MSP.member', 'Org3MSP.member')`,
			Sequence:        "1",
		}
	})

	AfterEach(func() {
		setup.cleanup()
	})

	// assertMarbleAPIs invoke marble APIs to add/transfer/delete marbles and get marbles without rich queries.
	// These APIs are applicable to both levelDB and CouchDB.
	assertMarbleAPIs := func(ccName string, peer *nwo.Peer) {
		height := helper.getLedgerHeight(peer)

		By("adding six marbles, marble-0 to marble-5")
		for i := 0; i <= 5; i++ {
			helper.invokeMarblesChaincode(ccName, peer, "initMarble", fmt.Sprintf("marble-%d", i), "blue", "35", "tom")
			helper.waitUntilEqualLedgerHeight(height + i + 1)
		}

		By("getting marbles by range")
		expectedQueryResult := newMarbleQueryResult(1, 4, "blue", 35, "tom")
		helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "getMarblesByRange", "marble-1", "marble-5")

		By("transferring marble-0 to jerry")
		helper.invokeMarblesChaincode(ccName, peer, "transferMarble", "marble-0", "jerry")

		By("verifying new owner of marble-0 after transfer")
		expectedResult := newMarble("marble-0", "blue", 35, "jerry")
		helper.assertMarbleExists(ccName, peer, expectedResult, "marble-0")

		By("deleting marble-0")
		helper.invokeMarblesChaincode(ccName, peer, "delete", "marble-0")

		By("verifying deletion of marble-0")
		helper.assertMarbleDoesNotExist(peer, ccName, "marble-0")

		By("transferring marbles by color")
		helper.invokeMarblesChaincode(ccName, peer, "transferMarblesBasedOnColor", "blue", "jerry")

		By("verifying new owner after transfer by color")
		for i := 1; i <= 5; i++ {
			name := fmt.Sprintf("marble-%d", i)
			expectedResult = newMarble(name, "blue", 35, "jerry")
			helper.assertMarbleExists(ccName, peer, expectedResult, name)
		}

		By("getting history for marble-0")
		expectedHistoryResult := []*marbleHistoryResult{
			{IsDelete: "true"},
			{IsDelete: "false", Value: newMarble("marble-0", "blue", 35, "jerry")},
			{IsDelete: "false", Value: newMarble("marble-0", "blue", 35, "tom")},
		}
		helper.assertGetHistoryForMarble(ccName, peer, expectedHistoryResult, "marble-0")
	}

	// assertMarbleAPIs verifies marbles APIs using rich queries and pagination that are only applicable to CouchDB.
	assertMarbleAPIsRichQueries := func(ccName string, peer *nwo.Peer) {
		By("querying marbles by owner")
		expectedQueryResult := newMarbleQueryResult(1, 5, "blue", 35, "jerry")
		helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "queryMarblesByOwner", "jerry")

		By("quering marbles by search criteria")
		helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "queryMarbles", `{"selector":{"color":"blue"}}`)

		By("quering marbles by range with pagination size 3, 1st call")
		bookmark := ""
		expectedQueryResult = newMarbleQueryResult(1, 3, "blue", 35, "jerry")
		bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
			"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)

		By("quering marbles by range with pagination size 3, 2nd call")
		expectedQueryResult = newMarbleQueryResult(4, 5, "blue", 35, "jerry")
		bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
			"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)

		By("quering marbles by range with pagination size 3, 3rd call should return no marble")
		expectedQueryResult = make([]*marbleQueryResult, 0)
		helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
			"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)

		By("quering marbles by search criteria with pagination size 10, 1st call")
		bookmark = ""
		expectedQueryResult = newMarbleQueryResult(1, 5, "blue", 35, "jerry")
		bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
			"queryMarblesWithPagination", `{"selector":{"owner":"jerry"}}`, "10", bookmark)

		By("quering marbles by search criteria with pagination size 10, 2nd call should return no marble")
		expectedQueryResult = make([]*marbleQueryResult, 0)
		helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
			"queryMarblesWithPagination", `{"selector":{"owner":"jerry"}}`, "10", bookmark)
	}

	When("levelDB is used as stateDB", func() {
		It("calls marbles APIs", func() {
			peer := setup.network.Peer("org2", "peer0")

			By("deploying new lifecycle chaincode")
			nwo.EnableCapabilities(setup.network, setup.channelID, "Application", "V2_0", setup.orderer, setup.peers...)
			helper.deployChaincode(chaincode)

			By("verifying marbles chaincode APIs")
			assertMarbleAPIs(chaincode.Name, peer)
		})
	})

	When("CouchDB is used as stateDB", func() {
		var (
			couchProcess ifrit.Process
		)

		BeforeEach(func() {
			By("stopping peers")
			setup.stopPeers()

			By("configuring a peer with couchdb")
			// configure only one of the peers (org2, peer0) to use couchdb.
			// Note that we do not support a channel with mixed DBs.
			// However, for testing, it would be fine to use couchdb for one
			// peer and sending all the couchdb related test queries to this peer
			couchDB := &runner.CouchDB{}
			couchProcess = ifrit.Invoke(couchDB)
			Eventually(couchProcess.Ready(), runner.DefaultStartTimeout).Should(BeClosed())
			Consistently(couchProcess.Wait()).ShouldNot(Receive())
			couchAddr := couchDB.Address()
			peer := setup.network.Peer("org2", "peer0")
			core := setup.network.ReadPeerConfig(peer)
			core.Ledger.State.StateDatabase = "CouchDB"
			core.Ledger.State.CouchDBConfig.CouchDBAddress = couchAddr
			setup.network.WritePeerConfig(peer, core)

			By("restarting peers with couchDB")
			setup.startPeers()
		})

		AfterEach(func() {
			couchProcess.Signal(syscall.SIGTERM)
			Eventually(couchProcess.Wait(), setup.network.EventuallyTimeout).Should(Receive())
		})

		It("calls marbles APIs", func() {
			peer := setup.network.Peer("org2", "peer0")

			By("deploying new lifecycle chaincode")
			nwo.EnableCapabilities(setup.network, setup.channelID, "Application", "V2_0", setup.orderer, setup.peers...)
			helper.deployChaincode(chaincode)

			By("verifying marbles chaincode APIs")
			assertMarbleAPIs(chaincode.Name, peer)

			By("verifying marbles rich queries")
			assertMarbleAPIsRichQueries(chaincode.Name, peer)
		})
	})
})

// marble is the struct to unmarshal the response bytes returned from getMarble API
type marble struct {
	ObjectType string `json:"docType"` //docType is "marble"
	Name       string `json:"name"`
	Color      string `json:"color"`
	Size       int    `json:"size"`
	Owner      string `json:"owner"`
}

// marbleQueryResult is the struct to unmarshal the response bytes returned from marbles query APIs
type marbleQueryResult struct {
	Key    string  `json:"Key"`
	Record *marble `json:"Record"`
}

type metadata struct {
	RecordsCount string `json:"RecordsCount"`
	Bookmark     string `json:"Bookmark"`
}

// marbleQueryResult is the struct to unmarshal the metadata bytes returned from marbles pagination APIs
type paginationMetadata struct {
	ResponseMetadata *metadata `json:"ResponseMetadata"`
}

// marbleHistoryResult is the struct to unmarshal the response bytes returned from marbles history API
type marbleHistoryResult struct {
	TxId      string  `json:"TxId"`
	Value     *marble `json:"Value"`
	Timestamp string  `json:"Timestamp"`
	IsDelete  string  `json:"IsDelete"`
}

// newMarble creates a marble object for the given parameters
func newMarble(name, color string, size int, owner string) *marble {
	return &marble{"marble", name, color, size, owner}
}

// newMarbleQueryResult creates a slice of marbleQueryResult for the marbles based on startIndex and endIndex.
// Both startIndex and endIndex are inclusive
func newMarbleQueryResult(startIndex, endIndex int, color string, size int, owner string) []*marbleQueryResult {
	expectedResult := make([]*marbleQueryResult, 0)
	for i := startIndex; i <= endIndex; i++ {
		name := fmt.Sprintf("marble-%d", i)
		item := marbleQueryResult{Key: name, Record: newMarble(name, color, size, owner)}
		expectedResult = append(expectedResult, &item)
	}
	return expectedResult
}

// marblesTestHelper implements helper methods to call marbles chaincode APIs and verify results
type marblesTestHelper struct {
	*networkHelper
}

// invokeMarblesChaincode invokes marbles APIs such as initMarble, transfer and delete.
func (th *marblesTestHelper) invokeMarblesChaincode(chaincodeName string, peer *nwo.Peer, funcAndArgs ...string) {
	command := commands.ChaincodeInvoke{
		ChannelID: th.channelID,
		Orderer:   th.OrdererAddress(th.orderer, nwo.ListenPort),
		Name:      chaincodeName,
		Ctor:      prepareChaincodeInvokeArgs(funcAndArgs...),
		PeerAddresses: []string{
			th.PeerAddress(peer, nwo.ListenPort),
		},
		WaitForEvent: true,
	}
	th.invokeChaincode(peer, command)
	nwo.WaitUntilEqualLedgerHeight(th.Network, th.channelID, nwo.GetLedgerHeight(th.Network, peer, th.channelID), th.peers...)
}

// assertMarbleExists asserts that the marble exists and matches the expected result
func (th *marblesTestHelper) assertMarbleExists(chaincodeName string, peer *nwo.Peer, expectedResult *marble, marbleName string) {
	command := commands.ChaincodeQuery{
		ChannelID: th.channelID,
		Name:      chaincodeName,
		Ctor:      fmt.Sprintf(`{"Args":["readMarble","%s"]}`, marbleName),
	}
	sess, err := th.PeerUserSession(peer, "User1", command)
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
	result := &marble{}
	err = json.Unmarshal(sess.Out.Contents(), result)
	Expect(err).NotTo(HaveOccurred())
	Expect(result).To(Equal(expectedResult))

}

// assertMarbleDoesNotExist asserts that the marble does not exist
func (th *marblesTestHelper) assertMarbleDoesNotExist(peer *nwo.Peer, chaincodeName, marbleName string) {
	command := commands.ChaincodeQuery{
		ChannelID: th.channelID,
		Name:      chaincodeName,
		Ctor:      fmt.Sprintf(`{"Args":["readMarble","%s"]}`, marbleName),
	}
	th.queryChaincode(peer, command, "Marble does not exist", false)
}

// assertQueryMarbles queries the chaincode and verifies the result based on the function and arguments,
// including range queries and rich queries.
func (th *marblesTestHelper) assertQueryMarbles(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleQueryResult, funcAndArgs ...string) {
	command := commands.ChaincodeQuery{
		ChannelID: th.channelID,
		Name:      chaincodeName,
		Ctor:      prepareChaincodeInvokeArgs(funcAndArgs...),
	}
	sess, err := th.PeerUserSession(peer, "User1", command)
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
	results := make([]*marbleQueryResult, 0)
	err = json.Unmarshal(sess.Out.Contents(), &results)
	Expect(err).NotTo(HaveOccurred())
	Expect(results).To(Equal(expectedResult))
}

// assertQueryMarbles queries the chaincode with pagination and verifies the result based on the function and arguments,
// including range queries and rich queries.
func (th *marblesTestHelper) assertQueryMarblesWithPagination(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleQueryResult, funcAndArgs ...string) string {
	command := commands.ChaincodeQuery{
		ChannelID: th.channelID,
		Name:      chaincodeName,
		Ctor:      prepareChaincodeInvokeArgs(funcAndArgs...),
	}
	sess, err := th.PeerUserSession(peer, "User1", command)
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))

	// response bytes contains 2 json arrays: [{"Key":...}][{"ResponseMetadata": ...}]
	responseBytes := sess.Out.Contents()
	index := bytes.LastIndex(responseBytes, []byte("]["))

	// unmarshal and verify response result
	results := make([]*marbleQueryResult, 0)
	err = json.Unmarshal(responseBytes[:index+1], &results)
	Expect(err).NotTo(HaveOccurred())
	Expect(results).To(Equal(expectedResult))

	// unmarshal ResponseMetadata and return bookmark to the caller for next call
	respMetadata := make([]*paginationMetadata, 0)
	err = json.Unmarshal(responseBytes[index+1:], &respMetadata)
	Expect(err).NotTo(HaveOccurred())
	Expect(respMetadata).To(HaveLen(1))

	return respMetadata[0].ResponseMetadata.Bookmark
}

// assertGetHistoryForMarble queries the history for a specific marble and verifies the result
func (th *marblesTestHelper) assertGetHistoryForMarble(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleHistoryResult, marbleName string) {
	command := commands.ChaincodeQuery{
		ChannelID: th.channelID,
		Name:      chaincodeName,
		Ctor:      fmt.Sprintf(`{"Args":["getHistoryForMarble","%s"]}`, marbleName),
	}
	sess, err := th.PeerUserSession(peer, "User1", command)
	Expect(err).NotTo(HaveOccurred())
	Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))

	// unmarshal bytes and verify history
	results := make([]*marbleHistoryResult, 0)
	err = json.Unmarshal(sess.Out.Contents(), &results)
	Expect(err).NotTo(HaveOccurred())
	Expect(results).To(HaveLen(len(expectedResult)))
	for i, result := range results {
		Expect(result.IsDelete).To(Equal(expectedResult[i].IsDelete))
		if result.IsDelete == "true" {
			Expect(result.Value).To(BeNil())
			continue
		}
		Expect(result.Value).To(Equal(expectedResult[i].Value))
	}
}
