package aliasmgr

import (
	"math/rand"
	"path/filepath"
	"testing"

	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/stretchr/testify/require"
)

// TestAliasStorePeerAlias tests that putting and retrieving a peer's alias
// works properly.
func TestAliasStorePeerAlias(t *testing.T) {
	t.Parallel()

	// Create the backend database and use this to create the aliasStore.
	dbPath := filepath.Join(t.TempDir(), "testdb")
	db, err := kvdb.Create(
		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
	)
	require.NoError(t, err)
	defer db.Close()

	linkUpdater := func(shortID lnwire.ShortChannelID) error {
		return nil
	}

	aliasStore, err := NewManager(db, linkUpdater)
	require.NoError(t, err)

	var chanID1 [32]byte
	_, err = rand.Read(chanID1[:])
	require.NoError(t, err)

	// Test that we can put the (chanID, alias) mapping in the database.
	// Also check that we retrieve exactly what we put in.
	err = aliasStore.PutPeerAlias(chanID1, StartingAlias)
	require.NoError(t, err)

	storedAlias, err := aliasStore.GetPeerAlias(chanID1)
	require.NoError(t, err)
	require.Equal(t, StartingAlias, storedAlias)
}

// TestAliasStoreRequest tests that the aliasStore delivers the expected SCID.
func TestAliasStoreRequest(t *testing.T) {
	t.Parallel()

	// Create the backend database and use this to create the aliasStore.
	dbPath := filepath.Join(t.TempDir(), "testdb")
	db, err := kvdb.Create(
		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
	)
	require.NoError(t, err)
	defer db.Close()

	linkUpdater := func(shortID lnwire.ShortChannelID) error {
		return nil
	}

	aliasStore, err := NewManager(db, linkUpdater)
	require.NoError(t, err)

	// We'll assert that the very first alias we receive is StartingAlias.
	alias1, err := aliasStore.RequestAlias()
	require.NoError(t, err)
	require.Equal(t, StartingAlias, alias1)

	// The next alias should be the result of passing in StartingAlias to
	// getNextScid.
	nextAlias := getNextScid(alias1)
	alias2, err := aliasStore.RequestAlias()
	require.NoError(t, err)
	require.Equal(t, nextAlias, alias2)
}

// TestAliasLifecycle tests that the aliases can be created and deleted.
func TestAliasLifecycle(t *testing.T) {
	t.Parallel()

	// Create the backend database and use this to create the aliasStore.
	dbPath := filepath.Join(t.TempDir(), "testdb")
	db, err := kvdb.Create(
		kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
	)
	require.NoError(t, err)
	defer db.Close()

	updateChan := make(chan struct{}, 1)

	linkUpdater := func(shortID lnwire.ShortChannelID) error {
		updateChan <- struct{}{}
		return nil
	}

	aliasStore, err := NewManager(db, linkUpdater)
	require.NoError(t, err)

	const (
		base  = uint64(123123123)
		alias = uint64(456456456)
	)

	// Parse the aliases and base to short channel ID format.
	baseScid := lnwire.NewShortChanIDFromInt(base)
	aliasScid := lnwire.NewShortChanIDFromInt(alias)
	aliasScid2 := lnwire.NewShortChanIDFromInt(alias + 1)

	// Add the first alias.
	err = aliasStore.AddLocalAlias(aliasScid, baseScid, false, true)
	require.NoError(t, err)

	// The link updater should be called.
	<-updateChan

	// Query the aliases and verify the results.
	aliasList := aliasStore.GetAliases(baseScid)
	require.Len(t, aliasList, 1)
	require.Contains(t, aliasList, aliasScid)

	// Add the second alias.
	err = aliasStore.AddLocalAlias(aliasScid2, baseScid, false, true)
	require.NoError(t, err)

	// The link updater should be called.
	<-updateChan

	// Query the aliases and verify the results.
	aliasList = aliasStore.GetAliases(baseScid)
	require.Len(t, aliasList, 2)
	require.Contains(t, aliasList, aliasScid)
	require.Contains(t, aliasList, aliasScid2)

	// Delete the first alias.
	err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
	require.NoError(t, err)

	// The link updater should be called.
	<-updateChan

	// We expect to get an error if we attempt to delete the same alias
	// again.
	err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
	require.ErrorIs(t, err, ErrAliasNotFound)

	// The link updater should _not_ be called.
	select {
	case <-updateChan:
		t.Fatal("link alias updater should not have been called")
	default:
	}

	// Query the aliases and verify that first one doesn't exist anymore.
	aliasList = aliasStore.GetAliases(baseScid)
	require.Len(t, aliasList, 1)
	require.Contains(t, aliasList, aliasScid2)
	require.NotContains(t, aliasList, aliasScid)

	// Delete the second alias.
	err = aliasStore.DeleteLocalAlias(aliasScid2, baseScid)
	require.NoError(t, err)

	// The link updater should be called.
	<-updateChan

	// Query the aliases and verify that none exists.
	aliasList = aliasStore.GetAliases(baseScid)
	require.Len(t, aliasList, 0)

	// We now request an alias generated by the aliasStore. This should give
	// the first from the pre-defined list of allocated aliases.
	firstRequested, err := aliasStore.RequestAlias()
	require.NoError(t, err)
	require.Equal(t, StartingAlias, firstRequested)

	// We now manually add the next alias from the range as a custom alias.
	secondAlias := getNextScid(firstRequested)
	err = aliasStore.AddLocalAlias(secondAlias, baseScid, false, true)
	require.NoError(t, err)

	// When we now request another alias from the allocation list, we expect
	// the third one (tx position 2) to be returned.
	thirdRequested, err := aliasStore.RequestAlias()
	require.NoError(t, err)
	require.Equal(t, getNextScid(secondAlias), thirdRequested)
	require.EqualValues(t, 2, thirdRequested.TxPosition)
}

// TestGetNextScid tests that given a current lnwire.ShortChannelID,
// getNextScid returns the expected alias to use next.
func TestGetNextScid(t *testing.T) {
	tests := []struct {
		name     string
		current  lnwire.ShortChannelID
		expected lnwire.ShortChannelID
	}{
		{
			name:    "starting alias",
			current: StartingAlias,
			expected: lnwire.ShortChannelID{
				BlockHeight: AliasStartBlockHeight,
				TxIndex:     0,
				TxPosition:  1,
			},
		},
		{
			name: "txposition rollover",
			current: lnwire.ShortChannelID{
				BlockHeight: 16_100_000,
				TxIndex:     15,
				TxPosition:  65535,
			},
			expected: lnwire.ShortChannelID{
				BlockHeight: 16_100_000,
				TxIndex:     16,
				TxPosition:  0,
			},
		},
		{
			name: "txindex max no rollover",
			current: lnwire.ShortChannelID{
				BlockHeight: 16_100_000,
				TxIndex:     16777215,
				TxPosition:  15,
			},
			expected: lnwire.ShortChannelID{
				BlockHeight: 16_100_000,
				TxIndex:     16777215,
				TxPosition:  16,
			},
		},
		{
			name: "txindex rollover",
			current: lnwire.ShortChannelID{
				BlockHeight: 16_100_000,
				TxIndex:     16777215,
				TxPosition:  65535,
			},
			expected: lnwire.ShortChannelID{
				BlockHeight: 16_100_001,
				TxIndex:     0,
				TxPosition:  0,
			},
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			nextScid := getNextScid(test.current)
			require.Equal(t, test.expected, nextScid)
		})
	}
}
