package tests

import (
	"path"
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
	"git.frostfs.info/TrueCloudLab/frostfs-contract/container"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
	"github.com/nspcc-dev/neo-go/pkg/neotest"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/stretchr/testify/require"
)

const alphabetPath = "../alphabet"

func deployAlphabetContract(t *testing.T, e *neotest.Executor, addrNetmap, addrProxy util.Uint160, name string, index, total int64) util.Uint160 {
	c := neotest.CompileFile(t, e.CommitteeHash, alphabetPath, path.Join(alphabetPath, "config.yml"))

	args := make([]any, 5)
	args[0] = addrNetmap
	args[1] = addrProxy
	args[2] = name
	args[3] = index
	args[4] = total

	e.DeployContract(t, c, args)
	return c.Hash
}

func newAlphabetInvoker(t *testing.T) (*neotest.Executor, *neotest.ContractInvoker) {
	e := newExecutor(t)

	ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
	ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
	ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
	ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
	ctrProxy := neotest.CompileFile(t, e.CommitteeHash, proxyPath, path.Join(proxyPath, "config.yml"))

	e.DeployContract(t, ctrNNS, nil)
	deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash,
		container.RegistrationFeeKey, int64(containerFee),
		container.AliasFeeKey, int64(containerAliasFee))
	deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
	deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash)
	deployProxyContract(t, e, ctrNetmap.Hash)
	hash := deployAlphabetContract(t, e, ctrNetmap.Hash, ctrProxy.Hash, "Az", 0, 1)

	alphabet := getAlphabetAcc(t, e)

	setAlphabetRole(t, e, alphabet.PrivateKey().PublicKey().Bytes())

	return e, e.CommitteeInvoker(hash)
}

func TestEmit(t *testing.T) {
	_, c := newAlphabetInvoker(t)

	const method = "emit"

	alphabet := getAlphabetAcc(t, c.Executor)

	cCommittee := c.WithSigners(neotest.NewSingleSigner(alphabet))
	cCommittee.InvokeFail(t, "no gas to emit", method)

	transferNeoToContract(t, c)

	cCommittee.Invoke(t, stackitem.Null{}, method)

	notAlphabet := c.NewAccount(t)
	cNotAlphabet := c.WithSigners(notAlphabet)

	cNotAlphabet.InvokeFail(t, "invalid invoker", method)
}

func TestVote(t *testing.T) {
	e, c := newAlphabetInvoker(t)

	const method = "vote"

	newAlphabet := c.NewAccount(t)
	newAlphabetPub, ok := vm.ParseSignatureContract(newAlphabet.Script())
	require.True(t, ok)
	cNewAlphabet := c.WithSigners(newAlphabet)

	cNewAlphabet.InvokeFail(t, common.ErrAlphabetWitnessFailed, method, int64(0), []any{newAlphabetPub})
	c.InvokeFail(t, "invalid epoch", method, int64(1), []any{newAlphabetPub})

	setAlphabetRole(t, e, newAlphabetPub)
	transferNeoToContract(t, c)

	neoSH := e.NativeHash(t, nativenames.Neo)
	neoInvoker := c.CommitteeInvoker(neoSH)

	gasSH := e.NativeHash(t, nativenames.Gas)
	gasInvoker := e.CommitteeInvoker(gasSH)

	res, err := gasInvoker.TestInvoke(t, "balanceOf", gasInvoker.Committee.ScriptHash())
	require.NoError(t, err)

	// transfer some GAS to the new alphabet node
	gasInvoker.Invoke(t, stackitem.NewBool(true), "transfer", gasInvoker.Committee.ScriptHash(), newAlphabet.ScriptHash(), res.Top().BigInt().Int64()/2, nil)

	newInvoker := neoInvoker.WithSigners(newAlphabet)

	newInvoker.Invoke(t, stackitem.NewBool(true), "registerCandidate", newAlphabetPub)
	c.Invoke(t, stackitem.Null{}, method, int64(0), []any{newAlphabetPub})

	// wait one block util
	// a new committee is accepted
	c.AddNewBlock(t)

	cNewAlphabet.Invoke(t, stackitem.Null{}, "emit")
	c.InvokeFail(t, "invalid invoker", "emit")
}

func transferNeoToContract(t *testing.T, invoker *neotest.ContractInvoker) {
	neoSH, err := invoker.Chain.GetNativeContractScriptHash(nativenames.Neo)
	require.NoError(t, err)

	neoInvoker := invoker.CommitteeInvoker(neoSH)

	res, err := neoInvoker.TestInvoke(t, "balanceOf", neoInvoker.Committee.ScriptHash())
	require.NoError(t, err)

	// transfer all NEO to alphabet contract
	neoInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoInvoker.Committee.ScriptHash(), invoker.Hash, res.Top().BigInt().Int64(), nil)
}

func setAlphabetRole(t *testing.T, e *neotest.Executor, new []byte) {
	designSH, err := e.Chain.GetNativeContractScriptHash(nativenames.Designation)
	require.NoError(t, err)

	designInvoker := e.CommitteeInvoker(designSH)

	// set committee as NeoFSAlphabet
	designInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int64(noderoles.NeoFSAlphabet), []any{new})
}

func getAlphabetAcc(t *testing.T, e *neotest.Executor) *wallet.Account {
	multi, ok := e.Committee.(neotest.MultiSigner)
	require.True(t, ok)

	return multi.Single(0).Account()
}