package chanbackup

import (
	"fmt"
	"net"
	"testing"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/stretchr/testify/require"
)

type mockChannelSource struct {
	chans map[wire.OutPoint]*channeldb.OpenChannel

	failQuery bool

	addrs map[[33]byte][]net.Addr
}

func newMockChannelSource() *mockChannelSource {
	return &mockChannelSource{
		chans: make(map[wire.OutPoint]*channeldb.OpenChannel),
		addrs: make(map[[33]byte][]net.Addr),
	}
}

func (m *mockChannelSource) FetchAllChannels() ([]*channeldb.OpenChannel, error) {
	if m.failQuery {
		return nil, fmt.Errorf("fail")
	}

	chans := make([]*channeldb.OpenChannel, 0, len(m.chans))
	for _, channel := range m.chans {
		chans = append(chans, channel)
	}

	return chans, nil
}

func (m *mockChannelSource) FetchChannel(_ kvdb.RTx, chanPoint wire.OutPoint) (
	*channeldb.OpenChannel, error) {

	if m.failQuery {
		return nil, fmt.Errorf("fail")
	}

	channel, ok := m.chans[chanPoint]
	if !ok {
		return nil, fmt.Errorf("can't find chan")
	}

	return channel, nil
}

func (m *mockChannelSource) addAddrsForNode(nodePub *btcec.PublicKey, addrs []net.Addr) {
	var nodeKey [33]byte
	copy(nodeKey[:], nodePub.SerializeCompressed())

	m.addrs[nodeKey] = addrs
}

func (m *mockChannelSource) AddrsForNode(nodePub *btcec.PublicKey) ([]net.Addr, error) {
	if m.failQuery {
		return nil, fmt.Errorf("fail")
	}

	var nodeKey [33]byte
	copy(nodeKey[:], nodePub.SerializeCompressed())

	addrs, ok := m.addrs[nodeKey]
	if !ok {
		return nil, fmt.Errorf("can't find addr")
	}

	return addrs, nil
}

// TestFetchBackupForChan tests that we're able to construct a single channel
// backup for channels that are known, unknown, and also channels in which we
// can find addresses for and otherwise.
func TestFetchBackupForChan(t *testing.T) {
	t.Parallel()

	// First, we'll make two channels, only one of them will have all the
	// information we need to construct set of backups for them.
	randomChan1, err := genRandomOpenChannelShell()
	require.NoError(t, err, "unable to generate chan")
	randomChan2, err := genRandomOpenChannelShell()
	require.NoError(t, err, "unable to generate chan")

	chanSource := newMockChannelSource()
	chanSource.chans[randomChan1.FundingOutpoint] = randomChan1
	chanSource.chans[randomChan2.FundingOutpoint] = randomChan2

	chanSource.addAddrsForNode(randomChan1.IdentityPub, []net.Addr{addr1})

	testCases := []struct {
		chanPoint wire.OutPoint

		pass bool
	}{
		// Able to find channel, and addresses, should pass.
		{
			chanPoint: randomChan1.FundingOutpoint,
			pass:      true,
		},

		// Able to find channel, not able to find addrs, should fail.
		{
			chanPoint: randomChan2.FundingOutpoint,
			pass:      false,
		},

		// Not able to find channel, should fail.
		{
			chanPoint: op,
			pass:      false,
		},
	}
	for i, testCase := range testCases {
		_, err := FetchBackupForChan(
			testCase.chanPoint, chanSource, chanSource,
		)
		switch {
		// If this is a valid test case, and we failed, then we'll
		// return an error.
		case err != nil && testCase.pass:
			t.Fatalf("#%v, unable to make chan  backup: %v", i, err)

		// If this is an invalid test case, and we passed it, then
		// we'll return an error.
		case err == nil && !testCase.pass:
			t.Fatalf("#%v got nil error for invalid req: %v",
				i, err)
		}
	}
}

// TestFetchStaticChanBackups tests that we're able to properly query the
// channel source for all channels and construct a Single for each channel.
func TestFetchStaticChanBackups(t *testing.T) {
	t.Parallel()

	// First, we'll make the set of channels that we want to seed the
	// channel source with. Both channels will be fully populated in the
	// channel source.
	const numChans = 2
	randomChan1, err := genRandomOpenChannelShell()
	require.NoError(t, err, "unable to generate chan")
	randomChan2, err := genRandomOpenChannelShell()
	require.NoError(t, err, "unable to generate chan")

	chanSource := newMockChannelSource()
	chanSource.chans[randomChan1.FundingOutpoint] = randomChan1
	chanSource.chans[randomChan2.FundingOutpoint] = randomChan2
	chanSource.addAddrsForNode(randomChan1.IdentityPub, []net.Addr{addr1})
	chanSource.addAddrsForNode(randomChan2.IdentityPub, []net.Addr{addr2})

	// With the channel source populated, we'll now attempt to create a set
	// of backups for all the channels. This should succeed, as all items
	// are populated within the channel source.
	backups, err := FetchStaticChanBackups(chanSource, chanSource)
	require.NoError(t, err, "unable to create chan back ups")

	if len(backups) != numChans {
		t.Fatalf("expected %v chans, instead got %v", numChans,
			len(backups))
	}

	// We'll attempt to create a set up backups again, but this time the
	// second channel will have missing information, which should cause the
	// query to fail.
	var n [33]byte
	copy(n[:], randomChan2.IdentityPub.SerializeCompressed())
	delete(chanSource.addrs, n)

	_, err = FetchStaticChanBackups(chanSource, chanSource)
	if err == nil {
		t.Fatalf("query with incomplete information should fail")
	}

	// To wrap up, we'll ensure that if we're unable to query the channel
	// source at all, then we'll fail as well.
	chanSource = newMockChannelSource()
	chanSource.failQuery = true
	_, err = FetchStaticChanBackups(chanSource, chanSource)
	if err == nil {
		t.Fatalf("query should fail")
	}
}
