package alphabet_test

import (
	"testing"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring/processors/alphabet"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring/timers"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/require"
)

func TestProcessorEmitsGasToNetmapAndAlphabet(t *testing.T) {
	t.Parallel()
	var emission uint64 = 100_000
	var index int = 5
	var parsedWallets []util.Uint160 = []util.Uint160{{20}, {25}}

	alphabetContracts := innerring.NewAlphabetContracts()
	for i := 0; i <= index; i++ {
		alphabetContracts[innerring.GlagoliticLetter(i)] = util.Uint160{uint8(i)}
	}

	morphClient := &testMorphClient{}

	var node1 netmap.NodeInfo
	key1, err := keys.NewPublicKeyFromString("038c862959e56b43e20f79187c4fe9e0bc7c8c66c1603e6cf0ec7f87ab6b08dc35")
	require.NoError(t, err, "failed to parse key1")
	node1.SetPublicKey(key1.Bytes())

	var node2 netmap.NodeInfo
	key2, err := keys.NewPublicKeyFromString("02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3")
	require.NoError(t, err, "failed to parse key2")
	node2.SetPublicKey(key2.Bytes())

	nodes := []netmap.NodeInfo{node1, node2}

	network := &netmap.NetMap{}
	network.SetNodes(nodes)

	netmapClient := &testNetmapClient{
		netmap: network,
	}

	params := &alphabet.Params{
		ParsedWallets:     parsedWallets,
		Log:               test.NewLogger(t),
		PoolSize:          2,
		StorageEmission:   emission,
		IRList:            &testIndexer{index: index},
		AlphabetContracts: alphabetContracts,
		MorphClient:       morphClient,
		NetmapClient:      netmapClient,
	}

	processor, err := alphabet.New(params)
	require.NoError(t, err, "failed to create processor instance")

	processor.HandleGasEmission(timers.NewAlphabetEmitTick{})

	processor.WaitPoolRunning()

	require.EqualValues(t, []invokedMethod{
		{
			contract: alphabetContracts[innerring.GlagoliticLetter(index)],
			fee:      0,
			method:   "emit",
		},
	}, morphClient.invokedMethods, "invalid invoked morph methods")

	require.EqualValues(t, []transferGas{
		{
			receiver: key1.GetScriptHash(),
			amount:   fixedn.Fixed8(25_000),
		},
		{
			receiver: key2.GetScriptHash(),
			amount:   fixedn.Fixed8(25_000),
		},
	}, morphClient.transferedGas, "invalid transfered Gas")

	require.EqualValues(t, []batchTransferGas{
		{
			receivers: parsedWallets,
			amount:    fixedn.Fixed8(25_000),
		},
	}, morphClient.batchTransferedGas, "invalid batch transfered Gas")
}

func TestProcessorEmitsGasToNetmapIfNoParsedWallets(t *testing.T) {
	t.Parallel()
	var emission uint64 = 100_000
	var index int = 5
	var parsedWallets []util.Uint160 = []util.Uint160{}

	alphabetContracts := innerring.NewAlphabetContracts()
	for i := 0; i <= index; i++ {
		alphabetContracts[innerring.GlagoliticLetter(i)] = util.Uint160{uint8(i)}
	}

	morphClient := &testMorphClient{}

	var node1 netmap.NodeInfo
	key1, err := keys.NewPublicKeyFromString("038c862959e56b43e20f79187c4fe9e0bc7c8c66c1603e6cf0ec7f87ab6b08dc35")
	require.NoError(t, err, "failed to parse key1")
	node1.SetPublicKey(key1.Bytes())

	var node2 netmap.NodeInfo
	key2, err := keys.NewPublicKeyFromString("02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3")
	require.NoError(t, err, "failed to parse key2")
	node2.SetPublicKey(key2.Bytes())

	nodes := []netmap.NodeInfo{node1, node2}

	network := &netmap.NetMap{}
	network.SetNodes(nodes)

	netmapClient := &testNetmapClient{
		netmap: network,
	}

	params := &alphabet.Params{
		ParsedWallets:     parsedWallets,
		Log:               test.NewLogger(t),
		PoolSize:          2,
		StorageEmission:   emission,
		IRList:            &testIndexer{index: index},
		AlphabetContracts: alphabetContracts,
		MorphClient:       morphClient,
		NetmapClient:      netmapClient,
	}

	processor, err := alphabet.New(params)
	require.NoError(t, err, "failed to create processor instance")

	processor.HandleGasEmission(timers.NewAlphabetEmitTick{})

	processor.WaitPoolRunning()

	require.EqualValues(t, []invokedMethod{
		{
			contract: alphabetContracts[innerring.GlagoliticLetter(index)],
			fee:      0,
			method:   "emit",
		},
	}, morphClient.invokedMethods, "invalid invoked morph methods")

	require.EqualValues(t, []transferGas{
		{
			receiver: key1.GetScriptHash(),
			amount:   fixedn.Fixed8(50_000),
		},
		{
			receiver: key2.GetScriptHash(),
			amount:   fixedn.Fixed8(50_000),
		},
	}, morphClient.transferedGas, "invalid transfered Gas")

	require.Equal(t, 0, len(morphClient.batchTransferedGas), "invalid batch transfered Gas")
}

func TestProcessorDoesntEmitGasIfNoNetmapOrParsedWallets(t *testing.T) {
	t.Parallel()
	var emission uint64 = 100_000
	var index int = 5
	var parsedWallets []util.Uint160 = []util.Uint160{}

	alphabetContracts := innerring.NewAlphabetContracts()
	for i := 0; i <= index; i++ {
		alphabetContracts[innerring.GlagoliticLetter(i)] = util.Uint160{uint8(i)}
	}

	morphClient := &testMorphClient{}

	nodes := []netmap.NodeInfo{}
	network := &netmap.NetMap{}
	network.SetNodes(nodes)

	netmapClient := &testNetmapClient{
		netmap: network,
	}

	params := &alphabet.Params{
		ParsedWallets:     parsedWallets,
		Log:               test.NewLogger(t),
		PoolSize:          2,
		StorageEmission:   emission,
		IRList:            &testIndexer{index: index},
		AlphabetContracts: alphabetContracts,
		MorphClient:       morphClient,
		NetmapClient:      netmapClient,
	}

	processor, err := alphabet.New(params)
	require.NoError(t, err, "failed to create processor instance")

	processor.HandleGasEmission(timers.NewAlphabetEmitTick{})

	processor.WaitPoolRunning()

	require.EqualValues(t, []invokedMethod{
		{
			contract: alphabetContracts[innerring.GlagoliticLetter(index)],
			fee:      0,
			method:   "emit",
		},
	}, morphClient.invokedMethods, "invalid invoked morph methods")

	require.Equal(t, 0, len(morphClient.transferedGas), "invalid transfered Gas")

	require.Equal(t, 0, len(morphClient.batchTransferedGas), "invalid batch transfered Gas")
}

type testIndexer struct {
	index int
}

func (i *testIndexer) AlphabetIndex() int {
	return i.index
}

type invokedMethod struct {
	contract util.Uint160
	fee      fixedn.Fixed8
	method   string
	args     []any
}

type transferGas struct {
	receiver util.Uint160
	amount   fixedn.Fixed8
}

type batchTransferGas struct {
	receivers []util.Uint160
	amount    fixedn.Fixed8
}

type testMorphClient struct {
	invokedMethods     []invokedMethod
	transferedGas      []transferGas
	batchTransferedGas []batchTransferGas
}

func (c *testMorphClient) Invoke(contract util.Uint160, fee fixedn.Fixed8, method string, args ...any) (uint32, error) {
	c.invokedMethods = append(c.invokedMethods,
		invokedMethod{
			contract: contract,
			fee:      fee,
			method:   method,
			args:     args,
		})
	return 0, nil
}

func (c *testMorphClient) TransferGas(receiver util.Uint160, amount fixedn.Fixed8) error {
	c.transferedGas = append(c.transferedGas, transferGas{
		receiver: receiver,
		amount:   amount,
	})
	return nil
}

func (c *testMorphClient) BatchTransferGas(receivers []util.Uint160, amount fixedn.Fixed8) error {
	c.batchTransferedGas = append(c.batchTransferedGas, batchTransferGas{
		receivers: receivers,
		amount:    amount,
	})
	return nil
}

type testNetmapClient struct {
	netmap *netmap.NetMap
}

func (c *testNetmapClient) NetMap() (*netmap.NetMap, error) {
	return c.netmap, nil
}