package native_test

import (
	"math/big"
	"testing"

	"github.com/stretchr/testify/require"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"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/smartcontract/callflag"
	"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/emit"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/opcode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

func newNativeClient(t *testing.T, name string) *tutustest.ContractInvoker {
	return newCustomNativeClient(t, name, nil)
}

func newCustomNativeClient(t *testing.T, name string, f func(cfg *config.Blockchain)) *tutustest.ContractInvoker {
	bc, acc := chain.NewSingleWithCustomConfig(t, f)
	e := tutustest.NewExecutor(t, bc, acc, acc)

	return e.CommitteeInvoker(e.NativeHash(t, name))
}

func testGetSet(t *testing.T, c *tutustest.ContractInvoker, name string, defaultValue, minValue, maxValue int64) {
	getName := "get" + name
	setName := "set" + name

	randomInvoker := c.WithSigners(c.NewAccount(t))
	committeeInvoker := c.WithSigners(c.Committee)

	t.Run("set, not signed by committee", func(t *testing.T) {
		randomInvoker.InvokeFail(t, "invalid committee signature", setName, minValue+1)
	})
	t.Run("get, default value", func(t *testing.T) {
		randomInvoker.Invoke(t, defaultValue, getName)
	})
	t.Run("set, too small value", func(t *testing.T) {
		committeeInvoker.InvokeFail(t, "", setName, minValue-1)
	})

	if maxValue != 0 {
		t.Run("set, too large value", func(t *testing.T) {
			// use big.Int because max can be `math.MaxInt64`
			m := big.NewInt(maxValue)
			m.Add(m, big.NewInt(1))
			committeeInvoker.InvokeFail(t, "", setName, m)
		})
	}

	t.Run("set, success", func(t *testing.T) {
		v := defaultValue + 1
		if v > maxValue || // sanitize default value that is equal to max value.
			name == "MaxTraceableBlocks" { // sanitize MaxTraceableBlocks that can only be decreased.
			v = defaultValue - 1
		}
		// Set and get in the same block.
		txSet := committeeInvoker.PrepareInvoke(t, setName, v)
		txGet := randomInvoker.PrepareInvoke(t, getName)
		c.AddNewBlock(t, txSet, txGet)
		c.CheckHalt(t, txSet.Hash(), stackitem.Null{})

		switch name {
		case "LubPerBlock":
			// LubPerBlock is set on the next block
			c.CheckHalt(t, txGet.Hash(), stackitem.Make(defaultValue))
			c.AddNewBlock(t)
			randomInvoker.Invoke(t, v, getName)
		case "ExecFeeFactor":
			// ExecFeeFactor was risen, so the second transaction will fail because
			// of gas limit exceeding (its fees are out-of-date).
			c.CheckFault(t, txGet.Hash(), "gas limit exceeded")
			// Set in a separate block.
			committeeInvoker.Invoke(t, stackitem.Null{}, setName, v)
			// Get in the next block.
			randomInvoker.Invoke(t, v, getName)
		default:
			c.CheckHalt(t, txGet.Hash(), stackitem.Make(v))
			// Get in the next block.
			randomInvoker.Invoke(t, v, getName)
		}
	})
}

func testGetSetCache(t *testing.T, c *tutustest.ContractInvoker, name string, defaultValue int64) {
	getName := "get" + name
	setName := "set" + name

	committeeInvoker := c.WithSigners(c.Committee)

	newVal := defaultValue - 1

	// Change fee, abort the transaction and check that contract cache wasn't persisted
	// for FAULTed tx at the same block.
	w := io.NewBufBinWriter()
	emit.AppCall(w.BinWriter, committeeInvoker.Hash, setName, callflag.All, newVal)
	emit.Opcodes(w.BinWriter, opcode.ABORT)
	tx1 := committeeInvoker.PrepareInvocation(t, w.Bytes(), committeeInvoker.Signers)
	tx2 := committeeInvoker.PrepareInvoke(t, getName)
	committeeInvoker.AddNewBlock(t, tx1, tx2)
	committeeInvoker.CheckFault(t, tx1.Hash(), "ABORT")
	committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue))

	// Change fee and check that change is available for the next tx.
	tx1 = committeeInvoker.PrepareInvoke(t, setName, newVal)
	tx2 = committeeInvoker.PrepareInvoke(t, getName)
	committeeInvoker.AddNewBlock(t, tx1, tx2)
	committeeInvoker.CheckHalt(t, tx1.Hash())
	if name != "LubPerBlock" {
		committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(newVal))
	} else {
		committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(defaultValue))
		committeeInvoker.Invoke(t, newVal, getName)
	}
}

func setNodesByRole(t *testing.T, designateInvoker *tutustest.ContractInvoker, ok bool, r noderoles.Role, nodes keys.PublicKeys, oldKeys ...keys.PublicKeys) {
	pubs := make([]any, len(nodes))
	for i := range nodes {
		pubs[i] = nodes[i].Bytes()
	}
	if ok {
		h := designateInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int64(r), pubs)
		ntf := stackitem.NewArray([]stackitem.Item{
			stackitem.Make(int64(r)),
			stackitem.Make(designateInvoker.Chain.BlockHeight()),
		})
		if len(oldKeys) > 0 {
			old := stackitem.NewArray([]stackitem.Item{})
			for _, key := range oldKeys[0] {
				old.Append(stackitem.NewByteArray(key.Bytes()))
			}
			newKeys := stackitem.NewArray([]stackitem.Item{})
			for _, key := range nodes {
				newKeys.Append(stackitem.NewByteArray(key.Bytes()))
			}
			ntf.Append(old)
			ntf.Append(newKeys)
		}
		designateInvoker.CheckTxNotificationEvent(t, h, 0, state.NotificationEvent{
			ScriptHash: designateInvoker.Hash,
			Name:       native.DesignationEventName,
			Item:       ntf,
		})
	} else {
		designateInvoker.InvokeFail(t, "", "designateAsRole", int64(r), pubs)
	}
}

func checkNodeRoles(t *testing.T, designateInvoker *tutustest.ContractInvoker, ok bool, r noderoles.Role, index uint32, res keys.PublicKeys) {
	if ok {
		designateInvoker.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
			require.Equal(t, 1, len(stack))
			arr := stack[0].Value().([]stackitem.Item)
			require.Equal(t, len(res), len(arr))
			for i := range arr {
				require.Equal(t, res[i].Bytes(), arr[i].Value().([]byte), i)
			}
		}, "getDesignatedByRole", int64(r), int64(index))
	} else {
		designateInvoker.InvokeFail(t, "", "getDesignatedByRole", int64(r), int64(index))
	}
}

// newGovernmentHelper creates a GovernmentHelper for testing government contracts.
// This provides convenient methods for registering citizens, managing VTS, and
// testing cross-contract government operations.
func newGovernmentHelper(t *testing.T) *tutustest.GovernmentHelper {
	bc, acc := chain.NewSingleWithCustomConfig(t, nil)
	e := tutustest.NewExecutor(t, bc, acc, acc)
	return tutustest.NewGovernmentHelper(t, e)
}

// newGovernmentHelperWithConfig creates a GovernmentHelper with custom blockchain config.
func newGovernmentHelperWithConfig(t *testing.T, f func(cfg *config.Blockchain)) *tutustest.GovernmentHelper {
	bc, acc := chain.NewSingleWithCustomConfig(t, f)
	e := tutustest.NewExecutor(t, bc, acc, acc)
	return tutustest.NewGovernmentHelper(t, e)
}

// newRoleHelper creates a RoleHelper for testing role-based operations.
func newRoleHelper(t *testing.T) *tutustest.RoleHelper {
	bc, acc := chain.NewSingleWithCustomConfig(t, nil)
	e := tutustest.NewExecutor(t, bc, acc, acc)
	return tutustest.NewRoleHelper(t, e)
}

// newCrossContractHelper creates a CrossContractHelper for testing cross-contract calls.
func newCrossContractHelper(t *testing.T) *tutustest.CrossContractHelper {
	bc, acc := chain.NewSingleWithCustomConfig(t, nil)
	e := tutustest.NewExecutor(t, bc, acc, acc)
	return tutustest.NewCrossContractHelper(t, e)
}

// newEventMatcher creates an EventMatcher from transaction events.
func newEventMatcher(t *testing.T, c *tutustest.ContractInvoker, txHash util.Uint256) *tutustest.EventMatcher {
	aer, err := c.Chain.GetAppExecResults(txHash, 0)
	require.NoError(t, err)
	require.Equal(t, 1, len(aer))
	return tutustest.NewEventMatcher(t, aer[0].Events)
}
