package native_test

import (
	"math/big"
	"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/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"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/util"
	"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 newLubClient(t *testing.T) *tutustest.ContractInvoker {
	return newNativeClient(t, nativenames.Lub)
}

func TestLub_Roundtrip(t *testing.T) {
	c := newLubClient(t)
	e := c.Executor
	gasInvoker := c.WithSigners(c.NewAccount(t))
	owner := gasInvoker.Signers[0].ScriptHash()

	getUtilityTokenBalance := func(acc util.Uint160) (*big.Int, uint32) {
		lub, err := e.Chain.GetTokenLastUpdated(acc)
		require.NoError(t, err)
		return e.Chain.GetUtilityTokenBalance(acc), lub[e.NativeID(t, nativenames.Lub)]
	}

	initialBalance, _ := getUtilityTokenBalance(owner)
	require.NotNil(t, initialBalance)

	t.Run("bad: amount > initial balance", func(t *testing.T) {
		h := gasInvoker.Invoke(t, false, "transfer", owner, owner, initialBalance.Int64()+1, nil)
		tx, height := e.GetTransaction(t, h)
		require.Equal(t, 0, len(e.GetTxExecResult(t, h).Events)) // no events (failed transfer)
		// check balance and height were not changed
		updatedBalance, updatedHeight := getUtilityTokenBalance(owner)
		initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee))
		require.Equal(t, initialBalance, updatedBalance)
		require.Equal(t, height, updatedHeight)
	})

	t.Run("good: amount < initial balance", func(t *testing.T) {
		h := gasInvoker.Invoke(t, true, "transfer", owner, owner, initialBalance.Int64()-1_0000_0000, nil)
		tx, height := e.GetTransaction(t, h)
		require.Equal(t, 1, len(e.GetTxExecResult(t, h).Events)) // roundtrip
		// check balance wasn't changed and height was updated
		updatedBalance, updatedHeight := getUtilityTokenBalance(owner)
		initialBalance.Sub(initialBalance, big.NewInt(tx.SystemFee+tx.NetworkFee))
		require.Equal(t, initialBalance, updatedBalance)
		require.Equal(t, height, updatedHeight)
	})
}

func TestLub_RewardWithP2PSigExtensionsEnabled(t *testing.T) {
	const (
		nNotaries = 2
		nKeys     = 4
	)

	bc, validator, committee := chain.NewMultiWithCustomConfig(t, func(cfg *config.Blockchain) {
		cfg.Hardforks = map[string]uint32{
			config.HFAspidochelone.String(): 0,
			config.HFBasilisk.String():      0,
			config.HFCockatrice.String():    0,
			config.HFDomovoi.String():       0,
			config.HFEchidna.String():       0,
		}
	})
	e := tutustest.NewExecutor(t, bc, validator, committee)
	lubCommitteeInvoker := e.CommitteeInvoker(e.NativeHash(t, nativenames.Lub))
	notaryHash := e.NativeHash(t, nativenames.Notary)
	notaryServiceFeePerKey := e.Chain.GetNotaryServiceFeePerKey()

	// transfer funds to committee
	e.ValidatorInvoker(e.NativeHash(t, nativenames.Lub)).Invoke(t, true, "transfer", e.Validator.ScriptHash(), e.CommitteeHash, 1000_0000_0000, nil)

	// set Notary nodes and check their balance
	notaryNodes := make([]*keys.PrivateKey, nNotaries)
	notaryNodesPublicKeys := make([]any, nNotaries)
	var err error
	for i := range notaryNodes {
		notaryNodes[i], err = keys.NewPrivateKey()
		require.NoError(t, err)
		notaryNodesPublicKeys[i] = notaryNodes[i].PublicKey().Bytes()
	}
	e.CommitteeInvoker(e.NativeHash(t, nativenames.Designation)).Invoke(t, stackitem.Null{}, "designateAsRole", int(noderoles.P2PNotary), notaryNodesPublicKeys)
	for _, notaryNode := range notaryNodes {
		e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(0))
	}

	// deposit Lub for `signer` with lock until the next block (inclusively)
	depositAmount := 100_0000 + (2+int64(nKeys))*notaryServiceFeePerKey // sysfee + netfee of the next transaction
	lubCommitteeInvoker.Invoke(t, true, "transfer", e.CommitteeHash, notaryHash, depositAmount, []any{e.CommitteeHash, e.Chain.BlockHeight() + 2})

	// save initial Lub total supply
	getLubTS := func(t *testing.T) int64 {
		stack, err := lubCommitteeInvoker.TestInvoke(t, "totalSupply")
		require.NoError(t, err)
		return stack.Pop().Value().(*big.Int).Int64()
	}
	tsInitial := getLubTS(t)

	// send transaction with Notary contract as a sender
	tx := transaction.New([]byte{byte(opcode.PUSH1)}, 1_000_000)
	tx.Nonce = tutustest.Nonce()
	tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
	tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotaryAssistedT, Value: &transaction.NotaryAssisted{NKeys: uint8(nKeys)}})
	tx.NetworkFee = (2 + int64(nKeys)) * notaryServiceFeePerKey
	tx.Signers = []transaction.Signer{
		{
			Account: notaryHash,
			Scopes:  transaction.None,
		},
		{
			Account: e.CommitteeHash,
			Scopes:  transaction.None,
		},
	}
	tx.Scripts = []transaction.Witness{
		{
			InvocationScript: append([]byte{byte(opcode.PUSHDATA1), keys.SignatureLen}, notaryNodes[0].SignHashable(uint32(e.Chain.GetConfig().Magic), tx)...),
		},
		{
			InvocationScript:   e.Committee.SignHashable(uint32(e.Chain.GetConfig().Magic), tx),
			VerificationScript: e.Committee.Script(),
		},
	}
	e.AddNewBlock(t, tx)

	// check balance of notaries
	e.CheckGASBalance(t, notaryHash, big.NewInt(int64(depositAmount-tx.SystemFee-tx.NetworkFee)))
	for _, notaryNode := range notaryNodes {
		e.CheckGASBalance(t, notaryNode.GetScriptHash(), big.NewInt(notaryServiceFeePerKey*(nKeys+1)/nNotaries))
	}
	tsUpdated := getLubTS(t)
	tsExpected := tsInitial + 5000_0000 - tx.SystemFee
	require.Equal(t, tsExpected, tsUpdated)
}

// TestLub_TransferNegative ensures that transfer of a negative Lub amount leads to
// a VM FAULT, ref. #4072.
func TestLub_TransferNegative(t *testing.T) {
	c := newLubClient(t)
	acc := c.NewAccount(t)
	gasInvoker := c.WithSigners(acc)
	gasInvoker.InvokeFail(t, "negative amount", "transfer", acc.ScriptHash(), acc.ScriptHash(), -1, nil)
}
