/*
Copyright 2022 IBM All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

import (
	"math"
	"strings"
	"testing"

	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
	"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
	"github.com/hyperledger/fabric-protos-go-apiv2/msp"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/stretchr/testify/require"
)

type readT struct {
	namespace string
	key       string
	block     uint64
}

type writeT struct {
	namespace string
	key       string
	value     []byte
}

type metaWriteT struct {
	namespace string
	key       string
	name      string
	value     []byte
}

type pvtCollectionT struct {
	namespace  string
	collection string
	hash       []byte
}

type responseT struct {
	status  int32
	message string
	payload []byte
}

type eventT struct {
	namespace string
	name      string
	payload   []byte
	txid      string
}

func TestPayloadDifferenceReadVersion(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
			{namespace: "ns1", key: "key2", block: 4},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
			{namespace: "ns1", key: "key2", block: 5},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "read value mismatch", "namespace", "ns1", "key", "key2", "initial-endorser-value", "4", "invoked-endorser-value", "5"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceReadMissing(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
			{namespace: "ns1", key: "key2", block: 4},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "missing read", "namespace", "ns1", "key", "key2", "initial-endorser-value", "4", "invoked-endorser-value", "0"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceReadExtra(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
			{namespace: "ns1", key: "key2", block: 5},
			{namespace: "ns2", key: "key1b", block: 4},
			{namespace: "ns2", key: "key2b", block: 5},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
			{namespace: "ns1", key: "key2", block: 5},
			{namespace: "ns1", key: "key3", block: 3},
			{namespace: "ns2", key: "key1b", block: 4},
			{namespace: "ns2", key: "key2b", block: 5},
			{namespace: "ns2", key: "key3b", block: 5},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "extraneous read", "namespace", "ns1", "key", "key3", "initial-endorser-value", "0", "invoked-endorser-value", "3"},
		{"type", "extraneous read", "namespace", "ns2", "key", "key3b", "initial-endorser-value", "0", "invoked-endorser-value", "5"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceReadMissingProtos(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: math.MaxInt64},
			{namespace: "ns1"},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{
			{namespace: "ns1", key: "key1", block: 4},
		},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "extraneous read", "namespace", "ns1", "key", "key1", "initial-endorser-value", "0", "invoked-endorser-value", "4"},
		{"type", "extraneous read", "namespace", "ns1", "key", "", "initial-endorser-value", "0", "invoked-endorser-value", "0"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceWriteValue(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{
			{namespace: "ns1", key: "key1", value: []byte("value1")},
			{namespace: "ns1", key: "key2", value: []byte("value2")},
			{namespace: "ns1", key: "key3", value: []byte("value3")},
		},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{
			{namespace: "ns1", key: "key1", value: []byte("value1")},
			{namespace: "ns1", key: "key2", value: []byte("value3")},
			{namespace: "ns1", key: "key4", value: []byte("value4")},
		},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "write value mismatch", "namespace", "ns1", "key", "key2", "initial-endorser-value", "value2", "invoked-endorser-value", "value3"},
		{"type", "missing write", "namespace", "ns1", "key", "key3", "initial-endorser-value", "value3", "invoked-endorser-value", ""},
		{"type", "extraneous write", "namespace", "ns1", "key", "key4", "initial-endorser-value", "", "invoked-endorser-value", "value4"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceMetadata(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{
			{namespace: "ns1", key: "key1", name: "meta1", value: []byte("value1")},
			{namespace: "ns2", key: "key2", name: "meta2", value: []byte("mv1")},
		},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{
			{namespace: "ns1", key: "key1", name: "meta1", value: []byte("value2")},
			{namespace: "ns3", key: "key2", name: "meta2", value: []byte("mv1")},
		},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "write metadata mismatch", "namespace", "ns1", "key", "key1", "name", "meta1", "initial-endorser-value", "value1", "invoked-endorser-value", "value2"},
		{"type", "missing metadata write", "namespace", "ns2", "key", "key2", "name", "meta2", "initial-endorser-value", "mv1", "invoked-endorser-value", ""},
		{"type", "extraneous metadata write", "namespace", "ns3", "key", "key2", "name", "meta2", "initial-endorser-value", "", "invoked-endorser-value", "mv1"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceSBEPolicy(t *testing.T) {
	sbe1 := &common.SignaturePolicyEnvelope{
		Rule: &common.SignaturePolicy{
			Type: &common.SignaturePolicy_NOutOf_{
				NOutOf: &common.SignaturePolicy_NOutOf{
					N: 1,
					Rules: []*common.SignaturePolicy{
						{Type: &common.SignaturePolicy_SignedBy{SignedBy: 0}},
					},
				},
			},
		},
		Identities: []*msp.MSPPrincipal{
			{Principal: []byte("orgA")},
		},
	}

	sbe2 := &common.SignaturePolicyEnvelope{
		Rule: &common.SignaturePolicy{
			Type: &common.SignaturePolicy_NOutOf_{
				NOutOf: &common.SignaturePolicy_NOutOf{
					N: 1,
					Rules: []*common.SignaturePolicy{
						{Type: &common.SignaturePolicy_SignedBy{SignedBy: 0}},
					},
				},
			},
		},
		Identities: []*msp.MSPPrincipal{
			{Principal: []byte("orgB")},
		},
	}

	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{
			{namespace: "ns1", key: "key1", name: "VALIDATION_PARAMETER", value: marshal(sbe1, t)},
			{namespace: "ns1", key: "key2", name: "VALIDATION_PARAMETER", value: marshal(sbe1, t)},
		},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{
			{namespace: "ns1", key: "key1", name: "VALIDATION_PARAMETER", value: marshal(sbe2, t)},
		},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "write metadata mismatch (SBE policy)", "namespace", "ns1", "key", "key1", "name", "VALIDATION_PARAMETER", "initial-endorser-value", "rule:{n_out_of:{n:1  rules:{signed_by:0}}}  identities:{principal:\"orgA\"}", "invoked-endorser-value", "rule:{n_out_of:{n:1  rules:{signed_by:0}}}  identities:{principal:\"orgB\"}"},
		{"type", "missing metadata write (SBE policy)", "namespace", "ns1", "key", "key2", "name", "VALIDATION_PARAMETER", "initial-endorser-value", "rule:{n_out_of:{n:1  rules:{signed_by:0}}}  identities:{principal:\"orgA\"}", "invoked-endorser-value", ""},
	}
	dif := diff.details()
	for i := range expected {
		for j := range expected[i] {
			expected[i][j] = strings.ReplaceAll(expected[i][j].(string), " ", "")
			dif[i][j] = strings.ReplaceAll(dif[i][j].(string), " ", "")
		}
	}
	require.ElementsMatch(t, expected, dif)
}

func TestPayloadDifferenceChaincodeResponse(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value1"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value2"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "chaincode response mismatch", "initial-endorser-response", "status: 200, message: no error, payload: my_value1", "invoked-endorser-response", "status: 200, message: no error, payload: my_value2"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferencePrivateData(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{
			{namespace: "ns1", key: "key1", value: []byte("value1")},
		},
		[]*metaWriteT{},
		[]*pvtCollectionT{
			{namespace: "ns1", collection: "collection1", hash: []byte{1, 2, 3}},
		},
		nil,
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{
			{namespace: "ns1", key: "key1", value: []byte("value1")},
		},
		[]*metaWriteT{},
		[]*pvtCollectionT{
			{namespace: "ns1", collection: "collection1", hash: []byte{4, 5, 6}},
		},
		nil,
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "private collection hash mismatch", "namespace", "ns1", "collection", "collection1", "initial-endorser-hash", "010203", "invoked-endorser-hash", "040506"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func TestPayloadDifferenceEvent(t *testing.T) {
	rpl1 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		&eventT{namespace: "ns1", name: "my_event", payload: []byte("my event payload 1")},
	)

	rpl2 := createProposalResponsePayload(
		t, &responseT{payload: []byte("my_value"), status: 200, message: "no error"},
		[]*readT{},
		[]*writeT{},
		[]*metaWriteT{},
		[]*pvtCollectionT{},
		&eventT{namespace: "ns1", name: "my_event", payload: []byte("my event payload 2")},
	)

	rpl1Bytes := marshal(rpl1, t)
	rpl2Bytes := marshal(rpl2, t)

	diff, err := payloadDifference(rpl1Bytes, rpl2Bytes)
	require.NoError(t, err)

	expected := [][]interface{}{
		{"type", "chaincode event mismatch", "initial-endorser-event", "chaincodeId: ns1, name: my_event, value: my event payload 1", "invoked-endorser-event", "chaincodeId: ns1, name: my_event, value: my event payload 2"},
	}
	require.ElementsMatch(t, expected, diff.details())
}

func createProposalResponsePayload(t *testing.T, response *responseT, reads []*readT, writes []*writeT, metaWrites []*metaWriteT, pvtData []*pvtCollectionT, event *eventT) *peer.ProposalResponsePayload {
	resp := &peer.Response{
		Status:  response.status,
		Payload: response.payload,
		Message: response.message,
	}

	rwset := &rwset.TxReadWriteSet{
		DataModel: rwset.TxReadWriteSet_KV,
		NsRwset:   collateReadWriteSets(t, reads, writes, metaWrites, pvtData),
	}

	action := &peer.ChaincodeAction{
		Response: resp,
		Results:  marshal(rwset, t),
	}

	if event != nil {
		ccEvent := &peer.ChaincodeEvent{
			ChaincodeId: event.namespace,
			TxId:        event.txid,
			EventName:   event.name,
			Payload:     event.payload,
		}

		action.Events = marshal(ccEvent, t)
	}

	payload := &peer.ProposalResponsePayload{
		ProposalHash: []byte{},
		Extension:    marshal(action, t),
	}

	return payload
}

func collateReadWriteSets(t *testing.T, reads []*readT, writes []*writeT, metaWrites []*metaWriteT, pvtData []*pvtCollectionT) []*rwset.NsReadWriteSet {
	grouped := map[string]*kvrwset.KVRWSet{}
	collections := map[string][]*rwset.CollectionHashedReadWriteSet{}

	for _, r := range reads {
		rwset := grouped[r.namespace]
		if rwset == nil {
			rwset = &kvrwset.KVRWSet{}
			grouped[r.namespace] = rwset
		}
		if r.key == "" && r.block == 0 { // signifies nil Read proto in these tests
			rwset.Reads = append(rwset.Reads, nil)
			continue
		}
		var version *kvrwset.Version
		if r.block != math.MaxInt64 { // signifies missing version in these tests
			version = &kvrwset.Version{BlockNum: r.block}
		}
		rwset.Reads = append(rwset.Reads, &kvrwset.KVRead{
			Key:     r.key,
			Version: version,
		})
	}
	for _, w := range writes {
		rwset := grouped[w.namespace]
		if rwset == nil {
			rwset = &kvrwset.KVRWSet{}
			grouped[w.namespace] = rwset
		}
		rwset.Writes = append(rwset.Writes, &kvrwset.KVWrite{
			Key:   w.key,
			Value: w.value,
		})
	}
	for _, mw := range metaWrites {
		rwset := grouped[mw.namespace]
		if rwset == nil {
			rwset = &kvrwset.KVRWSet{}
			grouped[mw.namespace] = rwset
		}
		rwset.MetadataWrites = append(rwset.MetadataWrites, &kvrwset.KVMetadataWrite{
			Key:     mw.key,
			Entries: []*kvrwset.KVMetadataEntry{{Name: mw.name, Value: mw.value}}, // support single entry for now
		})
	}

	for _, pd := range pvtData {
		collections[pd.namespace] = append(collections[pd.namespace], &rwset.CollectionHashedReadWriteSet{
			CollectionName: pd.collection,
			PvtRwsetHash:   pd.hash,
		})
	}

	var rwsets []*rwset.NsReadWriteSet
	for ns, rws := range grouped {
		rwsets = append(rwsets, &rwset.NsReadWriteSet{
			Namespace:             ns,
			Rwset:                 marshal(rws, t),
			CollectionHashedRwset: collections[ns],
		})
	}
	return rwsets
}
