package wallet_test

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

	"github.com/nspcc-dev/neo-go/internal/testcli"
	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
	"github.com/stretchr/testify/require"
)

// Test signing of multisig transactions.
// 1. Transfer funds to a created multisig address.
// 2. Transfer from multisig to another account.
func TestSignMultisigTx(t *testing.T) {
	e := testcli.NewExecutor(t, true)

	privs, pubs := testcli.GenerateKeys(t, 3)
	script, err := smartcontract.CreateMultiSigRedeemScript(2, pubs)
	require.NoError(t, err)
	multisigHash := hash.Hash160(script)
	multisigAddr := address.Uint160ToString(multisigHash)

	// Create 2 wallets participating in multisig.
	tmpDir := t.TempDir()
	wallet1Path := filepath.Join(tmpDir, "multiWallet1.json")
	wallet2Path := filepath.Join(tmpDir, "multiWallet2.json")

	addAccount := func(w string, wif string) {
		e.Run(t, "neo-go", "wallet", "init", "--wallet", w)
		e.In.WriteString("acc\rpass\rpass\r")
		e.Run(t, "neo-go", "wallet", "import-multisig",
			"--wallet", w,
			"--wif", wif,
			"--min", "2",
			hex.EncodeToString(pubs[0].Bytes()),
			hex.EncodeToString(pubs[1].Bytes()),
			hex.EncodeToString(pubs[2].Bytes()))
	}
	addAccount(wallet1Path, privs[0].WIF())
	addAccount(wallet2Path, privs[1].WIF())

	// Transfer funds to the multisig.
	e.In.WriteString("one\r")
	e.Run(t, "neo-go", "wallet", "nep17", "multitransfer",
		"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
		"--wallet", testcli.ValidatorWallet,
		"--from", testcli.ValidatorAddr,
		"--force",
		"NEO:"+multisigAddr+":4",
		"GAS:"+multisigAddr+":1")
	e.CheckTxPersisted(t)

	// Sign and transfer funds to another account.
	priv, err := keys.NewPrivateKey()
	require.NoError(t, err)

	t.Run("bad cases", func(t *testing.T) {
		txPath := filepath.Join(tmpDir, "multisigtx.json")
		t.Cleanup(func() {
			os.Remove(txPath)
		})
		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "nep17", "transfer",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet1Path, "--from", multisigAddr,
			"--to", priv.Address(), "--token", "NEO", "--amount", "1",
			"--out", txPath)

		// missing wallet
		e.RunWithError(t, "neo-go", "wallet", "sign")

		// missing in
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--wallet", wallet2Path)

		// missing address
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--wallet", wallet2Path,
			"--in", txPath)

		// invalid address
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--wallet", wallet2Path, "--address", util.Uint160{}.StringLE(),
			"--in", txPath)

		// invalid out
		e.In.WriteString("pass\r")
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath, "--out", t.TempDir())

		// invalid RPC endpoint
		e.In.WriteString("pass\r")
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--rpc-endpoint", "http://not-an-address",
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath)
	})

	// Create transaction and save it for further multisigning.
	txPath := filepath.Join(tmpDir, "multisigtx.json")
	t.Cleanup(func() {
		os.Remove(txPath)
	})
	e.In.WriteString("pass\r")
	e.Run(t, "neo-go", "wallet", "nep17", "transfer",
		"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
		"--wallet", wallet1Path, "--from", multisigAddr,
		"--to", priv.Address(), "--token", "NEO", "--amount", "1",
		"--out", txPath)

	simplePriv, err := keys.NewPrivateKey()
	require.NoError(t, err)
	{ // Simple signer, not in signers.
		e.In.WriteString("acc\rpass\rpass\r")
		e.Run(t, "neo-go", "wallet", "import",
			"--wallet", wallet1Path,
			"--wif", simplePriv.WIF())
		t.Run("sign with missing signer", func(t *testing.T) {
			e.In.WriteString("pass\r")
			e.RunWithError(t, "neo-go", "wallet", "sign",
				"--wallet", wallet1Path, "--address", simplePriv.Address(),
				"--in", txPath, "--out", txPath)
		})
	}

	t.Run("test invoke", func(t *testing.T) {
		t.Run("missing file", func(t *testing.T) {
			e.RunWithError(t, "neo-go", "util", "txdump")
			fmt.Println(e.Out.String())
		})

		t.Run("no invoke", func(t *testing.T) {
			e.Run(t, "neo-go", "util", "txdump", txPath)
			e.CheckTxTestInvokeOutput(t, 11)
			e.CheckEOF(t)
		})

		t.Run("excessive parameters", func(t *testing.T) {
			e.RunWithError(t, "neo-go", "util", "txdump",
				"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
				txPath, "garbage")
		})
		e.Run(t, "neo-go", "util", "txdump",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			txPath)
		e.CheckTxTestInvokeOutput(t, 11)
		res := new(result.Invoke)
		require.NoError(t, json.Unmarshal(e.Out.Bytes(), res))
		require.Equal(t, vmstate.Halt.String(), res.State, res.FaultException)
	})

	t.Run("console output", func(t *testing.T) {
		oldIn, err := os.ReadFile(txPath)
		require.NoError(t, err)
		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "sign",
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath)
		newIn, err := os.ReadFile(txPath)
		require.NoError(t, err)
		require.Equal(t, oldIn, newIn)

		pcOld := new(context.ParameterContext)
		require.NoError(t, json.Unmarshal(oldIn, pcOld))

		jOut := e.Out.Bytes()
		pcNew := new(context.ParameterContext)
		require.NoError(t, json.Unmarshal(jOut, pcNew))

		require.Equal(t, pcOld.Type, pcNew.Type)
		require.Equal(t, pcOld.Network, pcNew.Network)
		require.Equal(t, pcOld.Verifiable, pcNew.Verifiable)
		require.Equal(t, pcOld.Items[multisigHash].Script, pcNew.Items[multisigHash].Script)
		// It's completely signed after this, so parameters have signatures now as well.
		require.NotEqual(t, pcOld.Items[multisigHash].Parameters, pcNew.Items[multisigHash].Parameters)
		require.NotEqual(t, pcOld.Items[multisigHash].Signatures, pcNew.Items[multisigHash].Signatures)
	})

	t.Run("sign, save and send", func(t *testing.T) {
		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "sign",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath, "--out", txPath)
		e.CheckTxPersisted(t)
	})
	t.Run("double-sign", func(t *testing.T) {
		e.In.WriteString("pass\r")
		e.RunWithError(t, "neo-go", "wallet", "sign",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath, "--out", txPath)
	})

	b, _ := e.Chain.GetGoverningTokenBalance(priv.GetScriptHash())
	require.Equal(t, big.NewInt(1), b)
	b, _ = e.Chain.GetGoverningTokenBalance(multisigHash)
	require.Equal(t, big.NewInt(3), b)

	t.Run("via invokefunction", func(t *testing.T) {
		h := deployVerifyContract(t, e)

		e.In.WriteString("acc\rpass\rpass\r")
		e.Run(t, "neo-go", "wallet", "import-deployed",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet1Path, "--wif", simplePriv.WIF(),
			"--contract", h.StringLE())

		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "contract", "invokefunction",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet1Path, "--address", multisigHash.StringLE(), // test with scripthash instead of address
			"--out", txPath,
			e.Chain.GoverningTokenHash().StringLE(), "transfer",
			"bytes:"+multisigHash.StringBE(),
			"bytes:"+priv.GetScriptHash().StringBE(),
			"int:1", "bytes:",
			"--", multisigHash.StringLE()+":"+"Global",
			h.StringLE(),
			simplePriv.GetScriptHash().StringLE(),
		)

		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "sign",
			"--wallet", wallet2Path, "--address", multisigAddr,
			"--in", txPath, "--out", txPath)

		// Simple signer, not in signers.
		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "sign",
			"--wallet", wallet1Path, "--address", simplePriv.Address(),
			"--in", txPath, "--out", txPath)

		// Contract.
		e.In.WriteString("pass\r")
		e.Run(t, "neo-go", "wallet", "sign",
			"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
			"--wallet", wallet1Path, "--address", address.Uint160ToString(h),
			"--in", txPath, "--out", txPath)
		tx, _ := e.CheckTxPersisted(t)
		require.Equal(t, 3, len(tx.Signers))

		b, _ := e.Chain.GetGoverningTokenBalance(priv.GetScriptHash())
		require.Equal(t, big.NewInt(2), b)
		b, _ = e.Chain.GetGoverningTokenBalance(multisigHash)
		require.Equal(t, big.NewInt(2), b)
	})
}

func deployVerifyContract(t *testing.T, e *testcli.Executor) util.Uint160 {
	return testcli.DeployContract(t, e, "../smartcontract/testdata/verify.go", "../smartcontract/testdata/verify.yml", testcli.ValidatorWallet, testcli.ValidatorAddr, testcli.ValidatorPass)
}