package payload

import (
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/internal/random"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"github.com/stretchr/testify/require"
)

func TestNotaryRequestIsValid(t *testing.T) {
	mainTx := &transaction.Transaction{
		Attributes:      []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
		Script:          []byte{0, 1, 2},
		ValidUntilBlock: 123,
	}
	emptySingleInvocation := append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...)
	errorCases := map[string]*P2PNotaryRequest{
		"main tx: missing NotaryAssisted attribute": {MainTransaction: &transaction.Transaction{}},
		"main tx: zero NKeys":                       {MainTransaction: &transaction.Transaction{Attributes: []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}}}}},
		"fallback transaction: invalid signers count": {
			MainTransaction:     mainTx,
			FallbackTransaction: &transaction.Transaction{Signers: []transaction.Signer{{Account: random.Uint160()}}},
		},
		"fallback transaction: invalid witnesses count": {
			MainTransaction:     mainTx,
			FallbackTransaction: &transaction.Transaction{Signers: []transaction.Signer{{Account: random.Uint160()}}},
		},
		"fallback tx: invalid dummy Notary witness (bad witnesses length)": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{}},
			},
		},
		"fallback tx: invalid dummy Notary witness (bad invocation script length)": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{}, {}},
			},
		},
		"fallback tx: invalid dummy Notary witness (bad invocation script prefix)": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), 65}, make([]byte, keys.SignatureLen)...)}, {}},
			},
		},
		"fallback tx: invalid dummy Notary witness (non-empty verification script))": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 1)}, {}},
			},
		},
		"fallback tx: missing NotValidBefore attribute": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
		"fallback tx: invalid number of Conflicts attributes": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Attributes: []transaction.Attribute{{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}}},
				Signers:    []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts:    []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
		"fallback tx: does not conflicts with main tx": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: util.Uint256{}}},
				},
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
		"fallback tx: missing NotaryAssisted attribute": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
				},
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
		"fallback tx: non-zero NKeys": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
					{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}},
				},
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
		"fallback tx: ValidUntilBlock mismatch": {
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				ValidUntilBlock: 321,
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
					{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
				},
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		},
	}
	for name, errCase := range errorCases {
		t.Run(name, func(t *testing.T) {
			require.Error(t, errCase.isValid())
		})
	}
	t.Run("good", func(t *testing.T) {
		p := &P2PNotaryRequest{
			MainTransaction: mainTx,
			FallbackTransaction: &transaction.Transaction{
				ValidUntilBlock: 123,
				Attributes: []transaction.Attribute{
					{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
					{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
					{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
				},
				Signers: []transaction.Signer{{Account: random.Uint160()}, {Account: random.Uint160()}},
				Scripts: []transaction.Witness{{InvocationScript: emptySingleInvocation, VerificationScript: make([]byte, 0)}, {}},
			},
		}
		require.NoError(t, p.isValid())
	})
}

func TestNotaryRequestBytesFromBytes(t *testing.T) {
	mainTx := &transaction.Transaction{
		Attributes:      []transaction.Attribute{{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 1}}},
		Script:          []byte{0, 1, 2},
		ValidUntilBlock: 123,
		Signers:         []transaction.Signer{{Account: util.Uint160{1, 5, 9}}},
		Scripts: []transaction.Witness{{
			InvocationScript:   []byte{1, 4, 7},
			VerificationScript: []byte{3, 6, 9},
		}},
	}
	_ = mainTx.Hash()
	_ = mainTx.Size()
	fallbackTx := &transaction.Transaction{
		Script:          []byte{3, 2, 1},
		ValidUntilBlock: 123,
		Attributes: []transaction.Attribute{
			{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: 123}},
			{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: mainTx.Hash()}},
			{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: 0}},
		},
		Signers: []transaction.Signer{{Account: util.Uint160{1, 4, 7}}, {Account: util.Uint160{9, 8, 7}}},
		Scripts: []transaction.Witness{
			{InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, make([]byte, keys.SignatureLen)...), VerificationScript: make([]byte, 0)},
			{InvocationScript: []byte{1, 2, 3}, VerificationScript: []byte{1, 2, 3}}},
	}
	_ = fallbackTx.Hash()
	_ = fallbackTx.Size()
	p := &P2PNotaryRequest{
		MainTransaction:     mainTx,
		FallbackTransaction: fallbackTx,
		Witness: transaction.Witness{
			InvocationScript:   []byte{1, 2, 3},
			VerificationScript: []byte{7, 8, 9},
		},
	}

	_ = p.Hash() // initialize hash caches
	bytes, err := p.Bytes()
	require.NoError(t, err)
	actual, err := NewP2PNotaryRequestFromBytes(bytes)
	require.NoError(t, err)
	require.Equal(t, p, actual)
}

func TestP2PNotaryRequest_Copy(t *testing.T) {
	priv, err := keys.NewPrivateKey()
	require.NoError(t, err)
	orig := &P2PNotaryRequest{
		MainTransaction: &transaction.Transaction{
			NetworkFee:      2000,
			SystemFee:       500,
			Nonce:           12345678,
			ValidUntilBlock: 100,
			Version:         1,
			Signers: []transaction.Signer{
				{Account: random.Uint160(), Scopes: transaction.Global, AllowedContracts: []util.Uint160{random.Uint160()}, AllowedGroups: keys.PublicKeys{priv.PublicKey()}, Rules: []transaction.WitnessRule{{Action: 0x01, Condition: transaction.ConditionCalledByEntry{}}}},
				{Account: random.Uint160(), Scopes: transaction.CalledByEntry},
			},

			Attributes: []transaction.Attribute{
				{Type: transaction.HighPriority, Value: &transaction.OracleResponse{
					ID:     0,
					Code:   transaction.Success,
					Result: []byte{4, 8, 15, 16, 23, 42},
				}},
			},
			Scripts: []transaction.Witness{
				{
					InvocationScript:   []byte{0x04, 0x05},
					VerificationScript: []byte{0x06, 0x07},
				},
			}},
		FallbackTransaction: &transaction.Transaction{
			Version:    2,
			SystemFee:  200,
			NetworkFee: 100,
			Script:     []byte{3, 2, 1},
			Signers:    []transaction.Signer{{Account: util.Uint160{4, 5, 6}}},
			Attributes: []transaction.Attribute{{Type: transaction.NotValidBeforeT}},
			Scripts: []transaction.Witness{
				{
					InvocationScript:   []byte{0x0D, 0x0E},
					VerificationScript: []byte{0x0F, 0x10},
				},
			},
		},
		Witness: transaction.Witness{
			InvocationScript:   []byte{0x11, 0x12},
			VerificationScript: []byte{0x13, 0x14},
		},
	}

	p2pCopy := orig.Copy()

	require.Equal(t, orig, p2pCopy)
	require.NotSame(t, orig, p2pCopy)

	require.Equal(t, orig.MainTransaction, p2pCopy.MainTransaction)
	require.Equal(t, orig.FallbackTransaction, p2pCopy.FallbackTransaction)
	require.Equal(t, orig.Witness, p2pCopy.Witness)

	p2pCopy.MainTransaction.Version = 3
	p2pCopy.FallbackTransaction.Script[0] = 0x1F
	p2pCopy.Witness.VerificationScript[1] = 0x22

	require.NotEqual(t, orig.MainTransaction.Version, p2pCopy.MainTransaction.Version)
	require.NotEqual(t, orig.FallbackTransaction.Script[0], p2pCopy.FallbackTransaction.Script[0])
	require.NotEqual(t, orig.Witness.VerificationScript[1], p2pCopy.Witness.VerificationScript[1])
}
