package frostfs

import (
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/balance"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/frostfsid"
	nmClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
	frostfsEvent "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event/frostfs"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"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 TestHandleDeposit(t *testing.T) {
	t.Parallel()
	es := &testEpochState{
		epochCounter: 100,
	}
	b := &testBalaceClient{}
	m := &testMorphClient{
		balance: 150,
	}
	proc, err := newTestProc(t, func(p *Params) {
		p.EpochState = es
		p.BalanceClient = b
		p.MorphClient = m
	})
	require.NoError(t, err, "failed to create processor")

	ev := frostfsEvent.Deposit{
		IDValue:     []byte{1, 2, 3, 4, 5},
		FromValue:   util.Uint160{100},
		ToValue:     util.Uint160{200},
		AmountValue: 1000,
	}

	proc.handleDeposit(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var expMint balance.MintPrm
	expMint.SetAmount(ev.AmountValue)
	expMint.SetID(ev.IDValue)
	expMint.SetTo(ev.ToValue)

	require.EqualValues(t, []balance.MintPrm{expMint}, b.mint, "invalid mint value")
	require.EqualValues(t, []transferGas{
		{
			receiver: ev.ToValue,
			amount:   fixedn.Fixed8(50),
		},
	}, m.transferGas, "invalid transfer gas")

	es.epochCounter = 109

	proc.handleDeposit(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	expMint.SetAmount(ev.AmountValue)
	expMint.SetID(ev.IDValue)
	expMint.SetTo(ev.ToValue)

	require.EqualValues(t, []balance.MintPrm{expMint, expMint}, b.mint, "invalid mint value")
	require.EqualValues(t, []transferGas{
		{
			receiver: ev.ToValue,
			amount:   fixedn.Fixed8(50),
		},
	}, m.transferGas, "invalid transfer gas")
}

func TestHandleWithdraw(t *testing.T) {
	t.Parallel()
	es := &testEpochState{
		epochCounter: 100,
	}
	b := &testBalaceClient{}
	m := &testMorphClient{
		balance: 150,
	}
	proc, err := newTestProc(t, func(p *Params) {
		p.EpochState = es
		p.BalanceClient = b
		p.MorphClient = m
	})
	require.NoError(t, err, "failed to create processor")

	ev := frostfsEvent.Withdraw{
		IDValue:     []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
		UserValue:   util.Uint160{100},
		AmountValue: 1000,
	}

	proc.handleWithdraw(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	lock, err := util.Uint160DecodeBytesBE(ev.ID()[:util.Uint160Size])
	require.NoError(t, err, "failed to decode ID")
	var expLock balance.LockPrm
	expLock.SetAmount(ev.AmountValue)
	expLock.SetID(ev.IDValue)
	expLock.SetDueEpoch(int64(es.epochCounter) + int64(lockAccountLifetime))
	expLock.SetLock(lock)
	expLock.SetUser(ev.UserValue)

	require.EqualValues(t, []balance.LockPrm{expLock}, b.lock, "invalid lock value")
}

func TestHandleCheque(t *testing.T) {
	t.Parallel()
	es := &testEpochState{
		epochCounter: 100,
	}
	b := &testBalaceClient{}
	m := &testMorphClient{
		balance: 150,
	}
	proc, err := newTestProc(t, func(p *Params) {
		p.BalanceClient = b
		p.MorphClient = m
		p.EpochState = es
	})
	require.NoError(t, err, "failed to create processor")

	ev := frostfsEvent.Cheque{
		IDValue:     []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
		UserValue:   util.Uint160{100},
		AmountValue: 1000,
		LockValue:   util.Uint160{200},
	}

	proc.handleCheque(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var expBurn balance.BurnPrm
	expBurn.SetAmount(ev.AmountValue)
	expBurn.SetID(ev.IDValue)
	expBurn.SetTo(util.Uint160{200})

	require.EqualValues(t, []balance.BurnPrm{expBurn}, b.burn, "invalid burn value")
}

func TestHandleConfig(t *testing.T) {
	t.Parallel()
	es := &testEpochState{
		epochCounter: 100,
	}
	nm := &testNetmapClient{}
	m := &testMorphClient{
		balance: 150,
	}
	proc, err := newTestProc(t, func(p *Params) {
		p.NetmapClient = nm
		p.MorphClient = m
		p.EpochState = es
	})
	require.NoError(t, err, "failed to create processor")

	ev := frostfsEvent.Config{
		IDValue:     []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20},
		KeyValue:    []byte{1, 2, 3, 4, 5},
		ValueValue:  []byte{6, 7, 8, 9, 0},
		TxHashValue: util.Uint256{100},
	}

	proc.handleConfig(ev)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var expConfig nmClient.SetConfigPrm
	expConfig.SetHash(ev.TxHashValue)
	expConfig.SetID(ev.IDValue)
	expConfig.SetKey(ev.KeyValue)
	expConfig.SetValue(ev.ValueValue)

	require.EqualValues(t, []nmClient.SetConfigPrm{expConfig}, nm.config, "invalid config value")
}

func TestHandleUnbind(t *testing.T) {
	t.Parallel()
	es := &testEpochState{
		epochCounter: 100,
	}
	m := &testMorphClient{
		balance: 150,
	}
	id := &testIDClient{}
	proc, err := newTestProc(t, func(p *Params) {
		p.EpochState = es
		p.MorphClient = m
		p.FrostFSIDClient = id
	})
	require.NoError(t, err, "failed to create processor")

	p, err := keys.NewPrivateKey()
	require.NoError(t, err)

	evUnbind := frostfsEvent.Unbind{
		BindCommon: frostfsEvent.BindCommon{
			UserValue: util.Uint160{49}.BytesBE(),
			KeysValue: [][]byte{
				p.PublicKey().Bytes(),
			},
			TxHashValue: util.Uint256{100},
		},
	}

	proc.handleUnbind(evUnbind)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	var userID user.ID
	userID.SetScriptHash(util.Uint160{49})

	var expBind frostfsid.CommonBindPrm
	expBind.SetOwnerID(userID.WalletBytes())
	expBind.SetKeys(evUnbind.BindCommon.KeysValue)
	expBind.SetHash(evUnbind.BindCommon.TxHashValue)

	var expNilSlice []frostfsid.CommonBindPrm

	require.EqualValues(t, []frostfsid.CommonBindPrm{expBind}, id.remove, "invalid remove keys value")
	require.EqualValues(t, expNilSlice, id.add, "invalid add keys value")

	evBind := frostfsEvent.Bind{
		BindCommon: frostfsEvent.BindCommon{
			UserValue: util.Uint160{49}.BytesBE(),
			KeysValue: [][]byte{
				p.PublicKey().Bytes(),
			},
			TxHashValue: util.Uint256{100},
		},
	}

	proc.handleBind(evBind)

	for proc.pool.Running() > 0 {
		time.Sleep(10 * time.Millisecond)
	}

	require.EqualValues(t, []frostfsid.CommonBindPrm{expBind}, id.remove, "invalid remove keys value")
	require.EqualValues(t, []frostfsid.CommonBindPrm{expBind}, id.add, "invalid add keys value")
}

func newTestProc(t *testing.T, nonDefault func(p *Params)) (*Processor, error) {
	p := &Params{
		Log:                 test.NewLogger(t, true),
		PoolSize:            1,
		FrostFSContract:     util.Uint160{0},
		FrostFSIDClient:     &testIDClient{},
		BalanceClient:       &testBalaceClient{},
		NetmapClient:        &testNetmapClient{},
		MorphClient:         &testMorphClient{},
		EpochState:          &testEpochState{},
		AlphabetState:       &testAlphabetState{isAlphabet: true},
		Converter:           &testPrecisionConverter{},
		MintEmitCacheSize:   100,
		MintEmitThreshold:   10,
		MintEmitValue:       fixedn.Fixed8(50),
		GasBalanceThreshold: 50,
	}

	nonDefault(p)

	return New(p)
}

type testEpochState struct {
	epochCounter uint64
}

func (s *testEpochState) EpochCounter() uint64 {
	return s.epochCounter
}

type testAlphabetState struct {
	isAlphabet bool
}

func (s *testAlphabetState) IsAlphabet() bool {
	return s.isAlphabet
}

type testPrecisionConverter struct {
}

func (c *testPrecisionConverter) ToBalancePrecision(v int64) int64 {
	return v
}

type testBalaceClient struct {
	mint []balance.MintPrm
	lock []balance.LockPrm
	burn []balance.BurnPrm
}

func (c *testBalaceClient) Mint(p balance.MintPrm) error {
	c.mint = append(c.mint, p)
	return nil
}
func (c *testBalaceClient) Lock(p balance.LockPrm) error {
	c.lock = append(c.lock, p)
	return nil
}
func (c *testBalaceClient) Burn(p balance.BurnPrm) error {
	c.burn = append(c.burn, p)
	return nil
}

type testNetmapClient struct {
	config []nmClient.SetConfigPrm
}

func (c *testNetmapClient) SetConfig(p nmClient.SetConfigPrm) error {
	c.config = append(c.config, p)
	return nil
}

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

type testMorphClient struct {
	balance     int64
	transferGas []transferGas
}

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

type testIDClient struct {
	add    []frostfsid.CommonBindPrm
	remove []frostfsid.CommonBindPrm
}

func (c *testIDClient) AddKeys(p frostfsid.CommonBindPrm) error {
	c.add = append(c.add, p)
	return nil
}

func (c *testIDClient) RemoveKeys(args frostfsid.CommonBindPrm) error {
	c.remove = append(c.remove, args)
	return nil
}