package smartcontract_test

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

	"git.marketally.com/tutus-one/tutus-chain/internal/testcli"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"github.com/stretchr/testify/require"
)

func TestGenerate(t *testing.T) {
	m := manifest.NewManifest("MyContract")
	m.ABI.Methods = append(m.ABI.Methods,
		manifest.Method{
			Name:       manifest.MethodDeploy,
			ReturnType: smartcontract.VoidType,
		},
		manifest.Method{
			Name: "sum",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("first", smartcontract.IntegerType),
				manifest.NewParameter("second", smartcontract.IntegerType),
			},
			ReturnType: smartcontract.IntegerType,
		},
		manifest.Method{
			Name: "sum", // overloaded method
			Parameters: []manifest.Parameter{
				manifest.NewParameter("first", smartcontract.IntegerType),
				manifest.NewParameter("second", smartcontract.IntegerType),
				manifest.NewParameter("third", smartcontract.IntegerType),
			},
			ReturnType: smartcontract.IntegerType,
		},
		manifest.Method{
			Name:       "sum3",
			Parameters: []manifest.Parameter{},
			ReturnType: smartcontract.IntegerType,
			Safe:       true,
		},
		manifest.Method{
			Name: "zum",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("type", smartcontract.IntegerType),
				manifest.NewParameter("typev", smartcontract.IntegerType),
				manifest.NewParameter("func", smartcontract.IntegerType),
			},
			ReturnType: smartcontract.IntegerType,
		},
		manifest.Method{
			Name: "justExecute",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("arr", smartcontract.ArrayType),
			},
			ReturnType: smartcontract.VoidType,
		},
		manifest.Method{
			Name:       "getPublicKey",
			Parameters: nil,
			ReturnType: smartcontract.PublicKeyType,
		},
		manifest.Method{
			Name: "otherTypes",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("ctr", smartcontract.Hash160Type),
				manifest.NewParameter("tx", smartcontract.Hash256Type),
				manifest.NewParameter("sig", smartcontract.SignatureType),
				manifest.NewParameter("data", smartcontract.AnyType),
			},
			ReturnType: smartcontract.BoolType,
		},
		manifest.Method{
			Name: "searchStorage",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("ctx", smartcontract.InteropInterfaceType),
			},
			ReturnType: smartcontract.InteropInterfaceType,
		},
		manifest.Method{
			Name: "getFromMap",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("intMap", smartcontract.MapType),
				manifest.NewParameter("indices", smartcontract.ArrayType),
			},
			ReturnType: smartcontract.ArrayType,
		},
		manifest.Method{
			Name: "doSomething",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("bytes", smartcontract.ByteArrayType),
				manifest.NewParameter("str", smartcontract.StringType),
			},
			ReturnType: smartcontract.InteropInterfaceType,
		},
		manifest.Method{
			Name:       "getBlockWrapper",
			Parameters: []manifest.Parameter{},
			ReturnType: smartcontract.InteropInterfaceType,
		},
		manifest.Method{
			Name: "myFunc",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("in", smartcontract.MapType),
			},
			ReturnType: smartcontract.ArrayType,
		})

	manifestFile := filepath.Join(t.TempDir(), "manifest.json")
	outFile := filepath.Join(t.TempDir(), "out.go")

	rawManifest, err := json.Marshal(m)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))

	h := util.Uint160{
		0x04, 0x08, 0x15, 0x16, 0x23, 0x42, 0x43, 0x44, 0x00, 0x01,
		0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF, 0x03, 0x04,
	}
	e := testcli.NewExecutor(t, false)

	rawCfg := `package: wrapper
hash: ` + h.StringLE() + `
overrides:
    searchStorage.ctx: storage.Context
    searchStorage: iterator.Iterator
    getFromMap.intMap: "map[string]int"
    getFromMap.indices: "[]string"
    getFromMap: "[]int"
    getBlockWrapper: ledger.Block
    myFunc.in: "map[int]github.com/heyitsme/mycontract.Input"
    myFunc: "[]github.com/heyitsme/mycontract.Output"
callflags:
    doSomething: ReadStates
`
	cfgPath := filepath.Join(t.TempDir(), "binding.yml")
	require.NoError(t, os.WriteFile(cfgPath, []byte(rawCfg), os.ModePerm))

	e.Run(t, []string{"", "contract", "generate-wrapper",
		"--manifest", manifestFile,
		"--config", cfgPath,
		"--out", outFile,
		"--hash", h.StringLE(),
	}...)

	const expected = `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.

// Package wrapper contains wrappers for MyContract contract.
package wrapper

import (
	"github.com/heyitsme/mycontract"
	"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/iterator"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/native/ledger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/tutusinternal"
)

// Hash contains contract hash in big-endian form.
const Hash = "\x04\x08\x15\x16\x23\x42\x43\x44\x00\x01\xca\xfe\xba\xbe\xde\xad\xbe\xef\x03\x04"

// Sum invokes ` + "`sum`" + ` method of contract.
func Sum(first int, second int) int {
	return tutusinternal.CallWithToken(Hash, "sum", int(contract.All), first, second).(int)
}

// Sum2 invokes ` + "`sum`" + ` method of contract.
func Sum2(first int, second int, third int) int {
	return tutusinternal.CallWithToken(Hash, "sum", int(contract.All), first, second, third).(int)
}

// Sum3 invokes ` + "`sum3`" + ` method of contract.
func Sum3() int {
	return tutusinternal.CallWithToken(Hash, "sum3", int(contract.ReadOnly)).(int)
}

// Zum invokes ` + "`zum`" + ` method of contract.
func Zum(typev int, typev_ int, funcv int) int {
	return tutusinternal.CallWithToken(Hash, "zum", int(contract.All), typev, typev_, funcv).(int)
}

// JustExecute invokes ` + "`justExecute`" + ` method of contract.
func JustExecute(arr []any) {
	tutusinternal.CallWithTokenNoRet(Hash, "justExecute", int(contract.All), arr)
}

// GetPublicKey invokes ` + "`getPublicKey`" + ` method of contract.
func GetPublicKey() interop.PublicKey {
	return tutusinternal.CallWithToken(Hash, "getPublicKey", int(contract.All)).(interop.PublicKey)
}

// OtherTypes invokes ` + "`otherTypes`" + ` method of contract.
func OtherTypes(ctr interop.Hash160, tx interop.Hash256, sig interop.Signature, data any) bool {
	return tutusinternal.CallWithToken(Hash, "otherTypes", int(contract.All), ctr, tx, sig, data).(bool)
}

// SearchStorage invokes ` + "`searchStorage`" + ` method of contract.
func SearchStorage(ctx storage.Context) iterator.Iterator {
	return tutusinternal.CallWithToken(Hash, "searchStorage", int(contract.All), ctx).(iterator.Iterator)
}

// GetFromMap invokes ` + "`getFromMap`" + ` method of contract.
func GetFromMap(intMap map[string]int, indices []string) []int {
	return tutusinternal.CallWithToken(Hash, "getFromMap", int(contract.All), intMap, indices).([]int)
}

// DoSomething invokes ` + "`doSomething`" + ` method of contract.
func DoSomething(bytes []byte, str string) any {
	return tutusinternal.CallWithToken(Hash, "doSomething", int(contract.ReadStates), bytes, str).(any)
}

// GetBlockWrapper invokes ` + "`getBlockWrapper`" + ` method of contract.
func GetBlockWrapper() ledger.Block {
	return tutusinternal.CallWithToken(Hash, "getBlockWrapper", int(contract.All)).(ledger.Block)
}

// MyFunc invokes ` + "`myFunc`" + ` method of contract.
func MyFunc(in map[int]mycontract.Input) []mycontract.Output {
	return tutusinternal.CallWithToken(Hash, "myFunc", int(contract.All), in).([]mycontract.Output)
}
`

	data, err := os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, expected, string(data))

	e.Run(t, []string{"", "contract", "generate-wrapper",
		"--manifest", manifestFile,
		"--config", cfgPath,
		"--out", outFile,
	}...)
	expectedWithDynamicHash := `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.

// Package wrapper contains wrappers for MyContract contract.
package wrapper

import (
	"github.com/heyitsme/mycontract"
	"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/iterator"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/native/ledger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/storage"
)

// Contract represents the MyContract smart contract.
type Contract struct {
	Hash interop.Hash160
}

// NewContract returns a new Contract instance with the specified hash.
func NewContract(hash interop.Hash160) Contract {
	return Contract{Hash: hash}
}

// Sum invokes ` + "`sum`" + ` method of contract.
func (c Contract) Sum(first int, second int) int {
	return contract.Call(c.Hash, "sum", contract.All, first, second).(int)
}

// Sum2 invokes ` + "`sum`" + ` method of contract.
func (c Contract) Sum2(first int, second int, third int) int {
	return contract.Call(c.Hash, "sum", contract.All, first, second, third).(int)
}

// Sum3 invokes ` + "`sum3`" + ` method of contract.
func (c Contract) Sum3() int {
	return contract.Call(c.Hash, "sum3", contract.ReadOnly).(int)
}

// Zum invokes ` + "`zum`" + ` method of contract.
func (c Contract) Zum(typev int, typev_ int, funcv int) int {
	return contract.Call(c.Hash, "zum", contract.All, typev, typev_, funcv).(int)
}

// JustExecute invokes ` + "`justExecute`" + ` method of contract.
func (c Contract) JustExecute(arr []any) {
	contract.Call(c.Hash, "justExecute", contract.All, arr)
}

// GetPublicKey invokes ` + "`getPublicKey`" + ` method of contract.
func (c Contract) GetPublicKey() interop.PublicKey {
	return contract.Call(c.Hash, "getPublicKey", contract.All).(interop.PublicKey)
}

// OtherTypes invokes ` + "`otherTypes`" + ` method of contract.
func (c Contract) OtherTypes(ctr interop.Hash160, tx interop.Hash256, sig interop.Signature, data any) bool {
	return contract.Call(c.Hash, "otherTypes", contract.All, ctr, tx, sig, data).(bool)
}

// SearchStorage invokes ` + "`searchStorage`" + ` method of contract.
func (c Contract) SearchStorage(ctx storage.Context) iterator.Iterator {
	return contract.Call(c.Hash, "searchStorage", contract.All, ctx).(iterator.Iterator)
}

// GetFromMap invokes ` + "`getFromMap`" + ` method of contract.
func (c Contract) GetFromMap(intMap map[string]int, indices []string) []int {
	return contract.Call(c.Hash, "getFromMap", contract.All, intMap, indices).([]int)
}

// DoSomething invokes ` + "`doSomething`" + ` method of contract.
func (c Contract) DoSomething(bytes []byte, str string) any {
	return contract.Call(c.Hash, "doSomething", contract.ReadStates, bytes, str).(any)
}

// GetBlockWrapper invokes ` + "`getBlockWrapper`" + ` method of contract.
func (c Contract) GetBlockWrapper() ledger.Block {
	return contract.Call(c.Hash, "getBlockWrapper", contract.All).(ledger.Block)
}

// MyFunc invokes ` + "`myFunc`" + ` method of contract.
func (c Contract) MyFunc(in map[int]mycontract.Input) []mycontract.Output {
	return contract.Call(c.Hash, "myFunc", contract.All, in).([]mycontract.Output)
}
`
	data, err = os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, expectedWithDynamicHash, string(data))
}

func TestGenerateValidPackageName(t *testing.T) {
	m := manifest.NewManifest("My space\tcontract")
	m.ABI.Methods = append(m.ABI.Methods,
		manifest.Method{
			Name:       "get",
			Parameters: []manifest.Parameter{},
			ReturnType: smartcontract.IntegerType,
			Safe:       true,
		},
	)

	manifestFile := filepath.Join(t.TempDir(), "manifest.json")
	outFile := filepath.Join(t.TempDir(), "out.go")

	rawManifest, err := json.Marshal(m)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))

	h := util.Uint160{
		0x04, 0x08, 0x15, 0x16, 0x23, 0x42, 0x43, 0x44, 0x00, 0x01,
		0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF, 0x03, 0x04,
	}
	e := testcli.NewExecutor(t, false)
	e.Run(t, []string{"", "contract", "generate-wrapper",
		"--manifest", manifestFile,
		"--out", outFile,
		"--hash", "0x" + h.StringLE(),
	}...)

	data, err := os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, `// Code generated by neo-go contract generate-wrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.

// Package myspacecontract contains wrappers for My space	contract contract.
package myspacecontract

import (
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/contract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/interop/tutusinternal"
)

// Hash contains contract hash in big-endian form.
const Hash = "\x04\x08\x15\x16\x23\x42\x43\x44\x00\x01\xca\xfe\xba\xbe\xde\xad\xbe\xef\x03\x04"

// Get invokes `+"`get`"+` method of contract.
func Get() int {
	return tutusinternal.CallWithToken(Hash, "get", int(contract.ReadOnly)).(int)
}
`, string(data))
	e.Run(t, []string{"", "contract", "generate-rpcwrapper",
		"--manifest", manifestFile,
		"--out", outFile,
		"--hash", "0x" + h.StringLE(),
	}...)

	data, err = os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, `// Code generated by neo-go contract generate-rpcwrapper --manifest <file.json> --out <file.go> [--hash <hash>] [--config <config>]; DO NOT EDIT.

// Package myspacecontract contains RPC wrappers for My space	contract contract.
package myspacecontract

import (
	"git.marketally.com/tutus-one/tutus-chain/pkg/rpcclient/unwrap"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutusrpc/result"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"math/big"
)

// Hash contains contract hash.
var Hash = util.Uint160{0x4, 0x8, 0x15, 0x16, 0x23, 0x42, 0x43, 0x44, 0x0, 0x1, 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, 0x3, 0x4}

// Invoker is used by ContractReader to call various safe methods.
type Invoker interface {
	Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error)
}

// ContractReader implements safe contract methods.
type ContractReader struct {
	invoker Invoker
	hash    util.Uint160
}

// NewReader creates an instance of ContractReader using Hash and the given Invoker.
func NewReader(invoker Invoker) *ContractReader {
	var hash = Hash
	return &ContractReader{invoker, hash}
}

// Get invokes `+"`get`"+` method of contract.
func (c *ContractReader) Get() (*big.Int, error) {
	return unwrap.BigInt(c.invoker.Call(c.hash, "get"))
}
`, string(data))
}

// rewriteExpectedOutputs denotes whether expected output files should be rewritten
// for TestGenerateRPCBindings and TestAssistedRPCBindings.
const rewriteExpectedOutputs = false

func TestGenerateRPCBindings(t *testing.T) {
	tmpDir := t.TempDir()
	e := testcli.NewExecutor(t, false)

	var checkBinding = func(manifest string, hash string, good string) {
		t.Run(manifest, func(t *testing.T) {
			outFile := filepath.Join(tmpDir, "out.go")
			e.Run(t, []string{"", "contract", "generate-rpcwrapper",
				"--manifest", manifest,
				"--out", outFile,
				"--hash", hash,
			}...)

			data, err := os.ReadFile(outFile)
			require.NoError(t, err)
			data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
			if rewriteExpectedOutputs {
				require.NoError(t, os.WriteFile(good, data, os.ModePerm))
			} else {
				expected, err := os.ReadFile(good)
				require.NoError(t, err)
				expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
				require.Equal(t, string(expected), string(data))
			}
		})
	}

	checkBinding(filepath.Join("testdata", "nex", "nex.manifest.json"),
		"0xa2a67f09e8cf22c6bfd5cea24adc0f4bf0a11aa8",
		filepath.Join("testdata", "nex", "nex.go"))
	checkBinding(filepath.Join("testdata", "nameservice", "nns.manifest.json"),
		"0x50ac1c37690cc2cfc594472833cf57505d5f46de",
		filepath.Join("testdata", "nameservice", "nns.go"))
	checkBinding(filepath.Join("testdata", "gas", "gas.manifest.json"),
		"0xe895bb93700f57a3d47964dd6f14aa5e8615e869",
		filepath.Join("testdata", "gas", "gas.go"))
	checkBinding(filepath.Join("testdata", "verifyrpc", "verify.manifest.json"),
		"0x00112233445566778899aabbccddeeff00112233",
		filepath.Join("rpcbindings", "verify", "rpcbindings_test.go"))
	checkBinding(filepath.Join("testdata", "nonepiter", "iter.manifest.json"),
		"0x00112233445566778899aabbccddeeff00112233",
		filepath.Join("testdata", "nonepiter", "iter.go"))

	require.False(t, rewriteExpectedOutputs)
}

func TestAssistedRPCBindings(t *testing.T) {
	tmpDir := t.TempDir()
	e := testcli.NewExecutor(t, false)

	var checkBinding = func(source, configFile, expectedFile string, hasDefinedHash, guessEventTypes bool, suffix ...string) {
		testName := source
		if len(suffix) != 0 {
			testName += suffix[0]
		}
		testName += fmt.Sprintf(", predefined hash: %t", hasDefinedHash)
		t.Run(testName, func(t *testing.T) {
			outFile := filepath.Join(tmpDir, "out.go")
			if configFile == "" {
				if len(suffix) != 0 {
					configFile = filepath.Join(source, "config_"+suffix[0]+".yml")
				} else {
					configFile = filepath.Join(source, "config.yml")
				}
			}
			if expectedFile == "" {
				// Expected bindings are moved out of testdata to be able to perform static code analysis on them.
				expectedRoot := strings.Replace(source, "testdata", ".", 1)
				expectedFile = filepath.Join(expectedRoot, "rpcbindings", "rpcbindings_test.go")
				if len(suffix) != 0 {
					expectedFile = filepath.Join(expectedRoot, "rpcbindings", suffix[0], "rpcbindings_test.go")
				} else if !hasDefinedHash {
					expectedFile = filepath.Join(expectedRoot, "rpcbindings", "dynamic_hash", "rpcbindings_test.go")
				}
			}
			manifestF := filepath.Join(tmpDir, "manifest.json")
			bindingF := filepath.Join(tmpDir, "binding.yml")
			nefF := filepath.Join(tmpDir, "out.nef")
			cmd := []string{"", "contract", "compile",
				"--in", source,
				"--config", configFile,
				"--manifest", manifestF,
				"--bindings", bindingF,
				"--out", nefF,
			}
			if guessEventTypes {
				cmd = append(cmd, "--guess-eventtypes")
			}
			e.Run(t, cmd...)

			cmds := []string{"", "contract", "generate-rpcwrapper",
				"--config", bindingF,
				"--manifest", manifestF,
				"--out", outFile,
			}
			if hasDefinedHash {
				cmds = append(cmds, "--hash", "0x00112233445566778899aabbccddeeff00112233")
			}
			e.Run(t, cmds...)

			data, err := os.ReadFile(outFile)
			require.NoError(t, err)
			data = bytes.ReplaceAll(data, []byte("\r"), []byte{}) // Windows.
			if rewriteExpectedOutputs {
				require.NoError(t, os.WriteFile(expectedFile, data, os.ModePerm))
			} else {
				expected, err := os.ReadFile(expectedFile)
				require.NoError(t, err)
				expected = bytes.ReplaceAll(expected, []byte("\r"), []byte{}) // Windows.
				require.Equal(t, string(expected), string(data))
			}
		})
	}

	for _, hasDefinedHash := range []bool{true, false} {
		checkBinding(filepath.Join("testdata", "rpcbindings", "types"), "", "", hasDefinedHash, false)
		checkBinding(filepath.Join("testdata", "rpcbindings", "structs"), "", "", hasDefinedHash, false)
		checkBinding(filepath.Join("testdata", "rpcbindings", "royalty"), "", "", hasDefinedHash, false)
	}
	checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, false)
	checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, false, "extended")
	checkBinding(filepath.Join("testdata", "rpcbindings", "notifications"), "", "", true, true, "guessed")

	checkBinding(filepath.Join("..", "..", "examples", "nft-d"), filepath.Join("..", "..", "examples", "nft-d", "nft.yml"), filepath.Join("rpcbindings", "nft-d", "dynamic_hash", "rpcbindings_test.go"), false, false)
	checkBinding(filepath.Join("..", "..", "examples", "nft-d"), filepath.Join("..", "..", "examples", "nft-d", "nft.yml"), filepath.Join("rpcbindings", "nft-d", "rpcbindings_test.go"), true, true)
	checkBinding(filepath.Join("..", "..", "examples", "nft-nd"), filepath.Join("..", "..", "examples", "nft-nd", "nft.yml"), filepath.Join("rpcbindings", "nft-nd", "dynamic_hash", "rpcbindings_test.go"), false, false)
	checkBinding(filepath.Join("..", "..", "examples", "nft-nd"), filepath.Join("..", "..", "examples", "nft-nd", "nft.yml"), filepath.Join("rpcbindings", "nft-nd", "rpcbindings_test.go"), true, true)

	require.False(t, rewriteExpectedOutputs)
}

func TestGenerate_Errors(t *testing.T) {
	e := testcli.NewExecutor(t, false)
	args := []string{"tutus", "contract", "generate-wrapper"}

	t.Run("invalid hash", func(t *testing.T) {
		e.RunWithErrorCheckExit(t, "invalid contract hash", append(args, "--hash", "xxx", "--manifest", "yyy", "--out", "zzz")...)
	})
	t.Run("missing manifest argument", func(t *testing.T) {
		e.RunWithErrorCheck(t, `Required flag "manifest" not set`, append(args, "--hash", util.Uint160{}.StringLE(), "--out", "zzz")...)
	})
	t.Run("missing manifest file", func(t *testing.T) {
		e.RunWithErrorCheckExit(t, "can't read contract manifest", append(args, "--manifest", "notexists", "--hash", util.Uint160{}.StringLE(), "--out", "zzz")...)
	})
	t.Run("empty manifest", func(t *testing.T) {
		manifestFile := filepath.Join(t.TempDir(), "invalid.json")
		require.NoError(t, os.WriteFile(manifestFile, []byte("[]"), os.ModePerm))
		e.RunWithErrorCheckExit(t, "json: cannot unmarshal array into Go value of type manifest.Manifest", append(args, "--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(), "--out", "zzz")...)
	})
	t.Run("invalid manifest", func(t *testing.T) {
		manifestFile := filepath.Join(t.TempDir(), "invalid.json")
		m := manifest.NewManifest("MyContract") // no methods
		rawManifest, err := json.Marshal(m)
		require.NoError(t, err)
		require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))
		e.RunWithErrorCheckExit(t, "ABI: no methods", append(args, "--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(), "--out", "zzz")...)
	})

	manifestFile := filepath.Join(t.TempDir(), "manifest.json")
	m := manifest.NewManifest("MyContract")
	m.ABI.Methods = append(m.ABI.Methods, manifest.Method{
		Name:       "method0",
		Offset:     0,
		ReturnType: smartcontract.AnyType,
		Safe:       true,
	})
	rawManifest, err := json.Marshal(m)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))

	t.Run("missing config", func(t *testing.T) {
		e.RunWithErrorCheckExit(t, "can't read config file", append(args, "--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(),
			"--config", filepath.Join(t.TempDir(), "not.exists.yml"), "--out", "zzz")...)
	})
	t.Run("invalid config", func(t *testing.T) {
		rawCfg := `package: wrapper
callflags:
    someFunc: ReadSometimes 
`
		cfgPath := filepath.Join(t.TempDir(), "binding.yml")
		require.NoError(t, os.WriteFile(cfgPath, []byte(rawCfg), os.ModePerm))

		e.RunWithErrorCheckExit(t, "can't parse config file", append(args, "--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(),
			"--config", cfgPath, "--out", "zzz")...)
	})
}

func TestCompile_GuessEventTypes(t *testing.T) {
	e := testcli.NewExecutor(t, false)
	check := func(t *testing.T, source string, expectedErrText string) {
		tmpDir := t.TempDir()
		configFile := filepath.Join(source, "invalid.yml")
		manifestF := filepath.Join(tmpDir, "invalid.manifest.json")
		bindingF := filepath.Join(tmpDir, "invalid.binding.yml")
		nefF := filepath.Join(tmpDir, "invalid.out.nef")
		cmd := []string{"", "contract", "compile",
			"--in", source,
			"--config", configFile,
			"--manifest", manifestF,
			"--bindings", bindingF,
			"--out", nefF,
			"--guess-eventtypes",
		}
		e.RunWithErrorCheckExit(t, expectedErrText, cmd...)
	}

	t.Run("not declared in manifest", func(t *testing.T) {
		check(t, filepath.Join("testdata", "rpcbindings", "invalid1"), "inconsistent usages of event `Non declared event`: not declared in the contract config")
	})
	t.Run("invalid number of params", func(t *testing.T) {
		check(t, filepath.Join("testdata", "rpcbindings", "invalid2"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1")
	})
	/*
		// TODO: this on is a controversial one. If event information is provided in the config file, then conversion code
		// will be emitted by the compiler according to the parameter type provided via config. Thus, we can be sure that
		// either event parameter has the type specified in the config file or the execution of the contract will fail.
		// Thus, this testcase is always failing (no compilation error occurs).
		// Question: do we want to compare `RealType` of the emitted parameter with the one expected in the manifest?
		t.Run("SC parameter type mismatch", func(t *testing.T) {
			check(t, filepath.Join("testdata", "rpcbindings", "invalid3"), "inconsistent usages of event `SomeEvent` against config: number of params mismatch: 2 vs 1")
		})
	*/
	t.Run("extended types mismatch", func(t *testing.T) {
		check(t, filepath.Join("testdata", "rpcbindings", "invalid4"), "inconsistent usages of event `SomeEvent`: extended type of param #0 mismatch")
	})
	t.Run("named types redeclare", func(t *testing.T) {
		check(t, filepath.Join("testdata", "rpcbindings", "invalid5"), "configured declared named type intersects with the contract's one: `invalid5.NamedStruct`")
	})
}

func TestGenerateRPCBindings_Errors(t *testing.T) {
	e := testcli.NewExecutor(t, false)
	t.Run("duplicating resulting fields", func(t *testing.T) {
		check := func(t *testing.T, packageName string, autogen bool, expectedError string) {
			tmpDir := t.TempDir()
			source := filepath.Join("testdata", "rpcbindings", packageName)
			configFile := filepath.Join(source, "invalid.yml")
			out := filepath.Join(tmpDir, "rpcbindings.out")
			manifestF := filepath.Join(tmpDir, "manifest.json")
			bindingF := filepath.Join(tmpDir, "binding.yml")
			nefF := filepath.Join(tmpDir, "out.nef")
			cmd := []string{"", "contract", "compile",
				"--in", source,
				"--config", configFile,
				"--manifest", manifestF,
				"--bindings", bindingF,
				"--out", nefF,
			}
			if autogen {
				cmd = append(cmd, "--guess-eventtypes")
			}
			e.Run(t, cmd...)

			cmds := []string{"", "contract", "generate-rpcwrapper",
				"--config", bindingF,
				"--manifest", manifestF,
				"--out", out,
			}
			e.RunWithErrorCheckExit(t, expectedError, cmds...)
		}

		t.Run("event", func(t *testing.T) {
			check(t, "invalid6", false, "error during generation: named type `SomeStruct` has two fields with identical resulting binding name `Field`")
		})
		t.Run("autogen event", func(t *testing.T) {
			check(t, "invalid7", true, "error during generation: named type `invalid7.SomeStruct` has two fields with identical resulting binding name `Field`")
		})
		t.Run("struct", func(t *testing.T) {
			check(t, "invalid8", false, "error during generation: named type `invalid8.SomeStruct` has two fields with identical resulting binding name `Field`")
		})
	})
}
