package core

import (
	"errors"
	"testing"

	"github.com/nspcc-dev/neo-go/internal/testchain"
	"github.com/nspcc-dev/neo-go/pkg/core/block"
	"github.com/nspcc-dev/neo-go/pkg/core/native"
	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
	"github.com/nspcc-dev/neo-go/pkg/vm"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

func (bc *Blockchain) setNodesByRole(t *testing.T, ok bool, r noderoles.Role, nodes keys.PublicKeys) {
	w := io.NewBufBinWriter()
	for _, pub := range nodes {
		emit.Bytes(w.BinWriter, pub.Bytes())
	}
	emit.Int(w.BinWriter, int64(len(nodes)))
	emit.Opcodes(w.BinWriter, opcode.PACK)
	emit.Int(w.BinWriter, int64(r))
	emit.Int(w.BinWriter, 2)
	emit.Opcodes(w.BinWriter, opcode.PACK)
	emit.AppCallNoArgs(w.BinWriter, bc.contracts.Designate.Hash, "designateAsRole", callflag.All)
	require.NoError(t, w.Err)
	tx := transaction.New(w.Bytes(), 0)
	tx.NetworkFee = 10_000_000
	tx.SystemFee = 10_000_000
	tx.ValidUntilBlock = 100
	tx.Signers = []transaction.Signer{
		{
			Account: testchain.MultisigScriptHash(),
			Scopes:  transaction.None,
		},
		{
			Account: testchain.CommitteeScriptHash(),
			Scopes:  transaction.CalledByEntry,
		},
	}
	require.NoError(t, testchain.SignTx(bc, tx))
	tx.Scripts = append(tx.Scripts, transaction.Witness{
		InvocationScript:   testchain.SignCommittee(tx),
		VerificationScript: testchain.CommitteeVerificationScript(),
	})
	require.NoError(t, bc.AddBlock(bc.newBlock(tx)))

	aer, err := bc.GetAppExecResults(tx.Hash(), trigger.Application)
	require.NoError(t, err)
	require.Equal(t, 1, len(aer))
	if ok {
		require.Equal(t, vm.HaltState, aer[0].VMState)
		require.Equal(t, 1, len(aer[0].Events))

		ev := aer[0].Events[0]
		require.Equal(t, bc.contracts.Designate.Hash, ev.ScriptHash)
		require.Equal(t, native.DesignationEventName, ev.Name)
		require.Equal(t, []stackitem.Item{
			stackitem.Make(int64(r)),
			stackitem.Make(bc.BlockHeight()),
		}, ev.Item.Value().([]stackitem.Item))
	} else {
		require.Equal(t, vm.FaultState, aer[0].VMState)
		require.Equal(t, 0, len(aer[0].Events))
	}
}

func (bc *Blockchain) getNodesByRole(t *testing.T, ok bool, r noderoles.Role, index uint32, resLen int) {
	res, err := invokeContractMethod(bc, 10_000_000, bc.contracts.Designate.Hash, "getDesignatedByRole", int64(r), int64(index))
	require.NoError(t, err)
	if ok {
		require.Equal(t, vm.HaltState, res.VMState)
		require.Equal(t, 1, len(res.Stack))
		arrItem := res.Stack[0]
		require.Equal(t, stackitem.ArrayT, arrItem.Type())
		arr := arrItem.(*stackitem.Array)
		require.Equal(t, resLen, arr.Len())
	} else {
		checkFAULTState(t, res)
	}
}

func TestDesignate_DesignateAsRoleTx(t *testing.T) {
	bc := newTestChain(t)

	priv, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubs := keys.PublicKeys{priv.PublicKey()}

	bc.setNodesByRole(t, false, 0xFF, pubs)
	bc.setNodesByRole(t, true, noderoles.Oracle, pubs)
	index := bc.BlockHeight() + 1
	bc.getNodesByRole(t, false, 0xFF, 0, 0)
	bc.getNodesByRole(t, false, noderoles.Oracle, 100500, 0)
	bc.getNodesByRole(t, true, noderoles.Oracle, 0, 0)     // returns an empty list
	bc.getNodesByRole(t, true, noderoles.Oracle, index, 1) // returns pubs

	priv1, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubs = keys.PublicKeys{priv1.PublicKey()}
	bc.setNodesByRole(t, true, noderoles.StateValidator, pubs)
	bc.getNodesByRole(t, true, noderoles.StateValidator, bc.BlockHeight()+1, 1)

	t.Run("neofs", func(t *testing.T) {
		priv, err := keys.NewPrivateKey()
		require.NoError(t, err)
		pubs = keys.PublicKeys{priv.PublicKey()}
		bc.setNodesByRole(t, true, noderoles.NeoFSAlphabet, pubs)
		bc.getNodesByRole(t, true, noderoles.NeoFSAlphabet, bc.BlockHeight()+1, 1)
	})
}

func TestDesignate_DesignateAsRole(t *testing.T) {
	bc := newTestChain(t)

	des := bc.contracts.Designate
	tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
	bl := block.New(bc.config.StateRootInHeader)
	bl.Index = bc.BlockHeight() + 1
	ic := bc.newInteropContext(trigger.OnPersist, bc.dao, bl, tx)
	ic.SpawnVM()
	ic.VM.LoadScript([]byte{byte(opcode.RET)})

	_, _, err := des.GetDesignatedByRole(bc.dao, 0xFF, 255)
	require.True(t, errors.Is(err, native.ErrInvalidRole), "got: %v", err)

	pubs, index, err := des.GetDesignatedByRole(bc.dao, noderoles.Oracle, 255)
	require.NoError(t, err)
	require.Equal(t, 0, len(pubs))
	require.Equal(t, uint32(0), index)

	err = des.DesignateAsRole(ic, noderoles.Oracle, keys.PublicKeys{})
	require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err)

	err = des.DesignateAsRole(ic, noderoles.Oracle, make(keys.PublicKeys, 32+1))
	require.True(t, errors.Is(err, native.ErrLargeNodeList), "got: %v", err)

	priv, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pub := priv.PublicKey()

	err = des.DesignateAsRole(ic, 0xFF, keys.PublicKeys{pub})
	require.True(t, errors.Is(err, native.ErrInvalidRole), "got: %v", err)

	err = des.DesignateAsRole(ic, noderoles.Oracle, keys.PublicKeys{pub})
	require.True(t, errors.Is(err, native.ErrInvalidWitness), "got: %v", err)

	setSigner(tx, testchain.CommitteeScriptHash())
	err = des.DesignateAsRole(ic, noderoles.Oracle, keys.PublicKeys{pub})
	require.NoError(t, err)

	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.Oracle, bl.Index+1)
	require.NoError(t, err)
	require.Equal(t, keys.PublicKeys{pub}, pubs)
	require.Equal(t, bl.Index+1, index)

	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.StateValidator, 255)
	require.NoError(t, err)
	require.Equal(t, 0, len(pubs))
	require.Equal(t, uint32(0), index)

	// Set StateValidator role.
	_, err = keys.NewPrivateKey()
	require.NoError(t, err)
	pub1 := priv.PublicKey()
	err = des.DesignateAsRole(ic, noderoles.StateValidator, keys.PublicKeys{pub1})
	require.NoError(t, err)

	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.Oracle, 255)
	require.NoError(t, err)
	require.Equal(t, keys.PublicKeys{pub}, pubs)
	require.Equal(t, bl.Index+1, index)

	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.StateValidator, 255)
	require.NoError(t, err)
	require.Equal(t, keys.PublicKeys{pub1}, pubs)
	require.Equal(t, bl.Index+1, index)

	// Set P2PNotary role.
	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.P2PNotary, 255)
	require.NoError(t, err)
	require.Equal(t, 0, len(pubs))
	require.Equal(t, uint32(0), index)

	err = des.DesignateAsRole(ic, noderoles.P2PNotary, keys.PublicKeys{pub1})
	require.NoError(t, err)

	pubs, index, err = des.GetDesignatedByRole(ic.DAO, noderoles.P2PNotary, 255)
	require.NoError(t, err)
	require.Equal(t, keys.PublicKeys{pub1}, pubs)
	require.Equal(t, bl.Index+1, index)
}