package smartcontract

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

	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/require"
	"github.com/urfave/cli"
)

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: "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: "emptyName",
			Parameters: []manifest.Parameter{
				manifest.NewParameter("", smartcontract.MapType),
			},
			ReturnType: smartcontract.AnyType,
		},
		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,
	}
	app := cli.NewApp()
	app.Commands = []cli.Command{generateWrapperCmd}

	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))

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

	const expected = `// Package wrapper contains wrappers for MyContract contract.
package wrapper

import (
	"github.com/heyitsme/mycontract"
	"github.com/nspcc-dev/neo-go/pkg/interop"
	"github.com/nspcc-dev/neo-go/pkg/interop/contract"
	"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
	"github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
	"github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
	"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)

// 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 neogointernal.CallWithToken(Hash, "sum", int(contract.All), first, second).(int)
}

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

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

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

// GetPublicKey invokes ` + "`getPublicKey`" + ` method of contract.
func GetPublicKey() interop.PublicKey {
	return neogointernal.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 interface{}) bool {
	return neogointernal.CallWithToken(Hash, "otherTypes", int(contract.All), ctr, tx, sig, data).(bool)
}

// EmptyName invokes ` + "`emptyName`" + ` method of contract.
func EmptyName(arg0 map[string]interface{}) interface{} {
	return neogointernal.CallWithToken(Hash, "emptyName", int(contract.All), arg0).(interface{})
}

// SearchStorage invokes ` + "`searchStorage`" + ` method of contract.
func SearchStorage(ctx storage.Context) iterator.Iterator {
	return neogointernal.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 neogointernal.CallWithToken(Hash, "getFromMap", int(contract.All), intMap, indices).([]int)
}

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

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

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

	data, err := os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, expected, 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,
		},
	)

	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,
	}
	app := cli.NewApp()
	app.Commands = []cli.Command{generateWrapperCmd}
	require.NoError(t, app.Run([]string{"", "generate-wrapper",
		"--manifest", manifestFile,
		"--out", outFile,
		"--hash", "0x" + h.StringLE(),
	}))

	data, err := os.ReadFile(outFile)
	require.NoError(t, err)
	require.Equal(t, `// Package myspacecontract contains wrappers for My space	contract contract.
package myspacecontract

import (
	"github.com/nspcc-dev/neo-go/pkg/interop/contract"
	"github.com/nspcc-dev/neo-go/pkg/interop/neogointernal"
)

// 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 neogointernal.CallWithToken(Hash, "get", int(contract.All)).(int)
}
`, string(data))
}

func TestGenerate_Errors(t *testing.T) {
	app := cli.NewApp()
	app.Commands = []cli.Command{generateWrapperCmd}
	app.ExitErrHandler = func(*cli.Context, error) {}

	checkError := func(t *testing.T, msg string, args ...string) {
		// cli.ExitError doesn't implement wraping properly, so we check for an error message.
		err := app.Run(append([]string{"", "generate-wrapper"}, args...))
		require.True(t, strings.Contains(err.Error(), msg), "got: %v", err)
	}
	t.Run("missing manifest argument", func(t *testing.T) {
		checkError(t, errNoManifestFile.Error())
	})
	t.Run("missing manifest file", func(t *testing.T) {
		checkError(t, "can't read contract manifest", "--manifest", "notexists")
	})
	t.Run("invalid manifest", func(t *testing.T) {
		manifestFile := filepath.Join(t.TempDir(), "invalid.json")
		require.NoError(t, os.WriteFile(manifestFile, []byte("[]"), os.ModePerm))
		checkError(t, "", "--manifest", manifestFile)
	})

	manifestFile := filepath.Join(t.TempDir(), "manifest.json")
	m := manifest.NewManifest("MyContract")
	rawManifest, err := json.Marshal(m)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(manifestFile, rawManifest, os.ModePerm))

	t.Run("invalid hash", func(t *testing.T) {
		checkError(t, "invalid contract hash", "--manifest", manifestFile, "--hash", "xxx")
	})
	t.Run("missing config", func(t *testing.T) {
		checkError(t, "can't read config file",
			"--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(),
			"--config", filepath.Join(t.TempDir(), "not.exists.yml"))
	})
	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))

		checkError(t, "can't parse config file",
			"--manifest", manifestFile, "--hash", util.Uint160{}.StringLE(),
			"--config", cfgPath)
	})
}