package innerring

import (
	"fmt"
	"sync/atomic"
	"testing"
	"time"

	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/stretchr/testify/require"
)

func TestIndexerReturnsIndexes(t *testing.T) {
	t.Parallel()
	commiteeKeys, err := keys.NewPublicKeysFromStrings([]string{
		"03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae",
		"022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131",
	})
	require.NoError(t, err, "convert string to commitee public keys failed")
	cf := &testCommiteeFetcher{
		keys: commiteeKeys,
	}

	irKeys, err := keys.NewPublicKeysFromStrings([]string{
		"038c862959e56b43e20f79187c4fe9e0bc7c8c66c1603e6cf0ec7f87ab6b08dc35",
		"02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3",
		"022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131",
	})
	require.NoError(t, err, "convert string to IR public keys failed")
	irf := &testIRFetcher{
		keys: irKeys,
	}

	t.Run("success", func(t *testing.T) {
		t.Parallel()
		key := irKeys[2]

		indexer := newInnerRingIndexer(cf, irf, key, time.Second)

		idx, err := indexer.AlphabetIndex()
		require.NoError(t, err, "failed to get alphabet index")
		require.Equal(t, int32(1), idx, "invalid alphabet index")

		idx, err = indexer.InnerRingIndex()
		require.NoError(t, err, "failed to get IR index")
		require.Equal(t, int32(2), idx, "invalid IR index")

		size, err := indexer.InnerRingSize()
		require.NoError(t, err, "failed to get IR size")
		require.Equal(t, int32(3), size, "invalid IR size")
	})

	t.Run("not found alphabet", func(t *testing.T) {
		t.Parallel()
		key := irKeys[0]

		indexer := newInnerRingIndexer(cf, irf, key, time.Second)

		idx, err := indexer.AlphabetIndex()
		require.NoError(t, err, "failed to get alphabet index")
		require.Equal(t, int32(-1), idx, "invalid alphabet index")

		idx, err = indexer.InnerRingIndex()
		require.NoError(t, err, "failed to get IR index")
		require.Equal(t, int32(0), idx, "invalid IR index")
	})

	t.Run("not found IR", func(t *testing.T) {
		t.Parallel()
		key := commiteeKeys[0]

		indexer := newInnerRingIndexer(cf, irf, key, time.Second)

		idx, err := indexer.AlphabetIndex()
		require.NoError(t, err, "failed to get alphabet index")
		require.Equal(t, int32(0), idx, "invalid alphabet index")

		idx, err = indexer.InnerRingIndex()
		require.NoError(t, err, "failed to get IR index")
		require.Equal(t, int32(-1), idx, "invalid IR index")
	})
}

func TestIndexerCachesIndexes(t *testing.T) {
	t.Parallel()
	commiteeKeys, err := keys.NewPublicKeysFromStrings([]string{})
	require.NoError(t, err, "convert string to commitee public keys failed")
	cf := &testCommiteeFetcher{
		keys: commiteeKeys,
	}

	irKeys, err := keys.NewPublicKeysFromStrings([]string{})
	require.NoError(t, err, "convert string to IR public keys failed")
	irf := &testIRFetcher{
		keys: irKeys,
	}

	key, err := keys.NewPublicKeyFromString("022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131")
	require.NoError(t, err, "convert string to public key failed")

	indexer := newInnerRingIndexer(cf, irf, key, time.Second)

	idx, err := indexer.AlphabetIndex()
	require.NoError(t, err, "failed to get alphabet index")
	require.Equal(t, int32(-1), idx, "invalid alphabet index")

	idx, err = indexer.InnerRingIndex()
	require.NoError(t, err, "failed to get IR index")
	require.Equal(t, int32(-1), idx, "invalid IR index")

	size, err := indexer.InnerRingSize()
	require.NoError(t, err, "failed to get IR size")
	require.Equal(t, int32(0), size, "invalid IR size")

	require.Equal(t, int32(1), cf.calls.Load(), "invalid commitee calls count")
	require.Equal(t, int32(1), irf.calls.Load(), "invalid IR calls count")

	idx, err = indexer.AlphabetIndex()
	require.NoError(t, err, "failed to get alphabet index")
	require.Equal(t, int32(-1), idx, "invalid alphabet index")

	idx, err = indexer.InnerRingIndex()
	require.NoError(t, err, "failed to get IR index")
	require.Equal(t, int32(-1), idx, "invalid IR index")

	size, err = indexer.InnerRingSize()
	require.NoError(t, err, "failed to get IR size")
	require.Equal(t, int32(0), size, "invalid IR size")

	require.Equal(t, int32(1), cf.calls.Load(), "invalid commitee calls count")
	require.Equal(t, int32(1), irf.calls.Load(), "invalid IR calls count")

	time.Sleep(2 * time.Second)

	idx, err = indexer.AlphabetIndex()
	require.NoError(t, err, "failed to get alphabet index")
	require.Equal(t, int32(-1), idx, "invalid alphabet index")

	idx, err = indexer.InnerRingIndex()
	require.NoError(t, err, "failed to get IR index")
	require.Equal(t, int32(-1), idx, "invalid IR index")

	size, err = indexer.InnerRingSize()
	require.NoError(t, err, "failed to get IR size")
	require.Equal(t, int32(0), size, "invalid IR size")

	require.Equal(t, int32(2), cf.calls.Load(), "invalid commitee calls count")
	require.Equal(t, int32(2), irf.calls.Load(), "invalid IR calls count")
}

func TestIndexerThrowsErrors(t *testing.T) {
	t.Parallel()
	cf := &testCommiteeFetcher{
		err: fmt.Errorf("test commitee error"),
	}

	irKeys, err := keys.NewPublicKeysFromStrings([]string{})
	require.NoError(t, err, "convert string to IR public keys failed")
	irf := &testIRFetcher{
		keys: irKeys,
	}

	key, err := keys.NewPublicKeyFromString("022bb4041c50d607ff871dec7e4cd7778388e0ea6849d84ccbd9aa8f32e16a8131")
	require.NoError(t, err, "convert string to public key failed")

	indexer := newInnerRingIndexer(cf, irf, key, time.Second)

	idx, err := indexer.AlphabetIndex()
	require.ErrorContains(t, err, "test commitee error", "error from commitee not throwed")
	require.Equal(t, int32(0), idx, "invalid alphabet index")

	idx, err = indexer.InnerRingIndex()
	require.ErrorContains(t, err, "test commitee error", "error from IR not throwed")
	require.Equal(t, int32(0), idx, "invalid IR index")

	size, err := indexer.InnerRingSize()
	require.ErrorContains(t, err, "test commitee error", "error from IR not throwed")
	require.Equal(t, int32(0), size, "invalid IR size")

	commiteeKeys, err := keys.NewPublicKeysFromStrings([]string{})
	require.NoError(t, err, "convert string to commitee public keys failed")
	cf = &testCommiteeFetcher{
		keys: commiteeKeys,
	}

	irf = &testIRFetcher{
		err: fmt.Errorf("test IR error"),
	}

	indexer = newInnerRingIndexer(cf, irf, key, time.Second)

	idx, err = indexer.AlphabetIndex()
	require.ErrorContains(t, err, "test IR error", "error from commitee not throwed")
	require.Equal(t, int32(0), idx, "invalid alphabet index")

	idx, err = indexer.InnerRingIndex()
	require.ErrorContains(t, err, "test IR error", "error from IR not throwed")
	require.Equal(t, int32(0), idx, "invalid IR index")

	size, err = indexer.InnerRingSize()
	require.ErrorContains(t, err, "test IR error", "error from IR not throwed")
	require.Equal(t, int32(0), size, "invalid IR size")
}

type testCommiteeFetcher struct {
	keys  keys.PublicKeys
	err   error
	calls atomic.Int32
}

func (f *testCommiteeFetcher) Committee() (keys.PublicKeys, error) {
	f.calls.Add(1)
	return f.keys, f.err
}

type testIRFetcher struct {
	keys  keys.PublicKeys
	err   error
	calls atomic.Int32
}

func (f *testIRFetcher) InnerRingKeys() (keys.PublicKeys, error) {
	f.calls.Add(1)
	return f.keys, f.err
}

func BenchmarkKeyPosition(b *testing.B) {
	list := make(keys.PublicKeys, 7)
	for i := range list {
		p, err := keys.NewPrivateKey()
		require.NoError(b, err)
		list[i] = p.PublicKey()
	}

	key := new(keys.PublicKey)
	require.NoError(b, key.DecodeBytes(list[5].Bytes()))

	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		if keyPosition(key, list) != 5 {
			b.FailNow()
		}
	}
}