package transactionAPI

import (
	"bytes"
	"encoding/hex"
	"encoding/json"
	"errors"
	"math/big"
	"testing"

	"github.com/ElrondNetwork/elrond-go-core/core"
	"github.com/ElrondNetwork/elrond-go-core/data/receipt"
	"github.com/ElrondNetwork/elrond-go-core/data/smartContractResult"
	"github.com/ElrondNetwork/elrond-go-core/data/transaction"
	"github.com/ElrondNetwork/elrond-go/dataRetriever"
	"github.com/ElrondNetwork/elrond-go/dblookupext"
	"github.com/ElrondNetwork/elrond-go/node/mock"
	"github.com/ElrondNetwork/elrond-go/storage"
	dbLookupExtMock "github.com/ElrondNetwork/elrond-go/testscommon/dblookupext"
	storageStubs "github.com/ElrondNetwork/elrond-go/testscommon/storage"
	"github.com/stretchr/testify/require"
)

func TestPutEventsInTransactionReceipt(t *testing.T) {
	t.Parallel()

	txHash := []byte("txHash")
	receiptHash := []byte("hash")
	rec := &receipt.Receipt{
		TxHash:  txHash,
		Data:    []byte("invalid tx"),
		Value:   big.NewInt(1000),
		SndAddr: []byte("sndAddr"),
	}

	marshalizerdMock := &mock.MarshalizerFake{}
	dataStore := &mock.ChainStorerMock{
		GetStorerCalled: func(unitType dataRetriever.UnitType) storage.Storer {
			return &storageStubs.StorerStub{
				GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) {
					recBytes, _ := json.Marshal(rec)
					return recBytes, nil
				},
			}
		},
	}
	historyRepo := &dbLookupExtMock.HistoryRepositoryStub{
		GetEventsHashesByTxHashCalled: func(hash []byte, epoch uint32) (*dblookupext.ResultsHashesByTxHash, error) {
			return &dblookupext.ResultsHashesByTxHash{
				ReceiptsHash: receiptHash,
			}, nil
		},
	}

	pubKeyConverter := &mock.PubkeyConverterMock{}
	txUnmarshalerAndPreparer := newTransactionUnmarshaller(marshalizerdMock, pubKeyConverter)
	n := newAPITransactionResultProcessor(pubKeyConverter, historyRepo, dataStore, marshalizerdMock, txUnmarshalerAndPreparer, 0)

	epoch := uint32(0)

	tx := &transaction.ApiTransactionResult{}

	expectedRecAPI := &transaction.ApiReceipt{
		Value:   rec.Value,
		Data:    string(rec.Data),
		TxHash:  hex.EncodeToString(txHash),
		SndAddr: pubKeyConverter.Encode(rec.SndAddr),
	}

	n.putResultsInTransaction(txHash, tx, epoch)
	require.Equal(t, expectedRecAPI, tx.Receipt)
}

func TestPutEventsInTransactionSmartContractResults(t *testing.T) {
	t.Parallel()

	epoch := uint32(0)
	txHash := []byte("txHash")
	scrHash1 := []byte("scrHash1")
	scrHash2 := []byte("scrHash2")

	scr1 := &smartContractResult.SmartContractResult{
		OriginalTxHash: txHash,
		RelayerAddr:    []byte("rlr"),
		OriginalSender: []byte("osn"),
		PrevTxHash:     []byte("prevTxHash"),
		SndAddr:        []byte("snd"),
		RcvAddr:        []byte("rcv"),
		Nonce:          1,
		Value:          big.NewInt(1000),
		GasLimit:       1,
		GasPrice:       5,
		Code:           []byte("code"),
		Data:           []byte("data"),
	}
	scr2 := &smartContractResult.SmartContractResult{
		OriginalTxHash: txHash,
	}

	logsAndEvents := &transaction.Log{
		Address: []byte("sender"),
		Events: []*transaction.Event{
			{
				Address:    []byte("addr1"),
				Identifier: []byte("myLog"),
				Topics:     [][]byte{[]byte("topic1"), []byte("topic2")},
				Data:       []byte("data1"),
			},
		},
	}

	marshalizerdMock := &mock.MarshalizerFake{}
	dataStore := &mock.ChainStorerMock{
		GetStorerCalled: func(unitType dataRetriever.UnitType) storage.Storer {
			switch unitType {
			case dataRetriever.UnsignedTransactionUnit:
				return &storageStubs.StorerStub{
					GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) {
						switch {
						case bytes.Equal(key, scrHash1):
							return marshalizerdMock.Marshal(scr1)
						case bytes.Equal(key, scrHash2):
							return marshalizerdMock.Marshal(scr2)
						default:
							return nil, nil
						}
					},
				}
			case dataRetriever.TxLogsUnit:
				return &storageStubs.StorerStub{
					GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) {
						switch {
						case bytes.Equal(key, scrHash1):
							return marshalizerdMock.Marshal(logsAndEvents)
						default:
							return nil, nil
						}
					},
				}
			default:
				return mock.NewStorerMock()
			}
		},
	}
	historyRepo := &dbLookupExtMock.HistoryRepositoryStub{
		GetEventsHashesByTxHashCalled: func(hash []byte, e uint32) (*dblookupext.ResultsHashesByTxHash, error) {
			return &dblookupext.ResultsHashesByTxHash{
				ReceiptsHash: nil,
				ScResultsHashesAndEpoch: []*dblookupext.ScResultsHashesAndEpoch{
					{
						Epoch:           epoch,
						ScResultsHashes: [][]byte{scrHash1, scrHash2},
					},
				},
			}, nil
		},
	}

	pubKeyConverter := mock.NewPubkeyConverterMock(3)
	txUnmarshalerAndPreparer := newTransactionUnmarshaller(marshalizerdMock, pubKeyConverter)
	n := newAPITransactionResultProcessor(pubKeyConverter, historyRepo, dataStore, marshalizerdMock, txUnmarshalerAndPreparer, 0)

	expectedSCRS := []*transaction.ApiSmartContractResult{
		{
			Hash:           hex.EncodeToString(scrHash1),
			Nonce:          scr1.Nonce,
			Value:          scr1.Value,
			RelayedValue:   scr1.RelayedValue,
			Code:           string(scr1.Code),
			Data:           string(scr1.Data),
			PrevTxHash:     hex.EncodeToString(scr1.PrevTxHash),
			OriginalTxHash: hex.EncodeToString(scr1.OriginalTxHash),
			GasLimit:       scr1.GasLimit,
			GasPrice:       scr1.GasPrice,
			CallType:       scr1.CallType,
			CodeMetadata:   string(scr1.CodeMetadata),
			ReturnMessage:  string(scr1.ReturnMessage),
			SndAddr:        pubKeyConverter.Encode(scr1.SndAddr),
			RcvAddr:        pubKeyConverter.Encode(scr1.RcvAddr),
			RelayerAddr:    pubKeyConverter.Encode(scr1.RelayerAddr),
			OriginalSender: pubKeyConverter.Encode(scr1.OriginalSender),
			Logs: &transaction.ApiLogs{
				Address: pubKeyConverter.Encode(logsAndEvents.Address),
				Events: []*transaction.Events{
					{
						Address:    pubKeyConverter.Encode(logsAndEvents.Events[0].Address),
						Identifier: string(logsAndEvents.Events[0].Identifier),
						Topics:     logsAndEvents.Events[0].Topics,
						Data:       logsAndEvents.Events[0].Data,
					},
				},
			},
		},
		{
			Hash:           hex.EncodeToString(scrHash2),
			OriginalTxHash: hex.EncodeToString(scr1.OriginalTxHash),
			Logs:           nil,
		},
	}

	tx := &transaction.ApiTransactionResult{}
	n.putResultsInTransaction(txHash, tx, epoch)
	require.Equal(t, expectedSCRS, tx.SmartContractResults)
}

func TestPutLogsInTransaction(t *testing.T) {
	t.Parallel()

	epoch := uint32(0)
	txHash := []byte("txHash")

	logsAndEvents := &transaction.Log{
		Address: []byte("sender"),
		Events: []*transaction.Event{
			{
				Address:    []byte("addr1"),
				Identifier: []byte(core.BuiltInFunctionESDTNFTCreate),
				Topics:     [][]byte{[]byte("topic1"), []byte("topic2")},
				Data:       []byte("data1"),
			},
			{
				Address:    []byte("addr2"),
				Identifier: []byte(core.BuiltInFunctionESDTBurn),
				Topics:     [][]byte{[]byte("topic1"), []byte("topic2")},
				Data:       []byte("data1"),
			},
		},
	}

	marshalizerMock := &mock.MarshalizerFake{}
	dataStore := &mock.ChainStorerMock{
		GetStorerCalled: func(unitType dataRetriever.UnitType) storage.Storer {
			return &storageStubs.StorerStub{
				GetFromEpochCalled: func(key []byte, epoch uint32) ([]byte, error) {
					switch {
					case bytes.Equal(key, txHash):
						return marshalizerMock.Marshal(logsAndEvents)
					default:
						return nil, nil
					}
				},
			}
		},
	}

	historyRepo := &dbLookupExtMock.HistoryRepositoryStub{
		GetEventsHashesByTxHashCalled: func(hash []byte, e uint32) (*dblookupext.ResultsHashesByTxHash, error) {
			return nil, errors.New("local err")
		},
	}

	pubKeyConverter := &mock.PubkeyConverterMock{}
	txUnmarshalerAndPreparer := newTransactionUnmarshaller(marshalizerMock, pubKeyConverter)
	n := newAPITransactionResultProcessor(pubKeyConverter, historyRepo, dataStore, marshalizerMock, txUnmarshalerAndPreparer, 0)
	expectedLogs := &transaction.ApiLogs{
		Address: pubKeyConverter.Encode(logsAndEvents.Address),
		Events: []*transaction.Events{
			{
				Address:    pubKeyConverter.Encode(logsAndEvents.Events[0].Address),
				Identifier: string(logsAndEvents.Events[0].Identifier),
				Topics:     logsAndEvents.Events[0].Topics,
				Data:       logsAndEvents.Events[0].Data,
			},
			{
				Address:    pubKeyConverter.Encode(logsAndEvents.Events[1].Address),
				Identifier: string(logsAndEvents.Events[1].Identifier),
				Topics:     logsAndEvents.Events[1].Topics,
				Data:       logsAndEvents.Events[1].Data,
			},
		},
	}

	tx := &transaction.ApiTransactionResult{}
	n.putResultsInTransaction(txHash, tx, epoch)
	require.Equal(t, expectedLogs, tx.Logs)
}
