package native_test

import (
	"slices"
	"testing"

	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/noderoles"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutustest/chain"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"github.com/stretchr/testify/require"
)

func newDesignateClient(t *testing.T) *tutustest.ContractInvoker {
	return newNativeClient(t, nativenames.Designation)
}

func TestDesignate_DesignateAsRole(t *testing.T) {
	c := newDesignateClient(t)
	e := c.Executor
	designateInvoker := c.WithSigners(c.Committee)

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

	setNodesByRole(t, designateInvoker, false, 0xFF, pubs)
	setNodesByRole(t, designateInvoker, true, noderoles.Oracle, pubs, nil)
	index := e.Chain.BlockHeight() + 1
	checkNodeRoles(t, designateInvoker, false, 0xFF, 0, nil)
	checkNodeRoles(t, designateInvoker, false, noderoles.Oracle, 100500, nil)
	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, 0, keys.PublicKeys{}) // returns an empty list
	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, index, pubs)          // returns pubs

	priv1, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubs = keys.PublicKeys{priv1.PublicKey()}
	setNodesByRole(t, designateInvoker, true, noderoles.StateValidator, pubs, nil)
	checkNodeRoles(t, designateInvoker, true, noderoles.StateValidator, e.Chain.BlockHeight()+1, pubs)

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

func TestDesignate_DesignateAsRole_Echidna(t *testing.T) {
	c := newCustomNativeClient(t, nativenames.Designation, func(cfg *config.Blockchain) {
		cfg.Hardforks = map[string]uint32{
			config.HFEchidna.String(): 3,
		}
	})
	designateInvoker := c.WithSigners(c.Committee)

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

	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, 0, keys.PublicKeys{})                            // BlockHeight is 1.
	setNodesByRole(t, designateInvoker, true, noderoles.Oracle, oldPubs)                                         // BlockHeight is 2.
	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, designateInvoker.Chain.BlockHeight()+1, oldPubs) // BlockHeight is 3.

	priv, err = keys.NewPrivateKey()
	require.NoError(t, err)
	pubs := keys.PublicKeys{priv.PublicKey()}
	setNodesByRole(t, designateInvoker, true, noderoles.Oracle, pubs, oldPubs)                                // BlockHeight is 4.
	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, designateInvoker.Chain.BlockHeight()+1, pubs) // BlockHeight is 5.
}

type dummyOracle struct {
	updateNodes func(k keys.PublicKeys)
}

// AddRequests processes new requests.
func (o *dummyOracle) AddRequests(map[uint64]*state.OracleRequest) {
}

// RemoveRequests removes already processed requests.
func (o *dummyOracle) RemoveRequests([]uint64) {
	panic("TODO")
}

// UpdateOracleNodes updates oracle nodes.
func (o *dummyOracle) UpdateOracleNodes(k keys.PublicKeys) {
	if o.updateNodes != nil {
		o.updateNodes(k)
		return
	}
	panic("TODO")
}

// UpdateNativeContract updates oracle contract native script and hash.
func (o *dummyOracle) UpdateNativeContract([]byte, []byte, util.Uint160, int) {
}

// Start runs oracle module.
func (o *dummyOracle) Start() {
	panic("TODO")
}

// Shutdown shutdowns oracle module.
func (o *dummyOracle) Shutdown() {
	panic("TODO")
}

func TestDesignate_Cache(t *testing.T) {
	c := newDesignateClient(t)
	e := c.Executor
	designateInvoker := c.WithSigners(c.Committee)
	r := int64(noderoles.Oracle)
	var (
		updatedNodes keys.PublicKeys
		updateCalled bool
	)
	oracleServ := &dummyOracle{
		updateNodes: func(k keys.PublicKeys) {
			updatedNodes = k
			updateCalled = true
		},
	}
	privGood, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubsGood := []any{privGood.PublicKey().Bytes()}

	privBad, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubsBad := []any{privBad.PublicKey().Bytes()}

	// Firstly, designate good Oracle node and check that OracleService callback was called during PostPersist.
	e.Chain.SetOracle(oracleServ)
	txDesignateGood := designateInvoker.PrepareInvoke(t, "designateAsRole", r, pubsGood)
	e.AddNewBlock(t, txDesignateGood)
	e.CheckHalt(t, txDesignateGood.Hash(), stackitem.Null{})
	require.True(t, updateCalled)
	require.Equal(t, keys.PublicKeys{privGood.PublicKey()}, updatedNodes)
	updatedNodes = nil
	updateCalled = false

	// Check designated node in a separate block.
	checkNodeRoles(t, designateInvoker, true, noderoles.Oracle, e.Chain.BlockHeight()+1, keys.PublicKeys{privGood.PublicKey()})

	// Designate privBad as oracle node and abort the transaction. Designation cache changes
	// shouldn't be persisted to the contract and no notification should be sent.
	w := io.NewBufBinWriter()
	emit.AppCall(w.BinWriter, designateInvoker.Hash, "designateAsRole", callflag.All, int64(r), pubsBad)
	emit.Opcodes(w.BinWriter, opcode.ABORT)
	require.NoError(t, w.Err)
	script := w.Bytes()

	designateInvoker.InvokeScriptCheckFAULT(t, script, designateInvoker.Signers, "ABORT")
	require.Nil(t, updatedNodes)
	require.False(t, updateCalled)
}

func TestDesignate_GenesisRolesExtension(t *testing.T) {
	pk1, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pk2, err := keys.NewPrivateKey()
	require.NoError(t, err)
	pubs := keys.PublicKeys{pk1.PublicKey(), pk2.PublicKey()}

	bc, acc := chain.NewSingleWithCustomConfig(t, func(blockchain *config.Blockchain) {
		blockchain.Genesis.Roles = map[noderoles.Role]keys.PublicKeys{
			noderoles.StateValidator: pubs,
		}
	})
	e := tutustest.NewExecutor(t, bc, acc, acc)
	c := e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation))

	// Check designated node in a separate block.
	slices.SortFunc(pubs, (*keys.PublicKey).Cmp)
	checkNodeRoles(t, c, true, noderoles.StateValidator, e.Chain.BlockHeight()+1, pubs)
}
