package contract_test

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

	"git.marketally.com/tutus-one/tutus-chain/internal/contracts"
	"git.marketally.com/tutus-one/tutus-chain/internal/random"
	"git.marketally.com/tutus-one/tutus-chain/pkg/compiler"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/contract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"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"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"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/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

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

func TestGetCallFlags(t *testing.T) {
	bc, _ := chain.NewSingle(t)
	ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
	require.NoError(t, err)

	ic.VM.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, callflag.All)
	require.NoError(t, contract.GetCallFlags(ic))
	require.Equal(t, int64(callflag.All), ic.VM.Estack().Pop().Value().(*big.Int).Int64())
}

func TestCall(t *testing.T) {
	bc, _ := chain.NewSingle(t)
	ic, err := bc.GetTestVM(trigger.Application, &transaction.Transaction{}, &block.Block{})
	require.NoError(t, err)

	cs, currCs := contracts.GetTestContractState(t, pathToInternalContracts, 4, 5, random.Uint160()) // sender and IDs are not important for the test
	require.NoError(t, native.PutContractState(ic.DAO, bc.NativeManagementID(), cs))
	require.NoError(t, native.PutContractState(ic.DAO, bc.NativeManagementID(), currCs))

	currScript := currCs.NEF.Script
	h := cs.Hash

	addArgs := stackitem.NewArray([]stackitem.Item{stackitem.Make(1), stackitem.Make(2)})
	t.Run("Good", func(t *testing.T) {
		t.Run("2 arguments", func(t *testing.T) {
			loadScript(ic, currScript, 42)
			ic.VM.Estack().PushVal(addArgs)
			ic.VM.Estack().PushVal(callflag.All)
			ic.VM.Estack().PushVal("add")
			ic.VM.Estack().PushVal(h.BytesBE())
			require.NoError(t, contract.Call(ic))
			require.NoError(t, ic.VM.Run())
			require.Equal(t, 2, ic.VM.Estack().Len())
			require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
		})
		t.Run("3 arguments", func(t *testing.T) {
			loadScript(ic, currScript, 42)
			ic.VM.Estack().PushVal(stackitem.NewArray(
				append(addArgs.Value().([]stackitem.Item), stackitem.Make(3))))
			ic.VM.Estack().PushVal(callflag.All)
			ic.VM.Estack().PushVal("add")
			ic.VM.Estack().PushVal(h.BytesBE())
			require.NoError(t, contract.Call(ic))
			require.NoError(t, ic.VM.Run())
			require.Equal(t, 2, ic.VM.Estack().Len())
			require.Equal(t, big.NewInt(6), ic.VM.Estack().Pop().Value())
			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
		})
	})

	t.Run("CallExInvalidFlag", func(t *testing.T) {
		loadScript(ic, currScript, 42)
		ic.VM.Estack().PushVal(addArgs)
		ic.VM.Estack().PushVal(byte(0xFF))
		ic.VM.Estack().PushVal("add")
		ic.VM.Estack().PushVal(h.BytesBE())
		require.Error(t, contract.Call(ic))
	})

	runInvalid := func(args ...any) func(t *testing.T) {
		return func(t *testing.T) {
			loadScriptWithHashAndFlags(ic, currScript, h, callflag.All, 42)
			for i := range args {
				ic.VM.Estack().PushVal(args[i])
			}
			// interops can both return error and panic,
			// we don't care which kind of error has occurred
			require.Panics(t, func() {
				err := contract.Call(ic)
				if err != nil {
					panic(err)
				}
			})
		}
	}

	t.Run("Invalid", func(t *testing.T) {
		t.Run("Hash", runInvalid(addArgs, "add", h.BytesBE()[1:]))
		t.Run("MissingHash", runInvalid(addArgs, "add", util.Uint160{}.BytesBE()))
		t.Run("Method", runInvalid(addArgs, stackitem.NewInterop("add"), h.BytesBE()))
		t.Run("MissingMethod", runInvalid(addArgs, "sub", h.BytesBE()))
		t.Run("DisallowedMethod", runInvalid(stackitem.NewArray(nil), "ret7", h.BytesBE()))
		t.Run("Arguments", runInvalid(1, "add", h.BytesBE()))
		t.Run("NotEnoughArguments", runInvalid(
			stackitem.NewArray([]stackitem.Item{stackitem.Make(1)}), "add", h.BytesBE()))
		t.Run("TooMuchArguments", runInvalid(
			stackitem.NewArray([]stackitem.Item{
				stackitem.Make(1), stackitem.Make(2), stackitem.Make(3), stackitem.Make(4)}),
			"add", h.BytesBE()))
	})

	t.Run("ReturnValues", func(t *testing.T) {
		t.Run("Many", func(t *testing.T) {
			loadScript(ic, currScript, 42)
			ic.VM.Estack().PushVal(stackitem.NewArray(nil))
			ic.VM.Estack().PushVal(callflag.All)
			ic.VM.Estack().PushVal("invalidReturn")
			ic.VM.Estack().PushVal(h.BytesBE())
			require.NoError(t, contract.Call(ic))
			require.Error(t, ic.VM.Run())
		})
		t.Run("Void", func(t *testing.T) {
			loadScript(ic, currScript, 42)
			ic.VM.Estack().PushVal(stackitem.NewArray(nil))
			ic.VM.Estack().PushVal(callflag.All)
			ic.VM.Estack().PushVal("justReturn")
			ic.VM.Estack().PushVal(h.BytesBE())
			require.NoError(t, contract.Call(ic))
			require.NoError(t, ic.VM.Run())
			require.Equal(t, 2, ic.VM.Estack().Len())
			require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
			require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
		})
	})

	t.Run("IsolatedStack", func(t *testing.T) {
		loadScript(ic, currScript, 42)
		ic.VM.Estack().PushVal(stackitem.NewArray(nil))
		ic.VM.Estack().PushVal(callflag.All)
		ic.VM.Estack().PushVal("drop")
		ic.VM.Estack().PushVal(h.BytesBE())
		require.NoError(t, contract.Call(ic))
		require.Error(t, ic.VM.Run())
	})

	t.Run("CallInitialize", func(t *testing.T) {
		t.Run("Directly", runInvalid(stackitem.NewArray([]stackitem.Item{}), "_initialize", h.BytesBE()))

		loadScript(ic, currScript, 42)
		ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
		ic.VM.Estack().PushVal(callflag.All)
		ic.VM.Estack().PushVal("add3")
		ic.VM.Estack().PushVal(h.BytesBE())
		require.NoError(t, contract.Call(ic))
		require.NoError(t, ic.VM.Run())
		require.Equal(t, 2, ic.VM.Estack().Len())
		require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
		require.Equal(t, big.NewInt(42), ic.VM.Estack().Pop().Value())
	})
}

func TestSystemContractCall_Permissions(t *testing.T) {
	check := func(t *testing.T, cfg func(*config.Blockchain), shouldUpdateFail bool) {
		bc, acc := chain.NewSingleWithCustomConfig(t, cfg)
		e := tutustest.NewExecutor(t, bc, acc, acc)

		// Contract A has an unsafe method.
		srcA := `package contractA
		func RetOne() int {
			return 1
		}`
		ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
			NoEventsCheck:      true,
			NoPermissionsCheck: true,
			Name:               "contractA",
		})
		e.DeployContract(t, ctrA, nil)

		var hashAStr string
		for i := range util.Uint160Size {
			hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
			if i != util.Uint160Size-1 {
				hashAStr += ", "
			}
		}

		// Contract B has a method that calls contract's A and another update method that
		// calls contract's A after B's update.
		srcB := `package contractB
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/native/management"
		)
		func CallRetOne() int {
			res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
			return res
		}
		func Update(nef []byte, manifest []byte) int {
			management.Update(nef, manifest)
			res := contract.Call(interop.Hash160{` + hashAStr + `}, "retOne", contract.All).(int)
			return res
		}`
		ctrB := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
			Name:               "contractB",
			NoEventsCheck:      true,
			NoPermissionsCheck: true,
			Permissions: []manifest.Permission{
				{
					Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
					Methods:  manifest.WildStrings{Value: []string{"retOne"}},
				},
				{
					Methods: manifest.WildStrings{Value: []string{"update"}},
				},
			},
		})
		e.DeployContract(t, ctrB, nil)
		ctrBInvoker := e.ValidatorInvoker(ctrB.Hash)

		// ctrBUpdated differs from ctrB in that it has no permission to call retOne method of ctrA
		ctrBUpdated := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
			Name:               "contractB",
			NoEventsCheck:      true,
			NoPermissionsCheck: true,
			Permissions: []manifest.Permission{
				{
					Contract: manifest.PermissionDesc{Type: manifest.PermissionHash, Value: ctrA.Hash},
					Methods:  manifest.WildStrings{Value: []string{}},
				},
				{
					Methods: manifest.WildStrings{Value: []string{"update"}},
				},
			},
		})

		// Call to A before B update should HALT.
		ctrBInvoker.Invoke(t, stackitem.Make(1), "callRetOne")

		// Call to A in the same context as B update should HALT.
		n, err := ctrBUpdated.NEF.Bytes()
		require.NoError(t, err)
		m, err := json.Marshal(ctrBUpdated.Manifest)
		require.NoError(t, err)
		if shouldUpdateFail {
			ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "update", n, m)
		} else {
			ctrBInvoker.Invoke(t, stackitem.Make(1), "update", n, m)
		}

		// If contract is updated, then all to A after B update should FAULT (manifest
		// is updated, no permission to call retOne method).
		if !shouldUpdateFail {
			ctrBInvoker.InvokeFail(t, "System.Contract.Call failed: disallowed method call", "callRetOne")
		}
	}

	// Pre-Domovoi behaviour: an updated contract state is used for permissions check.
	check(t, func(cfg *config.Blockchain) {
		cfg.Hardforks = map[string]uint32{
			config.HFDomovoi.String(): 100500,
		}
	}, true)

	// Post-Domovoi behaviour: an executing contract state is used for permissions check.
	check(t, func(cfg *config.Blockchain) {
		cfg.Hardforks = map[string]uint32{
			config.HFDomovoi.String(): 0,
		}
	}, false)
}

func TestLoadToken(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)
	managementInvoker := e.ValidatorInvoker(e.NativeHash(t, nativenames.Management))

	cs, _ := contracts.GetTestContractState(t, pathToInternalContracts, 0, 1, acc.ScriptHash())
	rawManifest, err := json.Marshal(cs.Manifest)
	require.NoError(t, err)
	rawNef, err := cs.NEF.Bytes()
	require.NoError(t, err)
	tx := managementInvoker.PrepareInvoke(t, "deploy", rawNef, rawManifest)
	e.AddNewBlock(t, tx)
	e.CheckHalt(t, tx.Hash())
	cInvoker := e.ValidatorInvoker(cs.Hash)

	t.Run("good", func(t *testing.T) {
		realBalance, _ := bc.GetGoverningTokenBalance(acc.ScriptHash())
		cInvoker.Invoke(t, stackitem.NewBigInteger(big.NewInt(realBalance.Int64()+1)), "callT0", acc.ScriptHash())
	})
	t.Run("invalid param count", func(t *testing.T) {
		cInvoker.InvokeFail(t, "method not found: callT2/1", "callT2", acc.ScriptHash())
	})
	t.Run("invalid contract", func(t *testing.T) {
		cInvoker.InvokeFail(t, "token contract 0000000000000000000000000000000000000000 not found: key not found", "callT1")
	})
}

func TestSnapshotIsolation_Exceptions(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	// Contract A puts value in the storage, emits notifications and panics.
	srcA := `package contractA
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/runtime"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/storage"
		)
		func DoAndPanic(key, value []byte, nNtf int) int { // avoid https://git.marketally.com/tutus-one/tutus-chain/issues/2509
			c := storage.GetContext()
			storage.Put(c, key, value)
			for i := 0; i < nNtf; i++ {
				runtime.Notify("NotificationFromA", i)
			}
			panic("panic from A")
		}
		func CheckA(key []byte, nNtf int) bool {
			c := storage.GetContext()
			value := storage.Get(c, key)
			// If called from B, then no storage changes made by A should be visible by this moment (they have been discarded after exception handling).
			if value != nil {
				return false
			}
			notifications := runtime.GetNotifications(nil)
			if len(notifications) != nNtf {
				return false
			}
			// If called from B, then no notifications made by A should be visible by this moment (they have been discarded after exception handling).
			for i := 0; i < len(notifications); i++ {
				ntf := notifications[i]
				name := string(ntf[1].([]byte))
				if name == "NotificationFromA" {
					return false
				}
			}
			return true
		}
		func CheckB() bool {
			return contract.Call(runtime.GetCallingScriptHash(), "checkStorageChanges", contract.All).(bool)
		}`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
		ContractEvents: []compiler.HybridEvent{
			{Name: "NotificationFromA", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
		},
	})
	e.DeployContract(t, ctrA, nil)

	var hashAStr string
	for i := range util.Uint160Size {
		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
		if i != util.Uint160Size-1 {
			hashAStr += ", "
		}
	}
	// Contract B puts value in the storage, emits notifications and calls A either
	// in try-catch block or without it. After that checks that proper notifications
	// and storage changes are available from different contexts.
	srcB := `package contractB
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/runtime"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/storage"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/util"
		)
		var caughtKey = []byte("caught")
		func DoAndCatch(shouldRecover bool, keyA, valueA, keyB, valueB []byte, nNtfA, nNtfB1, nNtfB2 int) {
			if shouldRecover {
				defer func() {
					if r := recover(); r != nil {
						keyA := []byte("keyA") // defer can not capture variables from outside
						nNtfB1 := 2
						nNtfB2 := 4
						c := storage.GetContext()
						storage.Put(c, caughtKey, []byte{})
						for i := 0; i < nNtfB2; i++ {
							runtime.Notify("NotificationFromB after panic", i)
						}
						// Check that storage changes and notifications made by A are reverted.
						ok := contract.Call(interop.Hash160{` + hashAStr + `}, "checkA", contract.All, keyA, nNtfB1+nNtfB2).(bool)
						if !ok {
							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
						}
						// Check that storage changes made by B after catch are still available in current context.
						ok = CheckStorageChanges()
						if !ok {
							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
						}
						// Check that storage changes made by B after catch are still available from the outside context.
						ok = contract.Call(interop.Hash160{` + hashAStr + `}, "checkB", contract.All).(bool)
						if !ok {
							util.Abort() // should never ABORT if snapshot isolation is correctly implemented.
						}
					}
				}()
			}
			c := storage.GetContext()
			storage.Put(c, keyB, valueB)
			for i := 0; i < nNtfB1; i++ {
				runtime.Notify("NotificationFromB before panic", i)
			}
			internalCaller(keyA, valueA, nNtfA)
		}
		func internalCaller(keyA, valueA []byte, nNtfA int) {
			contract.Call(interop.Hash160{` + hashAStr + `}, "doAndPanic", contract.All, keyA, valueA, nNtfA)
		}
		func CheckStorageChanges() bool {
			c := storage.GetContext()
			itm := storage.Get(c, caughtKey)
			return itm != nil
		}`
	ctrB := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
		Name:               "contractB",
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
		ContractEvents: []compiler.HybridEvent{
			{Name: "NotificationFromB before panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
			{Name: "NotificationFromB after panic", Parameters: []compiler.HybridParameter{{Parameter: manifest.Parameter{Name: "i", Type: smartcontract.IntegerType}}}},
		},
	})
	e.DeployContract(t, ctrB, nil)

	keyA := []byte("keyA")     // hard-coded in the contract code due to `defer` inability to capture variables from outside.
	valueA := []byte("valueA") // hard-coded in the contract code
	keyB := []byte("keyB")
	valueB := []byte("valueB")
	nNtfA := 3
	nNtfBBeforePanic := 2 // hard-coded in the contract code
	nNtfBAfterPanic := 4  // hard-coded in the contract code
	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)

	// Firstly, do not catch exception and check that all notifications are presented in the notifications list.
	h := ctrInvoker.InvokeFail(t, `unhandled exception: "panic from A"`, "doAndCatch", false, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
	aer := e.GetTxExecResult(t, h)
	require.Equal(t, nNtfBBeforePanic+nNtfA, len(aer.Events))

	// Then catch exception thrown by A and check that only notifications/storage changes from B are saved.
	h = ctrInvoker.Invoke(t, stackitem.Null{}, "doAndCatch", true, keyA, valueA, keyB, valueB, nNtfA, nNtfBBeforePanic, nNtfBAfterPanic)
	aer = e.GetTxExecResult(t, h)
	require.Equal(t, nNtfBBeforePanic+nNtfBAfterPanic, len(aer.Events))
}

// This test is written to test nested calls with try-catch block and proper notifications handling.
func TestSnapshotIsolation_NestedContextException(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	srcA := `package contractA
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/runtime"
		)
		func CallA() {
			runtime.Notify("Calling A")
			contract.Call(runtime.GetExecutingScriptHash(), "a", contract.All)
			runtime.Notify("Finish")
		}
		func A() {
			defer func() {
				if r := recover(); r != nil {
					runtime.Notify("Caught")
				}
			}()
			runtime.Notify("A")
			contract.Call(runtime.GetExecutingScriptHash(), "b", contract.All)
			runtime.Notify("Unreachable A")
		}
		func B() int {
			runtime.Notify("B")
			contract.Call(runtime.GetExecutingScriptHash(), "c", contract.All)
			runtime.Notify("Unreachable B")
			return 5
		}
		func C() {
			runtime.Notify("C")
			panic("exception from C")
		}`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
		ContractEvents: []compiler.HybridEvent{
			{Name: "Calling A"},
			{Name: "Finish"},
			{Name: "Caught"},
			{Name: "A"},
			{Name: "Unreachable A"},
			{Name: "B"},
			{Name: "Unreachable B"},
			{Name: "C"},
		},
	})
	e.DeployContract(t, ctrA, nil)

	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
	h := ctrInvoker.Invoke(t, stackitem.Null{}, "callA")
	aer := e.GetTxExecResult(t, h)
	require.Equal(t, 4, len(aer.Events))
	require.Equal(t, "Calling A", aer.Events[0].Name)
	require.Equal(t, "A", aer.Events[1].Name)
	require.Equal(t, "Caught", aer.Events[2].Name)
	require.Equal(t, "Finish", aer.Events[3].Name)
}

// This test is written to avoid https://github.com/neo-project/neo/issues/2746.
func TestSnapshotIsolation_CallToItself(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	// Contract A calls method of self and throws if storage changes made by Do are unavailable after call to it.
	srcA := `package contractA
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/runtime"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/storage"
		)
		var key = []byte("key")
		func Test() {
			contract.Call(runtime.GetExecutingScriptHash(), "callMyselfAndCheck", contract.All)
		}
		func CallMyselfAndCheck() {
			contract.Call(runtime.GetExecutingScriptHash(), "do", contract.All)
			c := storage.GetContext()
			val := storage.Get(c, key)
			if val == nil {
				panic("changes from previous context were not persisted")
			}
		}
		func Do() {
			c := storage.GetContext()
			storage.Put(c, key, []byte("value"))
		}
		func Check() {
			c := storage.GetContext()
			val := storage.Get(c, key)
			if val == nil {
				panic("value is nil")
			}
		}
`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
		Permissions:        []manifest.Permission{{Methods: manifest.WildStrings{Value: nil}}},
	})
	e.DeployContract(t, ctrA, nil)

	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
	ctrInvoker.Invoke(t, stackitem.Null{}, "test")

	// A separate call is needed to check whether all VM contexts were properly
	// unwrapped and persisted during the previous call.
	ctrInvoker.Invoke(t, stackitem.Null{}, "check")
}

// This test is written to check https://git.marketally.com/tutus-one/tutus-chain/issues/2509
// and https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
func TestRET_after_FINALLY_PanicInsideVoidMethod(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	// Contract A throws catchable exception. It also has a non-void method.
	srcA := `package contractA
		func Panic() {
			panic("panic from A")
		}
		func ReturnSomeValue() int {
			return 5
		}`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
	})
	e.DeployContract(t, ctrA, nil)

	var hashAStr string
	for i := range util.Uint160Size {
		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
		if i != util.Uint160Size-1 {
			hashAStr += ", "
		}
	}
	// Contract B calls A and catches the exception thrown by A.
	srcB := `package contractB
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
		)
		func Catch() {
			defer func() {
				if r := recover(); r != nil {
					// Call method with return value to check https://github.com/neo-project/neo/pull/2745#discussion_r879167180.
					contract.Call(interop.Hash160{` + hashAStr + `}, "returnSomeValue", contract.All)
				}
			}()
			contract.Call(interop.Hash160{` + hashAStr + `}, "panic", contract.All)
		}`
	ctrB := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
		Name:               "contractB",
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Permissions: []manifest.Permission{
			{
				Methods: manifest.WildStrings{Value: nil},
			},
		},
	})
	e.DeployContract(t, ctrB, nil)

	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
	ctrInvoker.Invoke(t, stackitem.Null{}, "catch")
}

// This test is written to check https://github.com/neo-project/neo/pull/2745#discussion_r879125733.
func TestRET_after_FINALLY_CallNonVoidAfterVoidMethod(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	// Contract A has two methods. One of them has no return value, and the other has it.
	srcA := `package contractA
		import "git.marketally.com/tutus-one/tutus-chain/pkg/interop/runtime"
		func NoRet() {
			runtime.Log("no ret")
		}
		func HasRet() int {
			runtime.Log("ret")
			return 5
		}`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
	})
	e.DeployContract(t, ctrA, nil)

	var hashAStr string
	for i := range util.Uint160Size {
		hashAStr += fmt.Sprintf("%#x", ctrA.Hash[i])
		if i != util.Uint160Size-1 {
			hashAStr += ", "
		}
	}
	// Contract B calls A in try-catch block.
	srcB := `package contractB
		import (
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
			"git.marketally.com/tutus-one/tutus-chain/pkg/interop/util"
		)
		func CallAInTryCatch() {
			defer func() {
				if r := recover(); r != nil {
					util.Abort() // should never happen
				}
			}()
			contract.Call(interop.Hash160{` + hashAStr + `}, "noRet", contract.All)
			contract.Call(interop.Hash160{` + hashAStr + `}, "hasRet", contract.All)
		}`
	ctrB := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcB), &compiler.Options{
		Name:               "contractB",
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Permissions: []manifest.Permission{
			{
				Methods: manifest.WildStrings{Value: nil},
			},
		},
	})
	e.DeployContract(t, ctrB, nil)

	ctrInvoker := e.NewInvoker(ctrB.Hash, e.Committee)
	h := ctrInvoker.Invoke(t, stackitem.Null{}, "callAInTryCatch")
	aer := e.GetTxExecResult(t, h)

	require.Equal(t, 1, len(aer.Stack))
}

// This test is created to check https://github.com/neo-project/neo/pull/2755#discussion_r880087983.
func TestCALLL_from_VoidContext(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	// Contract A has void method `CallHasRet` which calls non-void method `HasRet`.
	srcA := `package contractA
		func CallHasRet() { // Creates a context with non-nil onUnload.
			HasRet()
		}
		func HasRet() int { // CALL_L clones parent context, check that onUnload is not cloned.
			return 5
		}`
	ctrA := tutustest.CompileSource(t, acc.ScriptHash(), strings.NewReader(srcA), &compiler.Options{
		NoEventsCheck:      true,
		NoPermissionsCheck: true,
		Name:               "contractA",
	})
	e.DeployContract(t, ctrA, nil)

	ctrInvoker := e.NewInvoker(ctrA.Hash, e.Committee)
	ctrInvoker.Invoke(t, stackitem.Null{}, "callHasRet")
}

func loadScript(ic *interop.Context, script []byte, args ...any) {
	ic.SpawnVM()
	ic.VM.LoadScriptWithFlags(script, callflag.AllowCall)
	for i := range args {
		ic.VM.Estack().PushVal(args[i])
	}
	ic.VM.SetGasLimit(-1)
}

func loadScriptWithHashAndFlags(ic *interop.Context, script []byte, hash util.Uint160, f callflag.CallFlag, args ...any) {
	ic.SpawnVM()
	ic.VM.LoadScriptWithHash(script, hash, f)
	for i := range args {
		ic.VM.Estack().PushVal(args[i])
	}
	ic.VM.SetGasLimit(-1)
}
