package protocol

import (
	"context"
	"crypto/ecdsa"
	"encoding/hex"
	"strings"
	"testing"

	"math/big"

	"github.com/stretchr/testify/suite"

	coretypes "github.com/status-im/status-go/eth-node/core/types"
	"github.com/status-im/status-go/eth-node/crypto"
	"github.com/status-im/status-go/eth-node/types"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/tt"
)

func padArray(bb []byte, size int) []byte {
	l := len(bb)
	if l == size {
		return bb
	}
	if l > size {
		return bb[l-size:]
	}
	tmp := make([]byte, size)
	copy(tmp[size-l:], bb)
	return tmp
}

type TransactionValidatorSuite struct {
	suite.Suite
}

func TestTransactionValidatorSuite(t *testing.T) {
	suite.Run(t, new(TransactionValidatorSuite))
}

func buildSignature(walletKey *ecdsa.PrivateKey, chatKey *ecdsa.PublicKey, hash string) ([]byte, error) {
	hashBytes, err := hex.DecodeString(hash[2:])
	if err != nil {
		return nil, err
	}
	chatKeyBytes := crypto.FromECDSAPub(chatKey)
	signatureMaterial := append(chatKeyBytes, hashBytes...)
	signatureMaterial = crypto.TextHash(signatureMaterial)
	signature, err := crypto.Sign(signatureMaterial, walletKey)
	if err != nil {
		return nil, err
	}
	signature[64] += 27
	return signature, nil
}

func buildData(fn string, to types.Address, value *big.Int) []byte {
	var data []byte
	addressBytes := make([]byte, 32)

	fnBytes, _ := hex.DecodeString(fn)
	copy(addressBytes[12:], to.Bytes())
	valueBytes := padArray(value.Bytes(), 32)

	data = append(data, fnBytes...)
	data = append(data, addressBytes...)
	data = append(data, valueBytes...)
	return data
}

func (s *TransactionValidatorSuite) TestValidateTransactions() {
	notTransferFunction := "a9059cbd"

	senderKey, err := crypto.GenerateKey()
	s.Require().NoError(err)

	senderWalletKey, err := crypto.GenerateKey()
	s.Require().NoError(err)

	myWalletKey1, err := crypto.GenerateKey()
	s.Require().NoError(err)
	myWalletKey2, err := crypto.GenerateKey()
	s.Require().NoError(err)

	senderAddress := crypto.PubkeyToAddress(senderWalletKey.PublicKey)
	myAddress1 := crypto.PubkeyToAddress(myWalletKey1.PublicKey)
	myAddress2 := crypto.PubkeyToAddress(myWalletKey2.PublicKey)

	db, err := openTestDB()
	s.Require().NoError(err)
	p := &sqlitePersistence{db: db}

	logger := tt.MustCreateTestLogger()
	validator := NewTransactionValidator([]types.Address{myAddress1, myAddress2}, p, nil, logger)

	contractString := "0x744d70fdbe2ba4cf95131626614a1763df805b9e"
	contractAddress := types.HexToAddress(contractString)

	defaultTransactionHash := "0x53edbe74408c2eeed4e5493b3aac0c006d8a14b140975f4306dd35f5e1d245bc"
	testCases := []struct {
		Name                     string
		Valid                    bool
		AccordingToSpec          bool
		Error                    bool
		Transaction              coretypes.Message
		OverrideSignatureChatKey *ecdsa.PublicKey
		OverrideTransactionHash  string
		Parameters               *common.CommandParameters
		WalletKey                *ecdsa.PrivateKey
		From                     *ecdsa.PublicKey
	}{
		{
			Name:            "valid eth transfer to any address",
			Valid:           true,
			AccordingToSpec: true,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value: "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name:            "valid eth transfer to specific address",
			Valid:           true,
			AccordingToSpec: true,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value:   "23",
				Address: strings.ToLower(myAddress1.Hex()),
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name: "invalid eth transfer, not includes pk of the chat in signature",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value:   "23",
				Address: strings.ToLower(myAddress1.Hex()),
			},
			WalletKey:                senderWalletKey,
			OverrideSignatureChatKey: &senderWalletKey.PublicKey,
			From:                     &senderKey.PublicKey,
		},
		{
			Name: "invalid eth transfer, not signed with the wallet key",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value:   "23",
				Address: strings.ToLower(myAddress1.Hex()),
			},
			WalletKey: senderKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name: "invalid eth transfer, wrong signature transaction hash",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			OverrideTransactionHash: "0xdd9202df5e2f3611b5b6b716aef2a3543cc0bdd7506f50926e0869b83c8383b9",
			Parameters: &common.CommandParameters{
				Value: "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},

		{
			Name: "invalid eth transfer, we own the wallet but not as specified",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value:   "23",
				Address: strings.ToLower(myAddress2.Hex()),
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name: "invalid eth transfer, not our wallet",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&senderAddress,
				1,
				big.NewInt(int64(23)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value: "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name:  "valid eth transfer, but not according to spec, wrong amount",
			Valid: true,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&myAddress1,
				1,
				big.NewInt(int64(20)),
				0,
				nil,
				nil,
				false,
			),
			Parameters: &common.CommandParameters{
				Value: "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name:            "valid token transfer to any address",
			Valid:           true,
			AccordingToSpec: true,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress1, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name:            "valid token transfer to a specific address",
			Valid:           true,
			AccordingToSpec: true,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress1, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Address:  strings.ToLower(myAddress1.Hex()),
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name:            "valid token transfer, not according to spec because of amount",
			Valid:           true,
			AccordingToSpec: false,
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress1, big.NewInt(int64(13))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name: "invalid token transfer, wrong contract",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&senderAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress1, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Address:  strings.ToLower(myAddress1.Hex()),
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},

		{
			Name: "invalid token transfer, not an address I own",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress1, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Address:  strings.ToLower(senderAddress.Hex()),
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},

		{
			Name: "invalid token transfer, not the specified address",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(transferFunction, myAddress2, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Address:  strings.ToLower(myAddress1.Hex()),
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
		{
			Name: "invalid token transfer, wrong fn",
			Transaction: coretypes.NewMessage(
				senderAddress,
				&contractAddress,
				1,
				big.NewInt(int64(0)),
				0,
				nil,
				buildData(notTransferFunction, myAddress1, big.NewInt(int64(23))),
				false,
			),
			Parameters: &common.CommandParameters{
				Contract: contractString,
				Value:    "23",
			},
			WalletKey: senderWalletKey,
			From:      &senderKey.PublicKey,
		},
	}

	for _, tc := range testCases {
		s.Run(tc.Name, func() {
			tc.Parameters.TransactionHash = defaultTransactionHash
			signatureTransactionHash := defaultTransactionHash
			signatureChatKey := tc.From
			if tc.OverrideTransactionHash != "" {
				signatureTransactionHash = tc.OverrideTransactionHash
			}
			if tc.OverrideSignatureChatKey != nil {
				signatureChatKey = tc.OverrideSignatureChatKey
			}
			signature, err := buildSignature(tc.WalletKey, signatureChatKey, signatureTransactionHash)
			s.Require().NoError(err)
			tc.Parameters.Signature = signature

			response, err := validator.validateTransaction(context.Background(), tc.Transaction, tc.Parameters, tc.From)
			if tc.Error {
				s.Error(err)
				return
			}
			s.Require().NoError(err)
			s.Equal(tc.AccordingToSpec, response.AccordingToSpec)
			s.Equal(tc.Valid, response.Valid)
		})
	}

}