package governance

import (
	"encoding/binary"
	"sort"
	"testing"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
	frostfscontract "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/frostfs"
	nmClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event/rolemanagement"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
	"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/stretchr/testify/require"
)

func TestHandleAlphabetSyncEvent(t *testing.T) {
	t.Parallel()
	testKeys := generateTestKeys(t)

	es := &testEpochState{
		epoch: 100,
	}
	as := &testAlphabetState{
		isAlphabet: true,
	}
	v := &testVoter{}
	irf := &testIRFetcher{
		publicKeys: testKeys.sidechainKeys,
	}
	m := &testMorphClient{
		commiteeKeys: testKeys.sidechainKeys,
	}
	mn := &testMainnetClient{
		alphabetKeys: testKeys.mainnetKeys,
	}
	f := &testFrostFSClient{}
	nm := &testNetmapClient{}

	proc, err := New(
		&Params{
			Log:           test.NewLogger(t, true),
			EpochState:    es,
			AlphabetState: as,
			Voter:         v,
			IRFetcher:     irf,
			MorphClient:   m,
			MainnetClient: mn,
			FrostFSClient: f,
			NetmapClient:  nm,
		},
	)

	require.NoError(t, err, "failed to create processor")

	ev := Sync{
		txHash: util.Uint256{100},
	}

	proc.HandleAlphabetSync(ev)

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

	require.EqualValues(t, []VoteValidatorPrm{
		{
			Validators: testKeys.newAlphabetExp,
			Hash:       &ev.txHash,
		},
	}, v.votes, "invalid vote calls")

	var irUpdateExp []nmClient.UpdateIRPrm

	require.EqualValues(t, irUpdateExp, nm.updates, "invalid IR updates")

	var expAlphabetUpdate client.UpdateAlphabetListPrm
	expAlphabetUpdate.SetHash(ev.txHash)
	expAlphabetUpdate.SetList(testKeys.newInnerRingExp)
	require.EqualValues(t, []client.UpdateAlphabetListPrm{expAlphabetUpdate}, m.alphabetUpdates, "invalid alphabet updates")

	var expNotaryUpdate client.UpdateNotaryListPrm
	expNotaryUpdate.SetHash(ev.txHash)
	expNotaryUpdate.SetList(testKeys.newAlphabetExp)
	require.EqualValues(t, []client.UpdateNotaryListPrm{expNotaryUpdate}, m.notaryUpdates, "invalid notary list updates")

	buf := make([]byte, 8)
	binary.LittleEndian.PutUint64(buf, es.epoch)

	id := append([]byte(alphabetUpdateIDPrefix), buf...)
	var expFrostFSAlphabetUpd frostfscontract.AlphabetUpdatePrm
	expFrostFSAlphabetUpd.SetID(id)
	expFrostFSAlphabetUpd.SetPubs(testKeys.newAlphabetExp)

	require.EqualValues(t, []frostfscontract.AlphabetUpdatePrm{expFrostFSAlphabetUpd}, f.updates, "invalid FrostFS alphabet updates")
}

func TestHandleAlphabetDesignateEvent(t *testing.T) {
	t.Parallel()
	testKeys := generateTestKeys(t)

	es := &testEpochState{
		epoch: 100,
	}
	as := &testAlphabetState{
		isAlphabet: true,
	}
	v := &testVoter{}
	irf := &testIRFetcher{
		publicKeys: testKeys.sidechainKeys,
	}
	m := &testMorphClient{
		commiteeKeys: testKeys.sidechainKeys,
	}
	mn := &testMainnetClient{
		alphabetKeys: testKeys.mainnetKeys,
	}
	f := &testFrostFSClient{}
	nm := &testNetmapClient{}

	proc, err := New(
		&Params{
			Log:           test.NewLogger(t, true),
			EpochState:    es,
			AlphabetState: as,
			Voter:         v,
			IRFetcher:     irf,
			MorphClient:   m,
			MainnetClient: mn,
			FrostFSClient: f,
			NetmapClient:  nm,
		},
	)

	require.NoError(t, err, "failed to create processor")

	ev := rolemanagement.Designate{
		TxHash: util.Uint256{100},
		Role:   noderoles.NeoFSAlphabet,
	}

	proc.HandleAlphabetSync(ev)

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

	require.EqualValues(t, []VoteValidatorPrm{
		{
			Validators: testKeys.newAlphabetExp,
			Hash:       &ev.TxHash,
		},
	}, v.votes, "invalid vote calls")

	var irUpdatesExp []nmClient.UpdateIRPrm
	require.EqualValues(t, irUpdatesExp, nm.updates, "invalid IR updates")

	var alpabetUpdExp client.UpdateAlphabetListPrm
	alpabetUpdExp.SetList(testKeys.newInnerRingExp)
	alpabetUpdExp.SetHash(ev.TxHash)
	require.EqualValues(t, []client.UpdateAlphabetListPrm{alpabetUpdExp}, m.alphabetUpdates, "invalid alphabet updates")

	var expNotaryUpdate client.UpdateNotaryListPrm
	expNotaryUpdate.SetList(testKeys.newAlphabetExp)
	expNotaryUpdate.SetHash(ev.TxHash)
	require.EqualValues(t, []client.UpdateNotaryListPrm{expNotaryUpdate}, m.notaryUpdates, "invalid notary list updates")

	buf := make([]byte, 8)
	binary.LittleEndian.PutUint64(buf, es.epoch)

	id := append([]byte(alphabetUpdateIDPrefix), buf...)
	var expFrostFSAlphabetUpd frostfscontract.AlphabetUpdatePrm
	expFrostFSAlphabetUpd.SetID(id)
	expFrostFSAlphabetUpd.SetPubs(testKeys.newAlphabetExp)

	require.EqualValues(t, []frostfscontract.AlphabetUpdatePrm{expFrostFSAlphabetUpd}, f.updates, "invalid FrostFS alphabet updates")
}

type testKeys struct {
	sidechainKeys   keys.PublicKeys
	mainnetKeys     keys.PublicKeys
	newAlphabetExp  keys.PublicKeys
	newInnerRingExp keys.PublicKeys
}

func generateTestKeys(t *testing.T) testKeys {
	for {
		var result testKeys

		for i := 0; i < 4; i++ {
			pk, err := keys.NewPrivateKey()
			require.NoError(t, err, "failed to create private key")
			result.sidechainKeys = append(result.sidechainKeys, pk.PublicKey())
		}

		result.mainnetKeys = append(result.mainnetKeys, result.sidechainKeys...)
		pk, err := keys.NewPrivateKey()
		require.NoError(t, err, "failed to create private key")
		result.mainnetKeys = append(result.mainnetKeys, pk.PublicKey())

		result.newAlphabetExp, err = newAlphabetList(result.sidechainKeys, result.mainnetKeys)
		require.NoError(t, err, "failed to create expected new alphabet")

		if len(result.newAlphabetExp) == 0 {
			continue //can be happen because of random and sort
		}

		var irKeys keys.PublicKeys
		irKeys = append(irKeys, result.sidechainKeys...)
		result.newInnerRingExp, err = updateInnerRing(irKeys, result.sidechainKeys, result.newAlphabetExp)
		require.NoError(t, err, "failed to create expected new IR")
		sort.Sort(result.newInnerRingExp)

		return result
	}
}

type testEpochState struct {
	epoch uint64
}

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

type testAlphabetState struct {
	isAlphabet bool
}

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

type testVoter struct {
	votes []VoteValidatorPrm
}

func (v *testVoter) VoteForSidechainValidator(prm VoteValidatorPrm) error {
	v.votes = append(v.votes, prm)
	return nil
}

type testIRFetcher struct {
	publicKeys keys.PublicKeys
}

func (f *testIRFetcher) InnerRingKeys() (keys.PublicKeys, error) {
	return f.publicKeys, nil
}

type testMorphClient struct {
	commiteeKeys keys.PublicKeys

	alphabetUpdates []client.UpdateAlphabetListPrm
	notaryUpdates   []client.UpdateNotaryListPrm
}

func (c *testMorphClient) Committee() (res keys.PublicKeys, err error) {
	return c.commiteeKeys, nil
}

func (c *testMorphClient) UpdateNeoFSAlphabetList(prm client.UpdateAlphabetListPrm) error {
	c.alphabetUpdates = append(c.alphabetUpdates, prm)
	return nil
}

func (c *testMorphClient) UpdateNotaryList(prm client.UpdateNotaryListPrm) error {
	c.notaryUpdates = append(c.notaryUpdates, prm)
	return nil
}

type testMainnetClient struct {
	alphabetKeys  keys.PublicKeys
	designateHash util.Uint160
}

func (c *testMainnetClient) NeoFSAlphabetList() (res keys.PublicKeys, err error) {
	return c.alphabetKeys, nil
}

func (c *testMainnetClient) GetDesignateHash() util.Uint160 {
	return c.designateHash
}

type testFrostFSClient struct {
	updates []frostfscontract.AlphabetUpdatePrm
}

func (c *testFrostFSClient) AlphabetUpdate(p frostfscontract.AlphabetUpdatePrm) error {
	c.updates = append(c.updates, p)
	return nil
}

type testNetmapClient struct {
	updates []nmClient.UpdateIRPrm
}

func (c *testNetmapClient) UpdateInnerRing(p nmClient.UpdateIRPrm) error {
	c.updates = append(c.updates, p)
	return nil
}