package compiler_test

import (
	"errors"
	"fmt"
	"math/big"
	"strconv"
	"strings"
	"testing"

	"github.com/nspcc-dev/neo-go/internal/fakechain"
	"github.com/nspcc-dev/neo-go/pkg/compiler"
	"github.com/nspcc-dev/neo-go/pkg/config"
	"github.com/nspcc-dev/neo-go/pkg/core"
	"github.com/nspcc-dev/neo-go/pkg/core/dao"
	"github.com/nspcc-dev/neo-go/pkg/core/interop"
	"github.com/nspcc-dev/neo-go/pkg/core/native"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/storage"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/encoding/base58"
	cinterop "github.com/nspcc-dev/neo-go/pkg/interop"
	"github.com/nspcc-dev/neo-go/pkg/neotest"
	"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
)

func TestTypeConstantSize(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop"
	var a %T // type declaration is always ok
	func Main() any {
		return %#v
	}`

	t.Run("Hash160", func(t *testing.T) {
		t.Run("good", func(t *testing.T) {
			a := make(cinterop.Hash160, smartcontract.Hash160Len)
			src := fmt.Sprintf(src, a, a)
			eval(t, src, []byte(a))
		})
		t.Run("bad", func(t *testing.T) {
			a := make(cinterop.Hash160, 19)
			src := fmt.Sprintf(src, a, a)
			_, err := compiler.Compile("foo.go", strings.NewReader(src))
			require.Error(t, err)
		})
	})
	t.Run("Hash256", func(t *testing.T) {
		t.Run("good", func(t *testing.T) {
			a := make(cinterop.Hash256, smartcontract.Hash256Len)
			src := fmt.Sprintf(src, a, a)
			eval(t, src, []byte(a))
		})
		t.Run("bad", func(t *testing.T) {
			a := make(cinterop.Hash256, 31)
			src := fmt.Sprintf(src, a, a)
			_, err := compiler.Compile("foo.go", strings.NewReader(src))
			require.Error(t, err)
		})
	})
}

func TestAddressToHash160BuiltinConversion(t *testing.T) {
	a := "NQRLhCpAru9BjGsMwk67vdMwmzKMRgsnnN"
	h, err := address.StringToUint160(a)
	require.NoError(t, err)
	a2 := "NPAsqZkx9WhNd4P72uhZxBhLinSuNkxfB8"
	addr2, err := address.StringToUint160(a2)
	require.NoError(t, err)
	t.Run("builtin conversion", func(t *testing.T) {
		src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop"
			"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
		)
		var addr = address.ToHash160("` + a + `")
		func Main() interop.Hash160 {
			return addr
		}`
		prog := eval(t, src, h.BytesBE())
		// Address BE bytes expected to be present at program, which indicates that address conversion
		// was performed at compile-time.
		require.True(t, strings.Contains(string(prog), string(h.BytesBE())))
		// On the contrary, there should be no address string.
		require.False(t, strings.Contains(string(prog), a))
	})
	t.Run("generate code", func(t *testing.T) {
		src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop"
			"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
		)
		var addr = "` + a + `"
		func Main() interop.Hash160 {
			return address.ToHash160(addr)
		}`
		// Error on CALLT (std.Base58CheckDecode - method of StdLib native contract) is expected, which means
		// that address.ToHash160 code was honestly generated by the compiler without any optimisations.
		prog := evalWithError(t, src, "(CALLT): runtime error: invalid memory address or nil pointer dereference")
		// Address BE bytes expected not to be present at program, which indicates that address conversion
		// was not performed at compile-time.
		require.False(t, strings.Contains(string(prog), string(h.BytesBE())))
		// On the contrary, there should be an address string.
		require.True(t, strings.Contains(string(prog), a))
	})
	t.Run("AliasPackage", func(t *testing.T) {
		src := `
		package foo
		import ad "github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
		func Main() []byte {
			addr1 := ad.ToHash160("` + a + `")
			addr2 := ad.ToHash160("` + a2 + `")
			sum := append(addr1, addr2...)
			return sum
		}`
		eval(t, src, append(h.BytesBE(), addr2.BytesBE()...))
	})
}

func TestInvokeAddressToFromHash160(t *testing.T) {
	a := "NQRLhCpAru9BjGsMwk67vdMwmzKMRgsnnN"
	h, err := address.StringToUint160(a)
	require.NoError(t, err)

	bc, acc := chain.NewSingle(t)
	e := neotest.NewExecutor(t, bc, acc, acc)
	src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop"
			"github.com/nspcc-dev/neo-go/pkg/interop/lib/address"
		)
		const addr = "` + a + `"
		func ToHash160(a string) interop.Hash160 {
			return address.ToHash160(a)
		}
		func ToHash160AtCompileTime() interop.Hash160 {
			return address.ToHash160(addr)
		}
		func FromHash160(hash interop.Hash160) string {
			return address.FromHash160(hash)
		}`
	ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"})
	e.DeployContract(t, ctr, nil)
	c := e.CommitteeInvoker(ctr.Hash)

	t.Run("ToHash160", func(t *testing.T) {
		t.Run("invalid address length", func(t *testing.T) {
			c.InvokeFail(t, "invalid address length", "toHash160", base58.CheckEncode(make([]byte, util.Uint160Size+1+1)))
		})
		t.Run("invalid prefix", func(t *testing.T) {
			c.InvokeFail(t, "invalid address prefix", "toHash160", base58.CheckEncode(append([]byte{address.NEO2Prefix}, h.BytesBE()...)))
		})
		t.Run("good", func(t *testing.T) {
			c.Invoke(t, stackitem.NewBuffer(h.BytesBE()), "toHash160", a)
		})
	})
	t.Run("ToHash160Constant", func(t *testing.T) {
		t.Run("good", func(t *testing.T) {
			c.Invoke(t, stackitem.NewBuffer(h.BytesBE()), "toHash160AtCompileTime")
		})
	})
	t.Run("FromHash160", func(t *testing.T) {
		t.Run("good", func(t *testing.T) {
			c.Invoke(t, stackitem.NewByteArray([]byte(a)), "fromHash160", h.BytesBE())
		})
		t.Run("invalid length", func(t *testing.T) {
			c.InvokeFail(t, "invalid Hash160 length", "fromHash160", h.BytesBE()[:15])
		})
	})
}

func TestAbort(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/util"
	func Main() int {
		util.Abort()
		return 1
	}`
	v := vmAndCompile(t, src)
	require.Error(t, v.Run())
	require.True(t, v.HasFailed())
}

func TestAbortMsg(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/util"
	func Main() int {
		util.AbortMsg("some message")
		return 1
	}`
	v := vmAndCompile(t, src)
	err := v.Run()
	require.Error(t, err)
	require.True(t, v.HasFailed())
	require.True(t, strings.Contains(err.Error(), "ABORTMSG is executed. Reason: some message"), err)
}

func TestAssert(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/util"
	func Main(ok bool) int {
		util.Assert(ok)
		return 1
	}`

	// assert OK
	evalWithArgs(t, src, nil, []stackitem.Item{stackitem.Make(true)}, big.NewInt(1))

	// assert FALSE
	v := vmAndCompile(t, src)
	v.Estack().PushVal(false)
	err := v.Run()
	require.Error(t, err)
	require.True(t, v.HasFailed())
	require.True(t, strings.Contains(err.Error(), "ASSERT"), err)
}

func TestAssertMsg(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/util"
	func Main(ok bool) int {
		util.AssertMsg(ok, "some message")
		return 1
	}`

	// assert OK
	evalWithArgs(t, src, nil, []stackitem.Item{stackitem.Make(true)}, big.NewInt(1))

	// assert FALSE
	v := vmAndCompile(t, src)
	v.Estack().PushVal(false)
	err := v.Run()
	require.Error(t, err)
	require.True(t, v.HasFailed())
	require.True(t, strings.Contains(err.Error(), "ASSERTMSG is executed with false result. Reason: some message"), err)
}

func TestCurrentSigners(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := neotest.NewExecutor(t, bc, acc, acc)
	src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
			"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
		)
		func Main() []ledger.TransactionSigner {
			return runtime.CurrentSigners()
		}`
	ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"})
	e.DeployContract(t, ctr, nil)
	c := e.CommitteeInvoker(ctr.Hash)

	t.Run("non-empty", func(t *testing.T) {
		expected := stackitem.NewArray([]stackitem.Item{
			stackitem.NewArray([]stackitem.Item{
				stackitem.NewByteArray(e.CommitteeHash.BytesBE()),
				stackitem.NewBigInteger(big.NewInt(int64(transaction.Global))),
				stackitem.NewArray([]stackitem.Item{}),
				stackitem.NewArray([]stackitem.Item{}),
				stackitem.NewArray([]stackitem.Item{}),
			}),
		})
		c.Invoke(t, expected, "main")
	})
}

func TestStdLib_StrLen(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := neotest.NewExecutor(t, bc, acc, acc)
	src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
		)
		func Main(s string) int {
			return std.StrLen(s)
		}`
	ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"})
	e.DeployContract(t, ctr, nil)
	c := e.CommitteeInvoker(ctr.Hash)

	expected := stackitem.Make(1)
	c.Invoke(t, expected, "main", "🦆")
	c.Invoke(t, expected, "main", "ã")
	c.Invoke(t, expected, "main", "a")

	expected = stackitem.Make(7)
	c.Invoke(t, expected, "main", "abc 123")
}

func spawnVM(t *testing.T, ic *interop.Context, src string) *vm.VM {
	b, di, err := compiler.CompileWithOptions("foo.go", strings.NewReader(src), nil)
	require.NoError(t, err)
	v := core.SpawnVM(ic)
	invokeMethod(t, testMainIdent, b.Script, v, di)
	v.LoadScriptWithFlags(b.Script, callflag.All)
	return v
}

func TestAppCall(t *testing.T) {
	srcDeep := `package foo
	func Get42() int {
		return 42
	}`
	barCtr, di, err := compiler.CompileWithOptions("bar.go", strings.NewReader(srcDeep), nil)
	require.NoError(t, err)
	mBar, err := di.ConvertToManifest(&compiler.Options{Name: "Bar"})
	require.NoError(t, err)

	barH := hash.Hash160(barCtr.Script)

	srcInner := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
	import "github.com/nspcc-dev/neo-go/pkg/interop"
	var a int = 3
	func Main(a []byte, b []byte) []byte {
		panic("Main was called")
	}
	func Append(a []byte, b []byte) []byte {
		return append(a, b...)
	}
	func Add3(n int) int {
		return a + n
	}
	func CallInner() int {
		return contract.Call(%s, "get42", contract.All).(int)
	}`
	srcInner = fmt.Sprintf(srcInner,
		fmt.Sprintf("%#v", cinterop.Hash160(barH.BytesBE())))

	inner, di, err := compiler.CompileWithOptions("foo.go", strings.NewReader(srcInner), nil)
	require.NoError(t, err)
	m, err := di.ConvertToManifest(&compiler.Options{
		Name: "Foo",
		Permissions: []manifest.Permission{
			*manifest.NewPermission(manifest.PermissionWildcard),
		},
	})
	require.NoError(t, err)

	ih := hash.Hash160(inner.Script)
	var contractGetter = func(_ *dao.Simple, h util.Uint160) (*state.Contract, error) {
		if h.Equals(ih) {
			return &state.Contract{
				ContractBase: state.ContractBase{
					Hash:     ih,
					NEF:      *inner,
					Manifest: *m,
				},
			}, nil
		} else if h.Equals(barH) {
			return &state.Contract{
				ContractBase: state.ContractBase{
					Hash:     barH,
					NEF:      *barCtr,
					Manifest: *mBar,
				},
			}, nil
		}
		return nil, errors.New("not found")
	}

	fc := fakechain.NewFakeChain()
	ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false),
		interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, nil, zaptest.NewLogger(t))

	t.Run("valid script", func(t *testing.T) {
		src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE()))
		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())

		assertResult(t, v, []byte{1, 2, 3, 4})
	})

	t.Run("callEx, valid", func(t *testing.T) {
		src := getCallExScript(fmt.Sprintf("%#v", ih.BytesBE()), "contract.ReadStates|contract.AllowCall")
		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())

		assertResult(t, v, big.NewInt(42))
	})
	t.Run("callEx, missing flags", func(t *testing.T) {
		src := getCallExScript(fmt.Sprintf("%#v", ih.BytesBE()), "contract.NoneFlag")
		v := spawnVM(t, ic, src)
		require.Error(t, v.Run())
	})

	t.Run("missing script", func(t *testing.T) {
		h := ih
		h[0] = ^h[0]

		src := getAppCallScript(fmt.Sprintf("%#v", h.BytesBE()))
		v := spawnVM(t, ic, src)
		require.Error(t, v.Run())
	})

	t.Run("convert from string constant", func(t *testing.T) {
		src := `
		package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
		const scriptHash = ` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `
		func Main() []byte {
			x := []byte{1, 2}
			y := []byte{3, 4}
			result := contract.Call([]byte(scriptHash), "append", contract.All, x, y)
			return result.([]byte)
		}
		`

		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())

		assertResult(t, v, []byte{1, 2, 3, 4})
	})

	t.Run("convert from var", func(t *testing.T) {
		src := `
		package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
		func Main() []byte {
			x := []byte{1, 2}
			y := []byte{3, 4}
			var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `)
			result := contract.Call(addr, "append", contract.All, x, y)
			return result.([]byte)
		}
		`

		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())

		assertResult(t, v, []byte{1, 2, 3, 4})
	})

	t.Run("InitializedGlobals", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
		func Main() int {
			var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `)
			result := contract.Call(addr, "add3", contract.All, 39)
			return result.(int)
		}`

		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())

		assertResult(t, v, big.NewInt(42))
	})

	t.Run("AliasPackage", func(t *testing.T) {
		src := `package foo
		import ee "github.com/nspcc-dev/neo-go/pkg/interop/contract"
		func Main() int {
			var addr = []byte(` + fmt.Sprintf("%#v", string(ih.BytesBE())) + `)
			result := ee.Call(addr, "add3", ee.All, 39)
			return result.(int)
		}`
		v := spawnVM(t, ic, src)
		require.NoError(t, v.Run())
		assertResult(t, v, big.NewInt(42))
	})
}

func getAppCallScript(h string) string {
	return `
	package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
	func Main() []byte {
		x := []byte{1, 2}
		y := []byte{3, 4}
		result := contract.Call(` + h + `, "append", contract.All, x, y)
		return result.([]byte)
	}
	`
}

func getCallExScript(h string, flags string) string {
	return `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/contract"
	func Main() int {
		result := contract.Call(` + h + `, "callInner", ` + flags + `)
		return result.(int)
	}`
}

func TestBuiltinDoesNotCompile(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/interop/util"
	func Main() bool {
		a := 1
		b := 2
		return util.Equals(a, b)
	}`

	v := vmAndCompile(t, src)
	ctx := v.Context()
	retCount := 0
	for op, _, err := ctx.Next(); err == nil; op, _, err = ctx.Next() {
		if ctx.IP() >= len(ctx.Program()) {
			break
		}
		if op == opcode.RET {
			retCount++
		}
	}
	require.Equal(t, 1, retCount)
}

func TestInteropPackage(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/block"
	func Main() int {
		b := block.Block{}
		a := block.GetTransactionCount(b)
		return a
	}`
	eval(t, src, big.NewInt(42))
}

func TestBuiltinPackage(t *testing.T) {
	src := `package foo
	import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/util"
	func Main() int {
		if util.Equals(1, 2) { // always returns true
			return 1
		}
		return 2
	}`
	eval(t, src, big.NewInt(1))
}

func TestLenForNil(t *testing.T) {
	src := `
	package foo
	func Main() bool {
		var a []int = nil
		return len(a) == 0
	}`

	eval(t, src, true)
}

func TestCallTConversionErrors(t *testing.T) {
	t.Run("variable hash", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
		func Main() int {
			var hash string
			return neogointernal.CallWithToken(hash, "method", 0).(int)
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
	t.Run("bad hash", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
		func Main() int {
			return neogointernal.CallWithToken("badstring", "method", 0).(int)
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
	t.Run("variable method", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
		func Main() int {
			var method string
			return neogointernal.CallWithToken("\xf5\x63\xea\x40\xbc\x28\x3d\x4d\x0e\x05\xc4\x8e\xa3\x05\xb3\xf2\xa0\x73\x40\xef", method, 0).(int)
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
	t.Run("variable flags", func(t *testing.T) {
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
		func Main() {
			var flags int
			neogointernal.CallWithTokenNoRet("\xf5\x63\xea\x40\xbc\x28\x3d\x4d\x0e\x05\xc4\x8e\xa3\x05\xb3\xf2\xa0\x73\x40\xef", "method", flags)
		}`
		_, err := compiler.Compile("foo.go", strings.NewReader(src))
		require.Error(t, err)
	})
}

func TestCallWithVersion(t *testing.T) {
	bc, acc := chain.NewSingle(t)
	e := neotest.NewExecutor(t, bc, acc, acc)
	src := `package foo
		import (
			"github.com/nspcc-dev/neo-go/pkg/interop"
			"github.com/nspcc-dev/neo-go/pkg/interop/contract"
			util "github.com/nspcc-dev/neo-go/pkg/interop/lib/contract"
		)
		func CallWithVersion(hash interop.Hash160, version int, method string) any {
			return util.CallWithVersion(hash, version, method, contract.All)
		}`
	ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{Name: "Helper"})
	e.DeployContract(t, ctr, nil)
	c := e.CommitteeInvoker(ctr.Hash)

	policyH := state.CreateNativeContractHash(nativenames.Policy)
	t.Run("good", func(t *testing.T) {
		c.Invoke(t, e.Chain.GetBaseExecFee(), "callWithVersion", policyH.BytesBE(), 0, "getExecFeeFactor")
	})
	t.Run("unknown contract", func(t *testing.T) {
		c.InvokeFail(t, "unknown contract", "callWithVersion", util.Uint160{1, 2, 3}.BytesBE(), 0, "getExecFeeFactor")
	})
	t.Run("invalid version", func(t *testing.T) {
		c.InvokeFail(t, "contract version mismatch", "callWithVersion", policyH.BytesBE(), 1, "getExecFeeFactor")
	})
}

func TestForcedNotifyArgumentsConversion(t *testing.T) {
	const methodWithEllipsis = "withEllipsis"
	const methodWithoutEllipsis = "withoutEllipsis"
	check := func(t *testing.T, method string, targetSCParamTypes []smartcontract.ParamType, expectedVMParamTypes []stackitem.Type, noEventsCheck bool) {
		bc, acc := chain.NewSingleWithCustomConfig(t, func(blockchain *config.Blockchain) {
			blockchain.Hardforks = map[string]uint32{config.HFBasilisk.String(): 100500} // Disable runtime notifications check to reuse the same contract for different event parameter types.
		})
		e := neotest.NewExecutor(t, bc, acc, acc)
		src := `package foo
		import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
		const arg4 = 4			// Const value.
		func WithoutEllipsis() {
			var arg0 int		// Default value.
			var arg1 int = 1	// Initialized value.
			arg2 := 2			// Short decl.
			var arg3 int
			arg3 = 3			// Declare first, change value afterwards.
			runtime.Notify("withoutEllipsis", arg0, arg1, arg2, arg3, arg4, 5, f(6))	// The fifth argument is basic literal.
		}
		func WithEllipsis() {
			arg := []any{0, 1, f(2), 3, 4, 5, 6}
			runtime.Notify("withEllipsis", arg...)
		}
		func f(i int) int {
			return i
		}`
		count := len(targetSCParamTypes)
		if count != len(expectedVMParamTypes) {
			t.Fatalf("parameters count mismatch: %d vs %d", count, len(expectedVMParamTypes))
		}
		scParams := make([]compiler.HybridParameter, len(targetSCParamTypes))
		vmParams := make([]stackitem.Item, len(expectedVMParamTypes))
		for i := range scParams {
			scParams[i] = compiler.HybridParameter{Parameter: manifest.Parameter{
				Name: strconv.Itoa(i),
				Type: targetSCParamTypes[i],
			}}
			defaultValue := stackitem.NewBigInteger(big.NewInt(int64(i)))
			var (
				val stackitem.Item
				err error
			)
			if expectedVMParamTypes[i] == stackitem.IntegerT {
				val = defaultValue
			} else {
				val, err = defaultValue.Convert(expectedVMParamTypes[i]) // exactly the same conversion should be emitted by compiler and performed by the contract code.
				require.NoError(t, err)
			}
			vmParams[i] = val
		}
		ctr := neotest.CompileSource(t, e.CommitteeHash, strings.NewReader(src), &compiler.Options{
			Name: "Helper",
			ContractEvents: []compiler.HybridEvent{
				{
					Name:       methodWithoutEllipsis,
					Parameters: scParams,
				},
				{
					Name:       methodWithEllipsis,
					Parameters: scParams,
				},
			},
			NoEventsCheck: noEventsCheck,
		})
		e.DeployContract(t, ctr, nil)
		c := e.CommitteeInvoker(ctr.Hash)

		t.Run(method, func(t *testing.T) {
			h := c.Invoke(t, stackitem.Null{}, method)
			aer := c.GetTxExecResult(t, h)
			require.Equal(t, 1, len(aer.Events))
			require.Equal(t, stackitem.NewArray(vmParams), aer.Events[0].Item)
		})
	}
	checkSingleType := func(t *testing.T, method string, targetSCEventType smartcontract.ParamType, expectedVMType stackitem.Type, noEventsCheck ...bool) {
		count := 7
		scParams := make([]smartcontract.ParamType, count)
		vmParams := make([]stackitem.Type, count)
		for i := range scParams {
			scParams[i] = targetSCEventType
			vmParams[i] = expectedVMType
		}
		var noEvents bool
		if len(noEventsCheck) > 0 {
			noEvents = noEventsCheck[0]
		}
		check(t, method, scParams, vmParams, noEvents)
	}

	t.Run("good, single type, default values", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.IntegerType, stackitem.IntegerT)
	})
	t.Run("good, single type, conversion to BooleanT", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.BoolType, stackitem.BooleanT)
	})
	t.Run("good, single type, Hash160Type->ByteArray", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.Hash160Type, stackitem.ByteArrayT)
	})
	t.Run("good, single type, Hash256Type->ByteArray", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.Hash256Type, stackitem.ByteArrayT)
	})
	t.Run("good, single type, Signature->ByteArray", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.SignatureType, stackitem.ByteArrayT)
	})
	t.Run("good, single type, String->ByteArray", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.StringType, stackitem.ByteArrayT) // Special case, runtime.Notify will convert any Buffer to ByteArray.
	})
	t.Run("good, single type, PublicKeyType->ByteArray", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.PublicKeyType, stackitem.ByteArrayT)
	})
	t.Run("good, single type, AnyType->do not change initial type", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.AnyType, stackitem.IntegerT) // Special case, compiler should leave the type "as is" and do not emit conversion code.
	})
	// Test for InteropInterface->... is missing, because we don't enforce conversion to stackitem.InteropInterface,
	// but compiler still checks these notifications against expected manifest.
	t.Run("good, multiple types, check the conversion order", func(t *testing.T) {
		check(t, methodWithoutEllipsis, []smartcontract.ParamType{
			smartcontract.IntegerType,
			smartcontract.BoolType,
			smartcontract.ByteArrayType,
			smartcontract.PublicKeyType,
			smartcontract.Hash160Type,
			smartcontract.AnyType, // leave initial type
			smartcontract.StringType,
		}, []stackitem.Type{
			stackitem.IntegerT,
			stackitem.BooleanT,
			stackitem.ByteArrayT,
			stackitem.ByteArrayT,
			stackitem.ByteArrayT,
			stackitem.IntegerT, // leave initial type
			stackitem.ByteArrayT,
		}, false)
	})
	t.Run("with ellipsis, do not emit conversion code", func(t *testing.T) {
		checkSingleType(t, methodWithEllipsis, smartcontract.IntegerType, stackitem.IntegerT)
		checkSingleType(t, methodWithEllipsis, smartcontract.BoolType, stackitem.IntegerT)
		checkSingleType(t, methodWithEllipsis, smartcontract.ByteArrayType, stackitem.IntegerT)
	})
	t.Run("no events check => no conversion code", func(t *testing.T) {
		checkSingleType(t, methodWithoutEllipsis, smartcontract.PublicKeyType, stackitem.IntegerT, true)
	})
}