package native_test

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/internal/contracts"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/fee"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest/chain"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

var pathToInternalContracts = filepath.Join("..", "..", "..", "internal", "contracts")

func TestNativeContract_Invoke(t *testing.T) {
	const (
		transferCPUFee          = 1 << 17
		transferStorageFee      = 50
		systemContractCallPrice = 1 << 15
	)
	bc, validator, committee := chain.NewMulti(t)
	e := tutustest.NewExecutor(t, bc, validator, committee)
	gasHash := e.NativeHash(t, nativenames.Lub)

	baseExecFee := bc.GetBaseExecFee()
	price := fee.Opcode(baseExecFee, opcode.SYSCALL, // System.Contract.Call
		opcode.PUSHDATA1, // contract hash (20 byte)
		opcode.PUSHDATA1, // method
		opcode.PUSH15,    // call flags
		// `transfer` args:
		opcode.PUSHDATA1, // from
		opcode.PUSHDATA1, // to
		opcode.PUSH1,     // amount
		opcode.PUSHNULL,  // data
		// end args
		opcode.PUSH4, // amount of args
		opcode.PACK,  // pack args
	)
	price += systemContractCallPrice*baseExecFee + // System.Contract.Call price
		transferCPUFee*baseExecFee + // `transfer` itself
		transferStorageFee*bc.GetStoragePrice() // `transfer` storage price

	tx := e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil)
	e.SignTx(t, tx, -1, validator)
	e.AddNewBlock(t, tx)
	e.CheckHalt(t, tx.Hash(), stackitem.Make(true))

	// Enough for Call and other opcodes, but not enough for "transfer" call.
	tx = e.NewUnsignedTx(t, gasHash, "transfer", validator.ScriptHash(), validator.ScriptHash(), 1, nil)
	e.SignTx(t, tx, vm.PicoGasToDatoshi(price)-1, validator)
	e.AddNewBlock(t, tx)
	e.CheckFault(t, tx.Hash(), "gas limit exceeded")
}

func TestNativeContract_InvokeInternal(t *testing.T) {
	bc, validator, committee := chain.NewMulti(t)
	e := tutustest.NewExecutor(t, bc, validator, committee)
	clState := bc.GetContractState(e.NativeHash(t, nativenames.CryptoLib))
	require.NotNil(t, clState)
	md := clState.Manifest.ABI.GetMethod("ripemd160", 1)
	require.NotNil(t, md)

	t.Run("fail, bad current script hash", func(t *testing.T) {
		ic, err := bc.GetTestVM(trigger.Application, nil, nil)
		require.NoError(t, err)
		v := ic.SpawnVM()
		fakeH := util.Uint160{1, 2, 3}
		v.LoadScriptWithHash(clState.NEF.Script, fakeH, callflag.All)
		input := []byte{1, 2, 3, 4}
		v.Estack().PushVal(input)
		v.Context().Jump(md.Offset)

		// Bad current script hash
		err = v.Run()
		require.Error(t, err)
		require.True(t, strings.Contains(err.Error(), fmt.Sprintf("native contract %s (version 0) not found", fakeH.StringLE())), err.Error())
	})

	/*
		t.Run("fail, bad NativeUpdateHistory height", func(t *testing.T) {
			bcBad, validatorBad, committeeBad := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) {
				c.NativeUpdateHistories = map[string][]uint32{
					nativenames.Policy:      {0},
					nativenames.Tutus:         {0},
					nativenames.Lub:         {0},
					nativenames.Designation: {0},
					nativenames.StdLib:      {0},
					nativenames.Management:  {0},
					nativenames.Oracle:      {0},
					nativenames.Ledger:      {0},
					nativenames.CryptoLib:   {1},
				}
			})
			eBad := tutustest.NewExecutor(t, bcBad, validatorBad, committeeBad)

			ic, err := bcBad.GetTestVM(trigger.Application, nil, nil)
			require.NoError(t, err)
			v := ic.SpawnVM()
			v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
			input := []byte{1, 2, 3, 4}
			v.Estack().PushVal(input)
			v.Context().Jump(md.Offset)

			// It's prohibited to call natives before NativeUpdateHistory[0] height.
			err = v.Run()
			require.Error(t, err)
			require.True(t, strings.Contains(err.Error(), "native contract CryptoLib is active after height = 1"))

			// Add new block => CryptoLib should be active now.
			eBad.AddNewBlock(t)
			ic, err = bcBad.GetTestVM(trigger.Application, nil, nil)
			require.NoError(t, err)
			v = ic.SpawnVM()
			v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All) // hash is not affected by native update history
			v.Estack().PushVal(input)
			v.Context().Jump(md.Offset)

			require.NoError(t, v.Run())
			value := v.Estack().Pop().Bytes()
			require.Equal(t, hash.RipeMD160(input).BytesBE(), value)
		})
	*/

	manState := bc.GetContractState(e.NativeHash(t, nativenames.Management))
	require.NotNil(t, manState)
	mdDeploy := manState.Manifest.ABI.GetMethod("deploy", 2)
	require.NotNil(t, mdDeploy)
	t.Run("fail, bad call flag", func(t *testing.T) {
		bc, _, _ := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) {
			c.Hardforks = nil
		})
		ic, err := bc.GetTestVM(trigger.Application, nil, nil)
		require.NoError(t, err)
		v := ic.SpawnVM()
		v.LoadScriptWithHash(manState.NEF.Script, manState.Hash, callflag.States|callflag.AllowNotify)
		input := []byte{1, 2, 3, 4}
		v.Estack().PushVal(input)
		v.Estack().PushVal(input)
		v.Context().Jump(mdDeploy.Offset)

		// Can't call with these flags, Aspidochelone is active.
		err = v.Run()
		require.Error(t, err)
		require.True(t, strings.Contains(err.Error(), "missing call flags for native 0 `deploy` operation call"))
	})

	t.Run("good, pre-aspidochelone deploy", func(t *testing.T) {
		bc, _, _ := chain.NewMultiWithCustomConfig(t, func(c *config.Blockchain) {
			c.Hardforks = map[string]uint32{
				config.HFAspidochelone.String(): 100500,
			}
		})

		ic, err := bc.GetTestVM(trigger.Application, nil, nil)
		require.NoError(t, err)
		v := ic.SpawnVM()
		v.LoadScriptWithHash(manState.NEF.Script, manState.Hash, callflag.States|callflag.AllowNotify)
		input := []byte{1, 2, 3, 4}
		v.Estack().PushVal(input)
		v.Estack().PushVal(input)
		v.Context().Jump(mdDeploy.Offset)

		// We have an invalid input, but call flags are OK.
		err = v.Run()
		require.Error(t, err)
		require.True(t, strings.Contains(err.Error(), "invalid NEF file"))
	})

	t.Run("success", func(t *testing.T) {
		ic, err := bc.GetTestVM(trigger.Application, nil, nil)
		require.NoError(t, err)
		v := ic.SpawnVM()
		v.LoadScriptWithHash(clState.NEF.Script, clState.Hash, callflag.All)
		input := []byte{1, 2, 3, 4}
		v.Estack().PushVal(input)
		v.Context().Jump(md.Offset)

		require.NoError(t, v.Run())

		value := v.Estack().Pop().Bytes()
		require.Equal(t, hash.RipeMD160(input).BytesBE(), value)
	})
}

func TestNativeContract_InvokeOtherContract(t *testing.T) {
	bc, validator, committee := chain.NewMulti(t)
	e := tutustest.NewExecutor(t, bc, validator, committee)
	managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management))
	gasInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Lub))

	cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 1, 2, validator.ScriptHash())
	cs.Hash = state.CreateContractHash(validator.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name) // set proper hash
	manifB, err := json.Marshal(cs.Manifest)
	require.NoError(t, err)
	nefB, err := cs.NEF.Bytes()
	require.NoError(t, err)
	si, err := cs.ToStackItem()
	require.NoError(t, err)
	managementInvoker.Invoke(t, si, "deploy", nefB, manifB)

	t.Run("non-native, no return", func(t *testing.T) {
		// `onNEP17Payment` will be invoked on test contract from GAS contract.
		gasInvoker.Invoke(t, true, "transfer", validator.ScriptHash(), cs.Hash, 1, nil)
	})
}
