package routing

import (
	"bytes"
	"fmt"
	"image/color"
	"math"
	"math/rand"
	"net"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/davecgh/go-spew/spew"
	"github.com/go-errors/errors"
	sphinx "github.com/lightningnetwork/lightning-onion"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/channeldb/models"
	"github.com/lightningnetwork/lnd/clock"
	"github.com/lightningnetwork/lnd/fn"
	"github.com/lightningnetwork/lnd/graph"
	"github.com/lightningnetwork/lnd/htlcswitch"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/record"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/lightningnetwork/lnd/zpay32"
	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
)

var (
	uniquePaymentID uint64 = 1 // to be used atomically

	testAddr = &net.TCPAddr{IP: (net.IP)([]byte{0xA, 0x0, 0x0, 0x1}),
		Port: 9000}
	testAddrs = []net.Addr{testAddr}

	testFeatures = lnwire.NewFeatureVector(nil, lnwire.Features)

	testHash = [32]byte{
		0xb7, 0x94, 0x38, 0x5f, 0x2d, 0x1e, 0xf7, 0xab,
		0x4d, 0x92, 0x73, 0xd1, 0x90, 0x63, 0x81, 0xb4,
		0x4f, 0x2f, 0x6f, 0x25, 0x88, 0xa3, 0xef, 0xb9,
		0x6a, 0x49, 0x18, 0x83, 0x31, 0x98, 0x47, 0x53,
	}

	testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)

	priv1, _    = btcec.NewPrivateKey()
	bitcoinKey1 = priv1.PubKey()

	priv2, _    = btcec.NewPrivateKey()
	bitcoinKey2 = priv2.PubKey()
)

type testCtx struct {
	router *ChannelRouter

	graphBuilder *mockGraphBuilder

	graph *channeldb.ChannelGraph

	aliases map[string]route.Vertex

	privKeys map[string]*btcec.PrivateKey

	channelIDs map[route.Vertex]map[route.Vertex]uint64
}

func (c *testCtx) getChannelIDFromAlias(t *testing.T, a, b string) uint64 {
	vertexA, ok := c.aliases[a]
	require.True(t, ok, "cannot find aliases for %s", a)

	vertexB, ok := c.aliases[b]
	require.True(t, ok, "cannot find aliases for %s", b)

	channelIDMap, ok := c.channelIDs[vertexA]
	require.True(t, ok, "cannot find channelID map %s(%s)", vertexA, a)

	channelID, ok := channelIDMap[vertexB]
	require.True(t, ok, "cannot find channelID using %s(%s)", vertexB, b)

	return channelID
}

var mockClosedSCIDs map[lnwire.ShortChannelID]struct{}

func createTestCtxFromGraphInstance(t *testing.T, startingHeight uint32,
	graphInstance *testGraphInstance) *testCtx {

	return createTestCtxFromGraphInstanceAssumeValid(
		t, startingHeight, graphInstance,
	)
}

func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
	startingHeight uint32, graphInstance *testGraphInstance) *testCtx {

	// We'll initialize an instance of the channel router with mock
	// versions of the chain and channel notifier. As we don't need to test
	// any p2p functionality, the peer send and switch send messages won't
	// be populated.
	chain := newMockChain(startingHeight)

	pathFindingConfig := PathFindingConfig{
		MinProbability: 0.01,
		AttemptCost:    100,
	}

	aCfg := AprioriConfig{
		PenaltyHalfLife:       time.Hour,
		AprioriHopProbability: 0.9,
		AprioriWeight:         0.5,
		CapacityFraction:      testCapacityFraction,
	}
	estimator, err := NewAprioriEstimator(aCfg)
	require.NoError(t, err)

	mcConfig := &MissionControlConfig{Estimator: estimator}

	mcController, err := NewMissionController(
		graphInstance.graphBackend, route.Vertex{}, mcConfig,
	)
	require.NoError(t, err, "failed to create missioncontrol")

	mc, err := mcController.GetNamespacedStore(
		DefaultMissionControlNamespace,
	)
	require.NoError(t, err)

	sourceNode, err := graphInstance.graph.SourceNode()
	require.NoError(t, err)
	sessionSource := &SessionSource{
		GraphSessionFactory: newMockGraphSessionFactoryFromChanDB(
			graphInstance.graph,
		),
		SourceNode:        sourceNode,
		GetLink:           graphInstance.getLink,
		PathFindingConfig: pathFindingConfig,
		MissionControl:    mc,
	}

	graphBuilder := newMockGraphBuilder(graphInstance.graph)

	router, err := New(Config{
		SelfNode:       sourceNode.PubKeyBytes,
		RoutingGraph:   newMockGraphSessionChanDB(graphInstance.graph),
		Chain:          chain,
		Payer:          &mockPaymentAttemptDispatcherOld{},
		Control:        makeMockControlTower(),
		MissionControl: mc,
		SessionSource:  sessionSource,
		GetLink:        graphInstance.getLink,
		NextPaymentID: func() (uint64, error) {
			next := atomic.AddUint64(&uniquePaymentID, 1)
			return next, nil
		},
		PathFindingConfig:  pathFindingConfig,
		Clock:              clock.NewTestClock(time.Unix(1, 0)),
		ApplyChannelUpdate: graphBuilder.ApplyChannelUpdate,
		ClosedSCIDs:        mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](
			&mockTrafficShaper{},
		),
	})
	require.NoError(t, router.Start(), "unable to start router")

	ctx := &testCtx{
		router:       router,
		graphBuilder: graphBuilder,
		graph:        graphInstance.graph,
		aliases:      graphInstance.aliasMap,
		privKeys:     graphInstance.privKeyMap,
		channelIDs:   graphInstance.channelIDs,
	}

	t.Cleanup(func() {
		ctx.router.Stop()
	})

	return ctx
}

func createTestNode() (*channeldb.LightningNode, error) {
	updateTime := rand.Int63()

	priv, err := btcec.NewPrivateKey()
	if err != nil {
		return nil, errors.Errorf("unable create private key: %v", err)
	}

	pub := priv.PubKey().SerializeCompressed()
	n := &channeldb.LightningNode{
		HaveNodeAnnouncement: true,
		LastUpdate:           time.Unix(updateTime, 0),
		Addresses:            testAddrs,
		Color:                color.RGBA{1, 2, 3, 0},
		Alias:                "kek" + string(pub),
		AuthSigBytes:         testSig.Serialize(),
		Features:             testFeatures,
	}
	copy(n.PubKeyBytes[:], pub)

	return n, nil
}

func createTestCtxFromFile(t *testing.T,
	startingHeight uint32, testGraph string) *testCtx {

	// We'll attempt to locate and parse out the file
	// that encodes the graph that our tests should be run against.
	graphInstance, err := parseTestGraph(t, true, testGraph)
	require.NoError(t, err, "unable to create test graph")

	return createTestCtxFromGraphInstance(t, startingHeight, graphInstance)
}

// Add valid signature to channel update simulated as error received from the
// network.
func signErrChanUpdate(t *testing.T, key *btcec.PrivateKey,
	errChanUpdate *lnwire.ChannelUpdate1) {

	chanUpdateMsg, err := errChanUpdate.DataToSign()
	require.NoError(t, err, "failed to retrieve data to sign")

	digest := chainhash.DoubleHashB(chanUpdateMsg)
	sig := ecdsa.Sign(key, digest)

	errChanUpdate.Signature, err = lnwire.NewSigFromSignature(sig)
	require.NoError(t, err, "failed to create new signature")
}

// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
// within the channel router contain a total fee less than or equal to the fee
// limit.
func TestFindRoutesWithFeeLimit(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// This test will attempt to find routes from roasbeef to sophon for 100
	// satoshis with a fee limit of 10 satoshis. There are two routes from
	// roasbeef to sophon:
	//	1. roasbeef -> songoku -> sophon
	//	2. roasbeef -> phamnuwen -> sophon
	// The second route violates our fee limit, so we should only expect to
	// see the first route.
	target := ctx.aliases["sophon"]
	paymentAmt := lnwire.NewMSatFromSatoshis(100)
	restrictions := &RestrictParams{
		FeeLimit:          lnwire.NewMSatFromSatoshis(10),
		ProbabilitySource: noProbabilitySource,
		CltvLimit:         math.MaxUint32,
	}

	req, err := NewRouteRequest(
		ctx.router.cfg.SelfNode, &target, paymentAmt, 0,
		restrictions, nil, nil, nil, MinCLTVDelta,
	)
	require.NoError(t, err, "invalid route request")

	route, _, err := ctx.router.FindRoute(req)
	require.NoError(t, err, "unable to find any routes")

	require.Falsef(t,
		route.TotalFees() > restrictions.FeeLimit,
		"route exceeded fee limit: %v", spew.Sdump(route),
	)

	hops := route.Hops
	require.Equal(t, 2, len(hops), "expected 2 hops")

	require.Equalf(t,
		ctx.aliases["songoku"], hops[0].PubKeyBytes,
		"expected first hop through songoku, got %s",
		getAliasFromPubKey(hops[0].PubKeyBytes, ctx.aliases),
	)
}

// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
// one of the target routes is seen as unavailable, then the next route in the
// queue is used instead. This process should continue until either a payment
// succeeds, or all routes have been exhausted.
func TestSendPaymentRouteFailureFallback(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
	paymentAmt := lnwire.NewMSatFromSatoshis(1000)
	payment := createDummyLightningPayment(
		t, ctx.aliases["sophon"], paymentAmt,
	)

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	// Get the channel ID.
	roasbeefSongoku := lnwire.NewShortChanIDFromInt(
		ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
	)

	// We'll modify the SendToSwitch method that's been set within the
	// router's configuration to ignore the path that has son goku as the
	// first hop. This should force the router to instead take the
	// the more costly path (through pham nuwen).
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			if firstHop == roasbeefSongoku {
				return [32]byte{}, htlcswitch.NewForwardingError(
					// TODO(roasbeef): temp node failure
					//  should be?
					&lnwire.FailTemporaryChannelFailure{},
					1,
				)
			}

			return preImage, nil
		})

	// Send off the payment request to the router, route through pham nuwen
	// should've been selected as a fall back and succeeded correctly.
	paymentPreImage, route, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	// The route selected should have two hops
	require.Equal(t, 2, len(route.Hops), "incorrect route length")

	// The preimage should match up with the once created above.
	if !bytes.Equal(paymentPreImage[:], preImage[:]) {
		t.Fatalf("incorrect preimage used: expected %x got %x",
			preImage[:], paymentPreImage[:])
	}

	// The route should have pham nuwen as the first hop.
	require.Equalf(t,
		ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
		"route should go through phamnuwen as first hop, instead "+
			"passes through: %v",
		getAliasFromPubKey(route.Hops[0].PubKeyBytes, ctx.aliases),
	)
}

// TestSendPaymentRouteInfiniteLoopWithBadHopHint tests that when sending
// a payment with a malformed hop hint in the first hop, the hint is ignored
// and the payment succeeds without an infinite loop of retries.
func TestSendPaymentRouteInfiniteLoopWithBadHopHint(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	source := ctx.aliases["roasbeef"]
	sourceNodeID, err := btcec.ParsePubKey(source[:])
	require.NoError(t, err)

	actualChannelID := ctx.getChannelIDFromAlias(t, "roasbeef", "songoku")
	badChannelID := uint64(66666)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to songoku for 1000 satoshis.
	paymentAmt := lnwire.NewMSatFromSatoshis(1000)
	payment := createDummyLightningPayment(
		t, ctx.aliases["songoku"], paymentAmt,
	)
	payment.RouteHints = [][]zpay32.HopHint{{
		zpay32.HopHint{
			NodeID:          sourceNodeID,
			ChannelID:       badChannelID,
			FeeBaseMSat:     uint32(50),
			CLTVExpiryDelta: uint16(200),
		},
	}}

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	// Mock a payment result that always fails with FailUnknownNextPeer when
	// the bad channel is the first hop.
	badShortChanID := lnwire.NewShortChanIDFromInt(badChannelID)
	newFwdError := htlcswitch.NewForwardingError(
		&lnwire.FailUnknownNextPeer{}, 0,
	)

	payer, ok := ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld)
	require.Equal(t, ok, true, "failed Payer cast")

	payer.setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			// Returns a FailUnknownNextPeer if it's trying
			// to pay an invalid channel.
			if firstHop == badShortChanID {
				return [32]byte{}, newFwdError
			}

			return preImage, nil
		})

	// Send off the payment request to the router, should succeed
	// ignoring the bad channel id hint.
	paymentPreImage, route, paymentErr := ctx.router.SendPayment(payment)
	require.NoErrorf(t, paymentErr, "unable to send payment: %v",
		payment.paymentHash)

	// The preimage should match up with the one created above.
	require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")

	// The route should have songoku as the first hop.
	require.Equal(t, actualChannelID, route.Hops[0].ChannelID,
		"route should go through the correct channel id",
	)
}

// TestChannelUpdateValidation tests that a failed payment with an associated
// channel update will only be applied to the graph when the update contains a
// valid signature.
func TestChannelUpdateValidation(t *testing.T) {
	t.Parallel()

	// Setup a three node network.
	chanCapSat := btcutil.Amount(100000)
	feeRate := lnwire.MilliSatoshi(400)
	testChannels := []*testChannel{
		symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: feeRate,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 1),
		symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: feeRate,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 2),
	}

	testGraph, err := createTestGraphFromChannels(
		t, true, testChannels, "a",
	)
	require.NoError(t, err, "unable to create graph")

	const startingBlockHeight = 101
	ctx := createTestCtxFromGraphInstance(t, startingBlockHeight, testGraph)

	// Assert that the initially configured fee is retrieved correctly.
	_, e1, e2, err := ctx.graph.FetchChannelEdgesByID(
		lnwire.NewShortChanIDFromInt(1).ToUint64(),
	)
	require.NoError(t, err, "cannot retrieve channel")

	require.Equal(t, feeRate, e1.FeeProportionalMillionths, "invalid fee")
	require.Equal(t, feeRate, e2.FeeProportionalMillionths, "invalid fee")

	// Setup a route from source a to destination c. The route will be used
	// in a call to SendToRoute. SendToRoute also applies channel updates,
	// but it saves us from including RequestRoute in the test scope too.
	hop1 := ctx.aliases["b"]
	hop2 := ctx.aliases["c"]
	hops := []*route.Hop{
		{
			ChannelID:     1,
			PubKeyBytes:   hop1,
			LegacyPayload: true,
		},
		{
			ChannelID:     2,
			PubKeyBytes:   hop2,
			LegacyPayload: true,
		},
	}

	rt, err := route.NewRouteFromHops(
		lnwire.MilliSatoshi(10000), 100,
		ctx.aliases["a"], hops,
	)
	require.NoError(t, err, "unable to create route")

	// Set up a channel update message with an invalid signature to be
	// returned to the sender.
	var invalidSignature lnwire.Sig
	errChanUpdate := lnwire.ChannelUpdate1{
		Signature:       invalidSignature,
		FeeRate:         500,
		ShortChannelID:  lnwire.NewShortChanIDFromInt(1),
		Timestamp:       uint32(testTime.Add(time.Minute).Unix()),
		MessageFlags:    e2.MessageFlags,
		ChannelFlags:    e2.ChannelFlags,
		HtlcMaximumMsat: e2.MaxHTLC,
	}

	// We'll modify the SendToSwitch method so that it simulates a failed
	// payment with an error originating from the first hop of the route.
	// The unsigned channel update is attached to the failure message.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			return [32]byte{}, htlcswitch.NewForwardingError(
				&lnwire.FailFeeInsufficient{
					Update: errChanUpdate,
				},
				1,
			)
		})

	// The payment parameter is mostly redundant in SendToRoute. Can be left
	// empty for this test.
	var payment lntypes.Hash

	// Instruct the mock graph builder to reject the next update we send
	// it.
	ctx.graphBuilder.setNextReject(true)

	// Send off the payment request to the router. The specified route
	// should be attempted and the channel update should be received by
	// graph and ignored because it is missing a valid signature.
	_, err = ctx.router.SendToRoute(payment, rt, nil)
	require.Error(t, err, "expected route to fail with channel update")

	_, e1, e2, err = ctx.graph.FetchChannelEdgesByID(
		lnwire.NewShortChanIDFromInt(1).ToUint64(),
	)
	require.NoError(t, err, "cannot retrieve channel")

	require.Equal(t, feeRate, e1.FeeProportionalMillionths,
		"fee updated without valid signature")
	require.Equal(t, feeRate, e2.FeeProportionalMillionths,
		"fee updated without valid signature")

	// Next, add a signature to the channel update.
	signErrChanUpdate(t, testGraph.privKeyMap["b"], &errChanUpdate)

	// Let the graph builder accept the next update.
	ctx.graphBuilder.setNextReject(false)

	// Retry the payment using the same route as before.
	_, err = ctx.router.SendToRoute(payment, rt, nil)
	require.Error(t, err, "expected route to fail with channel update")

	// This time a valid signature was supplied and the policy change should
	// have been applied to the graph.
	_, e1, e2, err = ctx.graph.FetchChannelEdgesByID(
		lnwire.NewShortChanIDFromInt(1).ToUint64(),
	)
	require.NoError(t, err, "cannot retrieve channel")

	require.Equal(t, feeRate, e1.FeeProportionalMillionths,
		"fee should not be updated")
	require.EqualValues(t, 500, int(e2.FeeProportionalMillionths),
		"fee not updated even though signature is valid")
}

// TestSendPaymentErrorRepeatedFeeInsufficient tests that if we receive
// multiple fee related errors from a channel that we're attempting to route
// through, then we'll prune the channel after the second attempt.
func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// Get the channel ID.
	roasbeefSongokuChanID := ctx.getChannelIDFromAlias(
		t, "roasbeef", "songoku",
	)
	songokuSophonChanID := ctx.getChannelIDFromAlias(
		t, "songoku", "sophon",
	)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to sophon for 1000 satoshis.
	amt := lnwire.NewMSatFromSatoshis(1000)
	payment := createDummyLightningPayment(
		t, ctx.aliases["sophon"], amt,
	)

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	// We'll also fetch the first outgoing channel edge from son goku
	// to sophon. We'll obtain this as we'll need to to generate the
	// FeeInsufficient error that we'll send back.
	_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(
		songokuSophonChanID,
	)
	require.NoError(t, err, "unable to fetch chan id")

	errChanUpdate := lnwire.ChannelUpdate1{
		ShortChannelID: lnwire.NewShortChanIDFromInt(
			songokuSophonChanID,
		),
		Timestamp:       uint32(edgeUpdateToFail.LastUpdate.Unix()),
		MessageFlags:    edgeUpdateToFail.MessageFlags,
		ChannelFlags:    edgeUpdateToFail.ChannelFlags,
		TimeLockDelta:   edgeUpdateToFail.TimeLockDelta,
		HtlcMinimumMsat: edgeUpdateToFail.MinHTLC,
		HtlcMaximumMsat: edgeUpdateToFail.MaxHTLC,
		BaseFee:         uint32(edgeUpdateToFail.FeeBaseMSat),
		FeeRate:         uint32(edgeUpdateToFail.FeeProportionalMillionths),
	}

	signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)

	// We'll now modify the SendToSwitch method to return an error for the
	// outgoing channel to Son goku. This will be a fee related error, so
	// it should only cause the edge to be pruned after the second attempt.
	dispatcher, ok := ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld) //nolint:lll
	require.True(t, ok)

	dispatcher.setPaymentResult(func(firstHop lnwire.ShortChannelID) (
		[32]byte, error) {

		roasbeefSongoku := lnwire.NewShortChanIDFromInt(
			roasbeefSongokuChanID,
		)
		if firstHop == roasbeefSongoku {
			return [32]byte{}, htlcswitch.NewForwardingError(
				// Within our error, we'll add a
				// channel update which is meant to
				// reflect the new fee schedule for the
				// node/channel.
				&lnwire.FailFeeInsufficient{
					Update: errChanUpdate,
				}, 1,
			)
		}

		return preImage, nil
	})

	// Send off the payment request to the router, route through phamnuwen
	// should've been selected as a fall back and succeeded correctly.
	paymentPreImage, route, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	// The route selected should have two hops
	require.Equal(t, 2, len(route.Hops), "incorrect route length")

	// The preimage should match up with the once created above.
	require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")

	// The route should have pham nuwen as the first hop.
	require.Equalf(t,
		ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
		"route should go through pham nuwen as first hop, "+
			"instead passes through: %v",
		getAliasFromPubKey(route.Hops[0].PubKeyBytes, ctx.aliases),
	)
}

// TestSendPaymentErrorFeeInsufficientPrivateEdge tests that if we receive
// a fee related error from a private channel that we're attempting to route
// through, then we'll update the fees in the route hints and successfully
// route through the private channel in the second attempt.
//
// The test will send a payment from roasbeef to elst, available paths are,
// path1: roasbeef -> songoku -> sophon -> elst, total fee: 210k
// path2: roasbeef -> phamnuwen -> sophon -> elst, total fee: 220k
// path3: roasbeef -> songoku ->(private channel) elst
// We will setup the path3 to have the lowest fee so it's always the preferred
// path.
func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// Get the channel ID.
	roasbeefSongoku := lnwire.NewShortChanIDFromInt(
		ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
	)

	var (
		preImage         [32]byte
		amt              = lnwire.NewMSatFromSatoshis(1000)
		privateChannelID = uint64(55555)
		feeBaseMSat      = uint32(15)
		expiryDelta      = uint16(32)
		sgNode           = ctx.aliases["songoku"]
	)

	sgNodeID, err := btcec.ParsePubKey(sgNode[:])
	require.NoError(t, err)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to elst, through a private channel between songoku and elst for
	// 1000 satoshis. This route has lowest fees compared with the rest.
	// This also holds when the private channel fee is updated to a higher
	// value.
	payment := createDummyLightningPayment(
		t, ctx.aliases["elst"], amt,
	)
	payment.RouteHints = [][]zpay32.HopHint{{
		// Add a private channel between songoku and elst.
		zpay32.HopHint{
			NodeID:          sgNodeID,
			ChannelID:       privateChannelID,
			FeeBaseMSat:     feeBaseMSat,
			CLTVExpiryDelta: expiryDelta,
		},
	}}

	// Prepare an error update for the private channel, with twice the
	// original fee.
	updatedFeeBaseMSat := feeBaseMSat * 2
	errChanUpdate := lnwire.ChannelUpdate1{
		ShortChannelID: lnwire.NewShortChanIDFromInt(privateChannelID),
		Timestamp:      uint32(testTime.Add(time.Minute).Unix()),
		BaseFee:        updatedFeeBaseMSat,
		TimeLockDelta:  expiryDelta,
	}
	signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)

	// We'll now modify the SendHTLC method to return an error for the
	// outgoing channel to songoku.
	errorReturned := false
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			if firstHop != roasbeefSongoku || errorReturned {
				return preImage, nil
			}

			errorReturned = true
			return [32]byte{}, htlcswitch.NewForwardingError(
				// Within our error, we'll add a
				// channel update which is meant to
				// reflect the new fee schedule for the
				// node/channel.
				&lnwire.FailFeeInsufficient{
					Update: errChanUpdate,
				}, 1,
			)
		},
	)

	// Send off the payment request to the router, route through son
	// goku and then across the private channel to elst.
	paymentPreImage, route, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	require.True(t, errorReturned,
		"failed to simulate error in the first payment attempt",
	)

	// The route selected should have two hops. Make sure that,
	//   path: roasbeef -> son goku -> sophon -> elst
	//   path: roasbeef -> pham nuwen -> sophon -> elst
	// are not selected instead.
	require.Equal(t, 2, len(route.Hops), "incorrect route length")

	// The preimage should match up with the one created above.
	require.Equal(t,
		paymentPreImage[:], preImage[:], "incorrect preimage used",
	)

	// The route should have son goku as the first hop.
	require.Equal(t, route.Hops[0].PubKeyBytes, ctx.aliases["songoku"],
		"route should go through son goku as first hop",
	)

	// The route should pass via the private channel.
	require.Equal(t,
		privateChannelID, route.FinalHop().ChannelID,
		"route did not pass through private channel "+
			"between pham nuwen and elst",
	)

	// The route should have the updated fee.
	require.Equal(t,
		lnwire.MilliSatoshi(updatedFeeBaseMSat).String(),
		route.HopFee(0).String(),
		"fee to forward to the private channel not matched",
	)
}

// TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit tests that upon receiving a
// ChannelUpdate in a fee related error from the private channel, we won't
// choose the route in our second attempt if the updated fee exceeds our fee
// limit specified in the payment.
//
// The test will send a payment from roasbeef to elst, available paths are,
// path1: roasbeef -> songoku -> sophon -> elst, total fee: 210k
// path2: roasbeef -> phamnuwen -> sophon -> elst, total fee: 220k
// path3: roasbeef -> songoku ->(private channel) elst
// We will setup the path3 to have the lowest fee and then update it with a fee
// exceeds our fee limit, thus this route won't be chosen.
func TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// Get the channel ID.
	roasbeefSongoku := lnwire.NewShortChanIDFromInt(
		ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
	)

	var (
		preImage         [32]byte
		amt              = lnwire.NewMSatFromSatoshis(1000)
		privateChannelID = uint64(55555)
		feeBaseMSat      = uint32(15)
		expiryDelta      = uint16(32)
		sgNode           = ctx.aliases["songoku"]
		feeLimit         = lnwire.MilliSatoshi(500000)
	)

	sgNodeID, err := btcec.ParsePubKey(sgNode[:])
	require.NoError(t, err)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to elst, through a private channel between songoku and elst for
	// 1000 satoshis. This route has lowest fees compared with the rest.
	payment := createDummyLightningPayment(
		t, ctx.aliases["elst"], amt,
	)
	payment.RouteHints = [][]zpay32.HopHint{{
		// Add a private channel between songoku and elst.
		zpay32.HopHint{
			NodeID:          sgNodeID,
			ChannelID:       privateChannelID,
			FeeBaseMSat:     feeBaseMSat,
			CLTVExpiryDelta: expiryDelta,
		},
	}}

	// Prepare an error update for the private channel. The updated fee
	// will exceeds the feeLimit.
	updatedFeeBaseMSat := feeBaseMSat + uint32(feeLimit)
	errChanUpdate := lnwire.ChannelUpdate1{
		ShortChannelID: lnwire.NewShortChanIDFromInt(privateChannelID),
		Timestamp:      uint32(testTime.Add(time.Minute).Unix()),
		BaseFee:        updatedFeeBaseMSat,
		TimeLockDelta:  expiryDelta,
	}
	signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)

	// We'll now modify the SendHTLC method to return an error for the
	// outgoing channel to songoku.
	errorReturned := false
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			if firstHop != roasbeefSongoku || errorReturned {
				return preImage, nil
			}

			errorReturned = true
			return [32]byte{}, htlcswitch.NewForwardingError(
				// Within our error, we'll add a
				// channel update which is meant to
				// reflect the new fee schedule for the
				// node/channel.
				&lnwire.FailFeeInsufficient{
					Update: errChanUpdate,
				}, 1,
			)
		},
	)

	// Send off the payment request to the router, route through son
	// goku and then across the private channel to elst.
	paymentPreImage, route, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	require.True(t, errorReturned,
		"failed to simulate error in the first payment attempt",
	)

	// The route selected should have three hops. Make sure that,
	//   path1: roasbeef -> son goku -> sophon -> elst
	//   path2: roasbeef -> pham nuwen -> sophon -> elst
	//   path3: roasbeef -> sophon -> (private channel) else
	// path1 is selected.
	require.Equal(t, 3, len(route.Hops), "incorrect route length")

	// The preimage should match up with the one created above.
	require.Equal(t,
		paymentPreImage[:], preImage[:], "incorrect preimage used",
	)

	// The route should have son goku as the first hop.
	require.Equal(t, route.Hops[0].PubKeyBytes, ctx.aliases["songoku"],
		"route should go through son goku as the first hop",
	)

	// The route should have sophon as the first hop.
	require.Equal(t, route.Hops[1].PubKeyBytes, ctx.aliases["sophon"],
		"route should go through sophon as the second hop",
	)
	// The route should pass via the public channel.
	require.Equal(t, route.FinalHop().PubKeyBytes, ctx.aliases["elst"],
		"route should go through elst as the final hop",
	)
}

// TestSendPaymentErrorNonFinalTimeLockErrors tests that if we receive either
// an ExpiryTooSoon or a IncorrectCltvExpiry error from a node, then we prune
// that node from the available graph within a mission control session. This
// test ensures that we'll route around errors due to nodes not knowing the
// current block height.
func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(
		t, startingBlockHeight, basicGraphFilePath,
	)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to sophon for 1k satoshis.
	amt := lnwire.NewMSatFromSatoshis(1000)
	payment := createDummyLightningPayment(
		t, ctx.aliases["sophon"], amt,
	)

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	// We'll also fetch the first outgoing channel edge from roasbeef to
	// son goku. This edge will be included in the time lock related expiry
	// errors that we'll get back due to disagrements in what the current
	// block height is.
	chanID := ctx.getChannelIDFromAlias(t, "roasbeef", "songoku")
	roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)

	_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID)
	require.NoError(t, err, "unable to fetch chan id")

	errChanUpdate := lnwire.ChannelUpdate1{
		ShortChannelID:  lnwire.NewShortChanIDFromInt(chanID),
		Timestamp:       uint32(edgeUpdateToFail.LastUpdate.Unix()),
		MessageFlags:    edgeUpdateToFail.MessageFlags,
		ChannelFlags:    edgeUpdateToFail.ChannelFlags,
		TimeLockDelta:   edgeUpdateToFail.TimeLockDelta,
		HtlcMinimumMsat: edgeUpdateToFail.MinHTLC,
		HtlcMaximumMsat: edgeUpdateToFail.MaxHTLC,
		BaseFee:         uint32(edgeUpdateToFail.FeeBaseMSat),
		FeeRate:         uint32(edgeUpdateToFail.FeeProportionalMillionths),
	}

	// We'll now modify the SendToSwitch method to return an error for the
	// outgoing channel to son goku. Since this is a time lock related
	// error, we should fail the payment flow all together, as Goku is the
	// only channel to Sophon.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			if firstHop == roasbeefSongoku {
				return [32]byte{}, htlcswitch.NewForwardingError(
					&lnwire.FailExpiryTooSoon{
						Update: errChanUpdate,
					}, 1,
				)
			}

			return preImage, nil
		})

	// assertExpectedPath is a helper function that asserts the returned
	// route properly routes around the failure we've introduced in the
	// graph.
	assertExpectedPath := func(retPreImage [32]byte, route *route.Route) {
		// The route selected should have two hops
		require.Equal(t, 2, len(route.Hops), "incorrect route length")

		// The preimage should match up with the once created above.
		require.Equal(t,
			preImage[:], retPreImage[:], "incorrect preimage used",
		)

		// The route should have satoshi as the first hop.
		require.Equalf(t,
			ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
			"route should go through phamnuwen as first hop, "+
				"instead passes through: %v",
			getAliasFromPubKey(
				route.Hops[0].PubKeyBytes, ctx.aliases,
			),
		)
	}

	// Send off the payment request to the router, this payment should
	// succeed as we should actually go through Pham Nuwen in order to get
	// to Sophon, even though he has higher fees.
	paymentPreImage, rt, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	assertExpectedPath(paymentPreImage, rt)

	// We'll now modify the error return an IncorrectCltvExpiry error
	// instead, this should result in the same behavior of roasbeef routing
	// around the faulty Son Goku node.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
			if firstHop == roasbeefSongoku {
				return [32]byte{}, htlcswitch.NewForwardingError(
					&lnwire.FailIncorrectCltvExpiry{
						Update: errChanUpdate,
					}, 1,
				)
			}

			return preImage, nil
		})

	// Once again, Roasbeef should route around Goku since they disagree
	// w.r.t to the block height, and instead go through Pham Nuwen. We
	// flip a bit in the payment hash to allow resending this payment.
	payment.paymentHash[1] ^= 1
	paymentPreImage, rt, err = ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	assertExpectedPath(paymentPreImage, rt)
}

// TestSendPaymentErrorPathPruning tests that the send of candidate routes
// properly gets pruned in response to ForwardingError response from the
// underlying SendToSwitch function.
func TestSendPaymentErrorPathPruning(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	// Craft a LightningPayment struct that'll send a payment from roasbeef
	// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
	paymentAmt := lnwire.NewMSatFromSatoshis(1000)
	payment := createDummyLightningPayment(
		t, ctx.aliases["sophon"], paymentAmt,
	)

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	roasbeefSongoku := lnwire.NewShortChanIDFromInt(
		ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
	)
	roasbeefPhanNuwen := lnwire.NewShortChanIDFromInt(
		ctx.getChannelIDFromAlias(t, "roasbeef", "phamnuwen"),
	)

	// First, we'll modify the SendToSwitch method to return an error
	// indicating that the channel from roasbeef to son goku is not operable
	// with an UnknownNextPeer.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {

			if firstHop == roasbeefSongoku {
				// We'll first simulate an error from the first
				// hop to simulate the channel from songoku to
				// sophon not having enough capacity.
				return [32]byte{}, htlcswitch.NewForwardingError(
					&lnwire.FailTemporaryChannelFailure{},
					1,
				)
			}

			// Next, we'll create an error from phan nuwen to
			// indicate that the sophon node is not longer online,
			// which should prune out the rest of the routes.
			if firstHop == roasbeefPhanNuwen {
				return [32]byte{}, htlcswitch.NewForwardingError(
					&lnwire.FailUnknownNextPeer{}, 1,
				)
			}

			return preImage, nil
		})

	require.IsType(t, ctx.router.cfg.MissionControl, &MissionControl{})
	mc, _ := ctx.router.cfg.MissionControl.(*MissionControl)

	err := mc.ResetHistory()
	require.NoError(t, err)

	// When we try to dispatch that payment, we should receive an error as
	// both attempts should fail and cause both routes to be pruned.
	_, _, err = ctx.router.SendPayment(payment)
	require.Error(t, err, "payment didn't return error")

	// The final error returned should also indicate that the peer wasn't
	// online (the last error we returned).
	require.Equal(t, channeldb.FailureReasonNoRoute, err)

	// Inspect the two attempts that were made before the payment failed.
	p, err := ctx.router.cfg.Control.FetchPayment(*payment.paymentHash)
	require.NoError(t, err)

	htlcs := p.GetHTLCs()
	require.Equal(t, 2, len(htlcs), "expected two attempts")

	// We expect the first attempt to have failed with a
	// TemporaryChannelFailure, the second with UnknownNextPeer.
	msg := htlcs[0].Failure.Message
	require.IsType(t, msg, &lnwire.FailTemporaryChannelFailure{})

	msg = htlcs[1].Failure.Message
	require.IsType(t, msg, &lnwire.FailUnknownNextPeer{})

	err = ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
	require.NoError(t, err, "reset history failed")

	// Next, we'll modify the SendToSwitch method to indicate that the
	// connection between songoku and isn't up.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {

			if firstHop == roasbeefSongoku {
				failure := htlcswitch.NewForwardingError(
					&lnwire.FailUnknownNextPeer{}, 1,
				)
				return [32]byte{}, failure
			}

			return preImage, nil
		})

	// This shouldn't return an error, as we'll make a payment attempt via
	// the pham nuwen channel based on the assumption that there might be an
	// intermittent issue with the songoku <-> sophon channel.
	paymentPreImage, rt, err := ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	// This path should go: roasbeef -> pham nuwen -> sophon
	require.Equal(t, 2, len(rt.Hops), "incorrect route length")
	require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
	require.Equalf(t,
		ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
		"route should go through phamnuwen as first hop, "+
			"instead passes through: %v",
		getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
	)

	require.IsType(t, ctx.router.cfg.MissionControl, &MissionControl{})
	mc, _ = ctx.router.cfg.MissionControl.(*MissionControl)

	err = mc.ResetHistory()
	require.NoError(t, err)

	// Finally, we'll modify the SendToSwitch function to indicate that the
	// roasbeef -> luoji channel has insufficient capacity. This should
	// again cause us to instead go via the satoshi route.
	ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
		func(firstHop lnwire.ShortChannelID) ([32]byte, error) {

			if firstHop == roasbeefSongoku {
				// We'll first simulate an error from the first
				// outgoing link to simulate the channel from luo ji to
				// roasbeef not having enough capacity.
				return [32]byte{}, htlcswitch.NewForwardingError(
					&lnwire.FailTemporaryChannelFailure{},
					1,
				)
			}
			return preImage, nil
		})

	// We flip a bit in the payment hash to allow resending this payment.
	payment.paymentHash[1] ^= 1
	paymentPreImage, rt, err = ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	// This should succeed finally.  The route selected should have two
	// hops.
	require.Equal(t, 2, len(rt.Hops), "incorrect route length")

	// The preimage should match up with the once created above.
	require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")

	// The route should have satoshi as the first hop.
	require.Equalf(t,
		ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
		"route should go through phamnuwen as first hop, "+
			"instead passes through: %v",
		getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
	)
}

// TestFindPathFeeWeighting tests that the findPath method will properly prefer
// routes with lower fees over routes with lower time lock values. This is
// meant to exercise the fact that the internal findPath method ranks edges
// with the square of the total fee in order bias towards lower fees.
func TestFindPathFeeWeighting(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	var preImage [32]byte
	copy(preImage[:], bytes.Repeat([]byte{9}, 32))

	sourceNode, err := ctx.graph.SourceNode()
	require.NoError(t, err, "unable to fetch source node")

	amt := lnwire.MilliSatoshi(100)

	target := ctx.aliases["luoji"]

	// We'll now attempt a path finding attempt using this set up. Due to
	// the edge weighting, we should select the direct path over the 2 hop
	// path even though the direct path has a higher potential time lock.
	path, err := dbFindPath(
		ctx.graph, nil, &mockBandwidthHints{},
		noRestrictions,
		testPathFindingConfig,
		sourceNode.PubKeyBytes, target, amt, 0, 0,
	)
	require.NoError(t, err, "unable to find path")

	// The route that was chosen should be exactly one hop, and should be
	// directly to luoji.
	require.Len(t, path, 1)
	require.Equal(t, ctx.aliases["luoji"], path[0].policy.ToNodePubKey())
}

// TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket
// function is able to gracefully handle being passed a nil set of hops for the
// route by the caller.
func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
	t.Parallel()

	sessionKey, _ := btcec.NewPrivateKey()
	emptyRoute := &route.Route{}
	_, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
	require.ErrorIs(t, err, route.ErrNoRouteHopsProvided)
}

// TestUnknownErrorSource tests that if the source of an error is unknown, all
// edges along the route will be pruned.
func TestUnknownErrorSource(t *testing.T) {
	t.Parallel()

	// Setup a network. It contains two paths to c: a->b->c and an
	// alternative a->d->c.
	chanCapSat := btcutil.Amount(100000)
	testChannels := []*testChannel{
		symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 400,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 1),
		symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 400,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 3),
		symmetricTestChannel("a", "d", chanCapSat, &testChannelPolicy{
			Expiry:      144,
			FeeRate:     400,
			FeeBaseMsat: 100000,
			MinHTLC:     1,
			MaxHTLC:     lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 2),
		symmetricTestChannel("d", "c", chanCapSat, &testChannelPolicy{
			Expiry:      144,
			FeeRate:     400,
			FeeBaseMsat: 100000,
			MinHTLC:     1,
			MaxHTLC:     lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 4),
	}

	testGraph, err := createTestGraphFromChannels(
		t, true, testChannels, "a",
	)
	require.NoError(t, err, "unable to create graph")

	const startingBlockHeight = 101
	ctx := createTestCtxFromGraphInstance(t, startingBlockHeight, testGraph)

	// Create a payment to node c.
	payment := createDummyLightningPayment(
		t, ctx.aliases["c"], lnwire.NewMSatFromSatoshis(1000),
	)

	// We'll modify the SendToSwitch method so that it simulates hop b as a
	// node that returns an unparsable failure if approached via the a->b
	// channel.
	dispatcher, ok := ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld) //nolint:lll
	require.True(t, ok)

	dispatcher.setPaymentResult(func(firstHop lnwire.ShortChannelID) (
		[32]byte, error) {

		// If channel a->b is used, return an error without
		// source and message. The sender won't know the origin
		// of the error.
		if firstHop.ToUint64() == 1 {
			return [32]byte{},
				htlcswitch.ErrUnreadableFailureMessage
		}

		// Otherwise the payment succeeds.
		return lntypes.Preimage{}, nil
	})

	// Send off the payment request to the router. The expectation is that
	// the route a->b->c is tried first. An unreadable faiure is returned
	// which should pruning the channel a->b. We expect the payment to
	// succeed via a->d.
	_, _, err = ctx.router.SendPayment(payment)
	require.NoErrorf(t, err, "unable to send payment: %v",
		payment.paymentHash)

	// Next we modify payment result to return an unknown failure.
	dispatcher, ok = ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld) //nolint:lll
	require.True(t, ok)

	dispatcher.setPaymentResult(func(firstHop lnwire.ShortChannelID) (
		[32]byte, error) {

		// If channel a->b is used, simulate that the failure
		// couldn't be decoded (FailureMessage is nil).
		if firstHop.ToUint64() == 2 {
			return [32]byte{},
				htlcswitch.NewUnknownForwardingError(1)
		}

		// Otherwise the payment succeeds.
		return lntypes.Preimage{}, nil
	})

	// Send off the payment request to the router. We expect the payment to
	// fail because both routes have been pruned.
	payment.paymentHash[1] ^= 1
	_, _, err = ctx.router.SendPayment(payment)
	if err == nil {
		t.Fatalf("expected payment to fail")
	}
}

// TestSendToRouteStructuredError asserts that SendToRoute returns a structured
// error.
func TestSendToRouteStructuredError(t *testing.T) {
	t.Parallel()

	// Setup a three node network.
	chanCapSat := btcutil.Amount(100000)
	testChannels := []*testChannel{
		symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 400,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 1),
		symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 400,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 2),
	}

	testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
	require.NoError(t, err, "unable to create graph")

	const startingBlockHeight = 101
	ctx := createTestCtxFromGraphInstance(t, startingBlockHeight, testGraph)

	// Set up an init channel for the control tower, such that we can make
	// sure the payment is initiated correctly.
	init := make(chan initArgs, 1)
	ctx.router.cfg.Control.(*mockControlTowerOld).init = init

	// Setup a route from source a to destination c. The route will be used
	// in a call to SendToRoute. SendToRoute also applies channel updates,
	// but it saves us from including RequestRoute in the test scope too.
	const payAmt = lnwire.MilliSatoshi(10000)
	hop1 := ctx.aliases["b"]
	hop2 := ctx.aliases["c"]
	hops := []*route.Hop{
		{
			ChannelID:     1,
			PubKeyBytes:   hop1,
			AmtToForward:  payAmt,
			LegacyPayload: true,
		},
		{
			ChannelID:     2,
			PubKeyBytes:   hop2,
			AmtToForward:  payAmt,
			LegacyPayload: true,
		},
	}

	rt, err := route.NewRouteFromHops(payAmt, 100, ctx.aliases["a"], hops)
	require.NoError(t, err, "unable to create route")

	finalHopIndex := len(hops)
	testCases := map[int]lnwire.FailureMessage{
		finalHopIndex: lnwire.NewFailIncorrectDetails(payAmt, 100),
		1: &lnwire.FailFeeInsufficient{
			Update: lnwire.ChannelUpdate1{},
		},
	}

	for failIndex, errorType := range testCases {
		failIndex := failIndex
		errorType := errorType

		t.Run(fmt.Sprintf("%T", errorType), func(t *testing.T) {
			// We'll modify the SendToSwitch method so that it
			// simulates a failed payment with an error originating
			// from the final hop in the route.
			ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
				func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
					return [32]byte{}, htlcswitch.NewForwardingError(
						errorType, failIndex,
					)
				},
			)

			// The payment parameter is mostly redundant in
			// SendToRoute.  Can be left empty for this test.
			var payment lntypes.Hash

			// Send off the payment request to the router. The
			// specified route should be attempted and the channel
			// update should be received by router and ignored
			// because it is missing a valid
			// signature.
			_, err = ctx.router.SendToRoute(payment, rt, nil)

			fErr, ok := err.(*htlcswitch.ForwardingError)
			require.True(
				t, ok, "expected forwarding error, got: %T", err,
			)

			require.IsType(
				t, errorType, fErr.WireMessage(),
				"expected type %T got %T", errorType,
				fErr.WireMessage(),
			)

			// Check that the correct values were used when
			// initiating the payment.
			select {
			case initVal := <-init:
				if initVal.c.Value != payAmt {
					t.Fatalf("expected %v, got %v", payAmt,
						initVal.c.Value)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatalf("initPayment not called")
			}
		})
	}
}

// TestSendToRouteMaxHops asserts that SendToRoute fails when using a route that
// exceeds the maximum number of hops.
func TestSendToRouteMaxHops(t *testing.T) {
	t.Parallel()

	// Setup a two node network.
	chanCapSat := btcutil.Amount(100000)
	testChannels := []*testChannel{
		symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 400,
			MinHTLC: 1,
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 1),
	}

	testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
	require.NoError(t, err, "unable to create graph")

	const startingBlockHeight = 101

	ctx := createTestCtxFromGraphInstance(t, startingBlockHeight, testGraph)

	// Create a 30 hop route that exceeds the maximum hop limit.
	const payAmt = lnwire.MilliSatoshi(10000)
	hopA := ctx.aliases["a"]
	hopB := ctx.aliases["b"]

	var hops []*route.Hop
	for i := 0; i < 15; i++ {
		hops = append(hops, &route.Hop{
			ChannelID:     1,
			PubKeyBytes:   hopB,
			AmtToForward:  payAmt,
			LegacyPayload: true,
		})

		hops = append(hops, &route.Hop{
			ChannelID:     1,
			PubKeyBytes:   hopA,
			AmtToForward:  payAmt,
			LegacyPayload: true,
		})
	}

	rt, err := route.NewRouteFromHops(payAmt, 100, ctx.aliases["a"], hops)
	require.NoError(t, err, "unable to create route")

	// Send off the payment request to the router. We expect an error back
	// indicating that the route is too long.
	var payHash lntypes.Hash
	_, err = ctx.router.SendToRoute(payHash, rt, nil)
	if err != route.ErrMaxRouteHopsExceeded {
		t.Fatalf("expected ErrMaxRouteHopsExceeded, but got %v", err)
	}
}

// TestBuildRoute tests whether correct routes are built.
func TestBuildRoute(t *testing.T) {
	// Setup a three node network.
	chanCapSat := btcutil.Amount(100000)
	paymentAddrFeatures := lnwire.NewFeatureVector(
		lnwire.NewRawFeatureVector(lnwire.PaymentAddrOptional),
		lnwire.Features,
	)
	testChannels := []*testChannel{
		// Create two local channels from a. The bandwidth is estimated
		// in this test as the channel capacity. For building routes, we
		// expected the channel with the largest estimated bandwidth to
		// be selected.
		symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 20000,
			MinHTLC: lnwire.NewMSatFromSatoshis(5),
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
		}, 1),
		symmetricTestChannel("a", "b", chanCapSat/2, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 20000,
			MinHTLC: lnwire.NewMSatFromSatoshis(5),
			MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat / 2),
		}, 6),

		// Create two channels from b to c. For building routes, we
		// expect the highest cost channel to be selected. Note that
		// this isn't a situation that we are expecting in reality.
		// Routing nodes are recommended to keep their channel policies
		// towards the same peer identical.
		symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  50000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(120),
			Features: paymentAddrFeatures,
		}, 2),
		symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  60000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(120),
			Features: paymentAddrFeatures,
		}, 7),

		// Create some channels that have conflicting min/max
		// constraints.
		symmetricTestChannel("a", "e", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  80000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(5),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(10),
			Features: paymentAddrFeatures,
		}, 5),
		symmetricTestChannel("e", "c", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  100000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(chanCapSat),
			Features: paymentAddrFeatures,
		}, 4),

		// Create some channels that have a conflicting max HTLC
		// constraint for one node pair, similar to the b->c channels.
		symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  50000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(25),
			Features: paymentAddrFeatures,
		}, 3),
		symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  60000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.MilliSatoshi(20100),
			Features: paymentAddrFeatures,
		}, 8),

		// Create a route with inbound fees.
		symmetricTestChannel("a", "d", chanCapSat, &testChannelPolicy{
			Expiry:  144,
			FeeRate: 20000,
			MinHTLC: lnwire.NewMSatFromSatoshis(5),
			MaxHTLC: lnwire.NewMSatFromSatoshis(
				chanCapSat,
			),
			InboundFeeBaseMsat: -1000,
			InboundFeeRate:     -1000,
		}, 9),
		symmetricTestChannel("d", "f", chanCapSat, &testChannelPolicy{
			Expiry:   144,
			FeeRate:  60000,
			MinHTLC:  lnwire.NewMSatFromSatoshis(20),
			MaxHTLC:  lnwire.NewMSatFromSatoshis(120),
			Features: paymentAddrFeatures,
			// The inbound fee will not be active for the last hop.
			InboundFeeBaseMsat: 2000,
			InboundFeeRate:     2000,
		}, 10),
	}

	testGraph, err := createTestGraphFromChannels(
		t, true, testChannels, "a",
	)
	require.NoError(t, err, "unable to create graph")

	const startingBlockHeight = 101

	ctx := createTestCtxFromGraphInstance(t, startingBlockHeight, testGraph)

	checkHops := func(rt *route.Route, expected []uint64,
		payAddr [32]byte) {

		t.Helper()

		require.Len(t, rt.Hops, len(expected))

		for i, hop := range rt.Hops {
			require.Equal(t, expected[i], hop.ChannelID)
		}

		lastHop := rt.Hops[len(rt.Hops)-1]
		require.NotNil(t, lastHop.MPP)
		require.Equal(t, lastHop.MPP.PaymentAddr(), payAddr)
	}

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

	noAmt := fn.None[lnwire.MilliSatoshi]()

	// Test that we can't build a route when no hops are given.
	hops = []route.Vertex{}
	_, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.None[[32]byte](), fn.None[[]byte](),
	)
	require.Error(t, err)

	// Create hop list for an unknown destination.
	hops := []route.Vertex{ctx.aliases["b"], ctx.aliases["y"]}
	_, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](),
	)
	noChanErr := ErrNoChannel{}
	require.ErrorAs(t, err, &noChanErr)
	require.Equal(t, 1, noChanErr.position)

	// Create hop list from the route node pubkeys.
	hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["c"]}
	amt := lnwire.NewMSatFromSatoshis(100)

	// Build the route for the given amount.
	rt, err := ctx.router.BuildRoute(
		fn.Some(amt), hops, nil, 40, fn.Some(payAddr),
		fn.None[[]byte](),
	)
	require.NoError(t, err)

	// Check that we get the expected route back. The total amount should be
	// the amount to deliver to hop c (100 sats) plus the max fee for the
	// connection b->c (6 sats).
	checkHops(rt, []uint64{1, 7}, payAddr)
	require.Equal(t, lnwire.MilliSatoshi(106000), rt.TotalAmount)

	// Build the route for the minimum amount.
	rt, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](),
	)
	require.NoError(t, err)

	// Check that we get the expected route back. The minimum that we can
	// send from b to c is 20 sats. Hop b charges 1200 msat for the
	// forwarding. The channel between hop a and b can carry amounts in the
	// range [5, 100], so 21200 msats is the minimum amount for this route.
	checkHops(rt, []uint64{1, 7}, payAddr)
	require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)

	// The receiver gets sent the minimal HTLC amount.
	require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)

	// Test a route that contains incompatible channel htlc constraints.
	// There is no amount that can pass through both channel 5 and 4.
	hops = []route.Vertex{ctx.aliases["e"], ctx.aliases["c"]}
	_, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.None[[32]byte](), fn.None[[]byte](),
	)
	require.Error(t, err)
	noChanErr = ErrNoChannel{}
	require.ErrorAs(t, err, &noChanErr)
	require.Equal(t, 0, noChanErr.position)

	// Test a route that contains channel constraints that lead to a
	// different selection of a unified edge, when the amount is rescaled
	// for the final edge. From a backward pass we expect the policy of
	// channel 8 to be used, because its policy has the highest fee rate,
	// bumping the amount to 20000 msat leading to a sender amount of 21200
	// msat including the fees for hop over channel 8. In the forward pass
	// however, we subtract that fee again, resulting in the min HTLC
	// amount. The forward pass doesn't check for a different policy that
	// could me more applicable, which is why we don't get back the highest
	// amount that could be delivered to the receiver of 21819 msat, using
	// policy of channel 3.
	hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]}
	rt, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](),
	)
	require.NoError(t, err)
	checkHops(rt, []uint64{1, 8}, payAddr)
	require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
	require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)

	// Check that we compute a correct forwarding amount that involves
	// inbound fees. We expect a similar amount as for the above case of
	// b->c, but reduced by the inbound discount on the channel a->d.
	// We get 106000 - 1000 (base in) - 0.001 * 106000 (rate in) = 104894.
	hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
	amt = lnwire.NewMSatFromSatoshis(100)
	rt, err = ctx.router.BuildRoute(
		fn.Some(amt), hops, nil, 40, fn.Some(payAddr),
		fn.None[[]byte](),
	)
	require.NoError(t, err)
	checkHops(rt, []uint64{9, 10}, payAddr)
	require.EqualValues(t, 104894, rt.TotalAmount)

	// Also check the min amount with inbound fees. The min amount bumps
	// this to 20000 msat for the last hop. The outbound fee is 1200 msat,
	// the inbound fee is -1021.2 msat (rounded down). This results in a
	// total fee of 179 msat, giving a sender amount of 20179 msat. The
	// determined receiver amount however reduces this to 20001 msat again
	// due to rounding. This would not be compatible with the sender amount
	// of 20179 msat, which results in underpayment of 1 msat in fee. There
	// is a third pass through newRoute in which this gets corrected to end
	hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
	rt, err = ctx.router.BuildRoute(
		noAmt, hops, nil, 40, fn.Some(payAddr), fn.None[[]byte](),
	)
	require.NoError(t, err)
	checkHops(rt, []uint64{9, 10}, payAddr)
	require.EqualValues(t, 20180, rt.TotalAmount, "%v", rt.TotalAmount)
}

// TestReceiverAmtForwardPass tests that the forward pass returns the expected
// receiver amount when given a set of edges and does not panic.
func TestReceiverAmtForwardPass(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name         string
		amt          lnwire.MilliSatoshi
		unifiedEdges []*unifiedEdge
		hops         []route.Vertex

		expectedAmt lnwire.MilliSatoshi
		expectedErr string
	}{
		{
			name:        "empty",
			expectedErr: "no edges to forward through",
		},
		{
			name: "single edge, no valid policy",
			amt:  1000,
			unifiedEdges: []*unifiedEdge{
				{
					policy: &models.CachedEdgePolicy{
						MinHTLC: 1001,
					},
				},
			},
			expectedErr: fmt.Sprintf("no matching outgoing " +
				"channel available for node index 0"),
		},
		{
			name: "single edge",
			amt:  1000,
			unifiedEdges: []*unifiedEdge{
				{
					policy: &models.CachedEdgePolicy{
						MinHTLC: 1000,
					},
				},
			},
			expectedAmt: 1000,
		},
		{
			name: "outbound fee, no rounding",
			amt:  1e9,
			unifiedEdges: []*unifiedEdge{
				{
					// The first hop's outbound fee is
					// irrelevant in fee calculation.
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat:               1234,
						FeeProportionalMillionths: 1234,
					},
				},
				{
					// No rounding is done here.
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat:               1000,
						FeeProportionalMillionths: 1000,
					},
				},
			},
			// From an outgoing amount of 999000000 msat, we get
			// in = out + base + out * rate = 1000000000.0
			//
			// The inverse outgoing amount for this is
			// out = (in - base) / (1 + rate) =
			// (1e9 - 1000) / (1 + 1e-3) = 999000000.0000001,
			// which is rounded down.
			expectedAmt: 999000000,
		},
		{
			name: "outbound fee, rounding",
			amt:  1e9,
			unifiedEdges: []*unifiedEdge{
				{
					// The first hop's outbound fee is
					// irrelevant in fee calculation.
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat:               1234,
						FeeProportionalMillionths: 1234,
					},
				},
				{
					// This policy is chosen such that we
					// round down.
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat:               1000,
						FeeProportionalMillionths: 999,
					},
				},
			},
			// The float amount for this is
			// out = (in - base) / (1 + rate) =
			// (1e9 - 1000) / (1 + 999e-6) = 999000998.002995,
			// which is rounded up.
			expectedAmt: 999000999,
		},
	}

	for _, tc := range testCases {
		amt, err := receiverAmtForwardPass(tc.amt, tc.unifiedEdges)

		if tc.expectedErr != "" {
			require.Error(t, err)
			require.ErrorContains(t, err, tc.expectedErr)

			continue
		}

		require.NoError(t, err)
		require.Equal(t, amt, tc.expectedAmt)
	}
}

// TestSenderAmtBackwardPass tests that the computation of the sender amount is
// done correctly for route building.
func TestSenderAmtBackwardPass(t *testing.T) {
	bandwidthHints := bandwidthManager{
		getLink: func(chanId lnwire.ShortChannelID) (
			htlcswitch.ChannelLink, error) {

			return nil, nil
		},
		localChans: make(map[lnwire.ShortChannelID]struct{}),
	}

	var (
		capacity        btcutil.Amount      = 1_000_000
		testReceiverAmt lnwire.MilliSatoshi = 1_000_000
		minHTLC         lnwire.MilliSatoshi = 1_000
	)

	edgeUnifiers := []*edgeUnifier{
		{
			edges: []*unifiedEdge{
				{
					// This outbound fee doesn't have an
					// effect (sender doesn't pay outbound).
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat: 112,
					},
					inboundFees: models.InboundFee{
						Base: 111,
					},
					capacity: capacity,
				},
			},
		},
		{
			edges: []*unifiedEdge{
				{
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat: 222,
					},
					inboundFees: models.InboundFee{
						Base: 222,
					},
					capacity: capacity,
				},
			},
		},
		{
			edges: []*unifiedEdge{
				{
					policy: &models.CachedEdgePolicy{
						FeeBaseMSat: 333,
						MinHTLC:     minHTLC,
					},
					// In pathfinding, inbound fees are not
					// populated for exit hops because the
					// newNodeEdgeUnifier enforces this.
					// This is important as otherwise we
					// would not fail the min HTLC check in
					// getEdge.
					capacity: capacity,
				},
			},
		},
	}

	// A search for an amount that is below the minimum HTLC amount should
	// fail.
	_, _, err := senderAmtBackwardPass(
		edgeUnifiers, fn.Some(minHTLC-1), &bandwidthHints,
	)
	require.Error(t, err)

	// Do a min amount search.
	_, senderAmount, err := senderAmtBackwardPass(
		edgeUnifiers, fn.None[lnwire.MilliSatoshi](), &bandwidthHints,
	)
	require.NoError(t, err)
	require.Equal(t, minHTLC+333+222+222+111, senderAmount)

	// Do a search for a specific amount.
	unifiedEdges, senderAmount, err := senderAmtBackwardPass(
		edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
	)
	require.NoError(t, err)
	require.Equal(t, testReceiverAmt+333+222+222+111, senderAmount)

	// Check that we arrive at the same receiver amount by doing a forward
	// pass.
	receiverAmt, err := receiverAmtForwardPass(senderAmount, unifiedEdges)
	require.NoError(t, err)
	require.Equal(t, testReceiverAmt, receiverAmt)

	// Insert a policy that leads to rounding.
	edgeUnifiers[1] = &edgeUnifier{
		edges: []*unifiedEdge{
			{
				policy: &models.CachedEdgePolicy{
					FeeBaseMSat:               20,
					FeeProportionalMillionths: 100,
				},
				inboundFees: models.InboundFee{
					Base: -10,
					Rate: -50,
				},
				capacity: capacity,
			},
		},
	}

	unifiedEdges, senderAmount, err = senderAmtBackwardPass(
		edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
	)
	require.NoError(t, err)

	// For this route, we have some rounding errors, so we can't expect the
	// exact amount, but it should be higher than the exact amount, to not
	// end up below a min HTLC constraint.
	receiverAmt, err = receiverAmtForwardPass(senderAmount, unifiedEdges)
	require.NoError(t, err)
	require.NotEqual(t, testReceiverAmt, receiverAmt)
	require.InDelta(t, int64(testReceiverAmt), int64(receiverAmt), 1)
	require.GreaterOrEqual(t, int64(receiverAmt), int64(testReceiverAmt))
}

// TestInboundOutbound tests the functions that computes the incoming and
// outgoing amounts based on the fees of the incoming and outgoing channels.
func TestInboundOutbound(t *testing.T) {
	var outgoingAmt uint64 = 10_000_000

	tests := []struct {
		name         string
		incomingBase int32
		incomingRate int32
		outgoingBase uint64
		outgoingRate uint64
	}{
		{
			name:         "only outbound fee",
			incomingBase: 0,
			incomingRate: 0,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name:         "positive inbound and outbound fee",
			incomingBase: 20,
			incomingRate: 100,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name:         "small negative inbound and outbound fee",
			incomingBase: -10,
			incomingRate: -50,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name:         "equal negative inbound and outbound fee",
			incomingBase: -20,
			incomingRate: -100,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name:         "large negative inbound and outbound fee",
			incomingBase: -30,
			incomingRate: -200,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name: "order of PPM negative inbound and " +
				"outbound fee (m=0)",
			incomingBase: -30,
			incomingRate: -1_000_000,
			outgoingBase: 20,
			outgoingRate: 100,
		},
		{
			name: "huge negative inbound and " +
				"outbound fee (m<0)",
			incomingBase: -30,
			incomingRate: -2_000_000,
			outgoingBase: 20,
			outgoingRate: 100,
		},
	}

	for _, tc := range tests {
		tc := tc

		t.Run(tc.name, func(tt *testing.T) {
			testInboundOutboundFee(
				tt, outgoingAmt, tc.incomingBase,
				tc.incomingRate, tc.outgoingBase,
				tc.outgoingRate,
			)
		})
	}
}

// testInboundOutboundFee is a helper function that tests the outgoing and
// incoming amount relationship.
func testInboundOutboundFee(t *testing.T, outgoingAmt uint64, inBase,
	inRate int32, outBase, outRate uint64) {

	debugStr := fmt.Sprintf(
		"outAmt=%d, inBase=%d, inRate=%d, outBase=%d, outRate=%d",
		outgoingAmt, inBase, inRate, outBase, outRate,
	)

	incomingEdge := &unifiedEdge{
		policy: &models.CachedEdgePolicy{},
		inboundFees: models.InboundFee{
			Base: inBase,
			Rate: inRate,
		},
	}

	outgoingEdge := &unifiedEdge{
		policy: &models.CachedEdgePolicy{
			FeeBaseMSat: lnwire.MilliSatoshi(
				outBase,
			),
			FeeProportionalMillionths: lnwire.MilliSatoshi(
				outRate,
			),
		},
	}

	// We compute the incoming amount based on the outgoing amount, which
	// mimicks the path finding process.
	incomingAmt := incomingFromOutgoing(
		lnwire.MilliSatoshi(outgoingAmt), incomingEdge,
		outgoingEdge,
	)

	// We do the reverse and compute the outgoing amount based on the
	// incoming amount.
	outgoingAmtNew := outgoingFromIncoming(
		incomingAmt, incomingEdge, outgoingEdge,
	)

	// We require that the incoming amount is always larger than or equal to
	// the outgoing amount, because total fees (=incoming-outgoing) should
	// not become negative.
	require.GreaterOrEqual(
		t, int64(incomingAmt), int64(outgoingAmtNew), debugStr,
		"expected incomingAmt >= outgoingAmtNew",
	)

	// We check that up to rounding the amounts are equal.
	require.InDelta(
		t, int64(outgoingAmt), int64(outgoingAmtNew), 1.0, debugStr,
		"expected |outgoingAmt - outgoingAmtNew | <= 1",
	)

	// If we round, the computed outgoing amount should be larger than the
	// exact outgoing amount, to not hit any min HTLC limits.
	require.GreaterOrEqual(
		t, int64(outgoingAmtNew), int64(outgoingAmt), debugStr,
		"expected outgoingAmtNew >= outgoingAmt",
	)
}

// FuzzInboundOutbound tests the incoming and outgoing amount calculation
// functions with fuzzing.
func FuzzInboundOutboundFee(f *testing.F) {
	f.Add(uint64(0), int32(0), int32(0), uint64(0), uint64(0))

	f.Fuzz(testInboundOutboundFee)
}

// TestSendToRouteSkipTempErrSuccess validates a successful payment send.
func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
	t.Parallel()

	var (
		payHash lntypes.Hash
		payAmt  = lnwire.MilliSatoshi(10000)
	)

	preimage := lntypes.Preimage{1}
	testAttempt := makeSettledAttempt(t, int(payAmt), preimage)

	node, err := createTestNode()
	require.NoError(t, err)

	// Create a simple 1-hop route.
	hops := []*route.Hop{
		{
			ChannelID:        1,
			PubKeyBytes:      node.PubKeyBytes,
			AmtToForward:     payAmt,
			OutgoingTimeLock: 120,
			MPP:              record.NewMPP(payAmt, [32]byte{}),
		},
	}
	rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
	require.NoError(t, err)

	// Create mockers.
	controlTower := &mockControlTower{}
	payer := &mockPaymentAttemptDispatcher{}
	missionControl := &mockMissionControl{}

	// Create the router.
	router := &ChannelRouter{cfg: &Config{
		Control:        controlTower,
		Payer:          payer,
		MissionControl: missionControl,
		Clock:          clock.NewTestClock(time.Unix(1, 0)),
		NextPaymentID: func() (uint64, error) {
			return 0, nil
		},
		ClosedSCIDs:   mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
	}}

	// Register mockers with the expected method calls.
	controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
	controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
	controlTower.On("SettleAttempt",
		payHash, mock.Anything, mock.Anything,
	).Return(testAttempt, nil)

	payer.On("SendHTLC",
		mock.Anything, mock.Anything, mock.Anything,
	).Return(nil)

	// Create a buffered chan and it will be returned by GetAttemptResult.
	resultChan := make(chan *htlcswitch.PaymentResult, 1)
	payer.On("GetAttemptResult",
		mock.Anything, mock.Anything, mock.Anything,
	).Return(resultChan, nil).Run(func(_ mock.Arguments) {
		// Send a successful payment result.
		resultChan <- &htlcswitch.PaymentResult{}
	})

	missionControl.On("ReportPaymentSuccess",
		mock.Anything, rt,
	).Return(nil)

	// Mock the control tower to return the mocked payment.
	payment := &mockMPPayment{}
	controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()

	// Mock the payment to return nil failure reason.
	payment.On("TerminalInfo").Return(nil, nil)

	// Expect a successful send to route.
	attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil)
	require.NoError(t, err)
	require.Equal(t, testAttempt, attempt)

	// Assert the above methods are called as expected.
	controlTower.AssertExpectations(t)
	payer.AssertExpectations(t)
	missionControl.AssertExpectations(t)
	payment.AssertExpectations(t)
}

// TestSendToRouteSkipTempErrNonMPP checks that an error is return when
// skipping temp error for non-MPP.
func TestSendToRouteSkipTempErrNonMPP(t *testing.T) {
	t.Parallel()

	var (
		payHash lntypes.Hash
		payAmt  = lnwire.MilliSatoshi(10000)
	)

	node, err := createTestNode()
	require.NoError(t, err)

	// Create a simple 1-hop route without the MPP field.
	hops := []*route.Hop{
		{
			ChannelID:    1,
			PubKeyBytes:  node.PubKeyBytes,
			AmtToForward: payAmt,
		},
	}
	rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
	require.NoError(t, err)

	// Create mockers.
	controlTower := &mockControlTower{}
	payer := &mockPaymentAttemptDispatcher{}
	missionControl := &mockMissionControl{}

	// Create the router.
	router := &ChannelRouter{cfg: &Config{
		Control:        controlTower,
		Payer:          payer,
		MissionControl: missionControl,
		Clock:          clock.NewTestClock(time.Unix(1, 0)),
		NextPaymentID: func() (uint64, error) {
			return 0, nil
		},
		ClosedSCIDs:   mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
	}}

	// Expect an error to be returned.
	attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil)
	require.ErrorIs(t, ErrSkipTempErr, err)
	require.Nil(t, attempt)

	// Assert the above methods are not called.
	controlTower.AssertExpectations(t)
	payer.AssertExpectations(t)
	missionControl.AssertExpectations(t)
}

// TestSendToRouteSkipTempErrTempFailure validates a temporary failure won't
// cause the payment to be failed.
func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
	t.Parallel()

	var (
		payHash lntypes.Hash
		payAmt  = lnwire.MilliSatoshi(10000)
	)

	testAttempt := makeFailedAttempt(t, int(payAmt))
	node, err := createTestNode()
	require.NoError(t, err)

	// Create a simple 1-hop route.
	hops := []*route.Hop{
		{
			ChannelID:        1,
			PubKeyBytes:      node.PubKeyBytes,
			AmtToForward:     payAmt,
			OutgoingTimeLock: 120,
			MPP:              record.NewMPP(payAmt, [32]byte{}),
		},
	}
	rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
	require.NoError(t, err)

	// Create mockers.
	controlTower := &mockControlTower{}
	payer := &mockPaymentAttemptDispatcher{}
	missionControl := &mockMissionControl{}

	// Create the router.
	router := &ChannelRouter{cfg: &Config{
		Control:        controlTower,
		Payer:          payer,
		MissionControl: missionControl,
		Clock:          clock.NewTestClock(time.Unix(1, 0)),
		NextPaymentID: func() (uint64, error) {
			return 0, nil
		},
		ClosedSCIDs:   mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
	}}

	// Create the error to be returned.
	tempErr := htlcswitch.NewForwardingError(
		&lnwire.FailTemporaryChannelFailure{}, 1,
	)

	// Register mockers with the expected method calls.
	controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
	controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
	controlTower.On("FailAttempt",
		payHash, mock.Anything, mock.Anything,
	).Return(testAttempt, nil)

	payer.On("SendHTLC",
		mock.Anything, mock.Anything, mock.Anything,
	).Return(tempErr)

	// Mock the control tower to return the mocked payment.
	payment := &mockMPPayment{}
	controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()

	// Mock the mission control to return a nil reason from reporting the
	// attempt failure.
	missionControl.On("ReportPaymentFail",
		mock.Anything, rt, mock.Anything, mock.Anything,
	).Return(nil, nil)

	// Mock the payment to return nil failure reason.
	payment.On("TerminalInfo").Return(nil, nil)

	// Expect a failed send to route.
	attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil)
	require.Equal(t, tempErr, err)
	require.Equal(t, testAttempt, attempt)

	// Assert the above methods are called as expected.
	controlTower.AssertExpectations(t)
	payer.AssertExpectations(t)
	missionControl.AssertExpectations(t)
	payment.AssertExpectations(t)
}

// TestSendToRouteSkipTempErrPermanentFailure validates a permanent failure
// will fail the payment.
func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
	var (
		payHash lntypes.Hash
		payAmt  = lnwire.MilliSatoshi(10000)
	)

	testAttempt := makeFailedAttempt(t, int(payAmt))
	node, err := createTestNode()
	require.NoError(t, err)

	// Create a simple 1-hop route.
	hops := []*route.Hop{
		{
			ChannelID:        1,
			PubKeyBytes:      node.PubKeyBytes,
			AmtToForward:     payAmt,
			OutgoingTimeLock: 120,
			MPP:              record.NewMPP(payAmt, [32]byte{}),
		},
	}
	rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
	require.NoError(t, err)

	// Create mockers.
	controlTower := &mockControlTower{}
	payer := &mockPaymentAttemptDispatcher{}
	missionControl := &mockMissionControl{}

	// Create the router.
	router := &ChannelRouter{cfg: &Config{
		Control:        controlTower,
		Payer:          payer,
		MissionControl: missionControl,
		Clock:          clock.NewTestClock(time.Unix(1, 0)),
		NextPaymentID: func() (uint64, error) {
			return 0, nil
		},
		ClosedSCIDs:   mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
	}}

	// Create the error to be returned.
	permErr := htlcswitch.NewForwardingError(
		&lnwire.FailIncorrectDetails{}, 1,
	)

	// Register mockers with the expected method calls.
	controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
	controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)

	controlTower.On("FailAttempt",
		payHash, mock.Anything, mock.Anything,
	).Return(testAttempt, nil)

	// Expect the payment to be failed.
	controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)

	// Mock an error to be returned from sending the htlc.
	payer.On("SendHTLC",
		mock.Anything, mock.Anything, mock.Anything,
	).Return(permErr)

	failureReason := channeldb.FailureReasonPaymentDetails
	missionControl.On("ReportPaymentFail",
		mock.Anything, rt, mock.Anything, mock.Anything,
	).Return(&failureReason, nil)

	// Mock the control tower to return the mocked payment.
	payment := &mockMPPayment{}
	controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()

	// Mock the payment to return a failure reason.
	payment.On("TerminalInfo").Return(nil, &failureReason)

	// Expect a failed send to route.
	attempt, err := router.SendToRouteSkipTempErr(payHash, rt, nil)
	require.Equal(t, permErr, err)
	require.Equal(t, testAttempt, attempt)

	// Assert the above methods are called as expected.
	controlTower.AssertExpectations(t)
	payer.AssertExpectations(t)
	missionControl.AssertExpectations(t)
	payment.AssertExpectations(t)
}

// TestSendToRouteTempFailure validates a temporary failure will cause the
// payment to be failed.
func TestSendToRouteTempFailure(t *testing.T) {
	var (
		payHash lntypes.Hash
		payAmt  = lnwire.MilliSatoshi(10000)
	)

	testAttempt := makeFailedAttempt(t, int(payAmt))
	node, err := createTestNode()
	require.NoError(t, err)

	// Create a simple 1-hop route.
	hops := []*route.Hop{
		{
			ChannelID:        1,
			PubKeyBytes:      node.PubKeyBytes,
			AmtToForward:     payAmt,
			OutgoingTimeLock: 120,
			MPP:              record.NewMPP(payAmt, [32]byte{}),
		},
	}
	rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
	require.NoError(t, err)

	// Create mockers.
	controlTower := &mockControlTower{}
	payer := &mockPaymentAttemptDispatcher{}
	missionControl := &mockMissionControl{}

	// Create the router.
	router := &ChannelRouter{cfg: &Config{
		Control:        controlTower,
		Payer:          payer,
		MissionControl: missionControl,
		Clock:          clock.NewTestClock(time.Unix(1, 0)),
		NextPaymentID: func() (uint64, error) {
			return 0, nil
		},
		ClosedSCIDs:   mockClosedSCIDs,
		TrafficShaper: fn.Some[TlvTrafficShaper](&mockTrafficShaper{}),
	}}

	// Create the error to be returned.
	tempErr := htlcswitch.NewForwardingError(
		&lnwire.FailTemporaryChannelFailure{}, 1,
	)

	// Register mockers with the expected method calls.
	controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
	controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
	controlTower.On("FailAttempt",
		payHash, mock.Anything, mock.Anything,
	).Return(testAttempt, nil)

	// Expect the payment to be failed.
	controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)

	payer.On("SendHTLC",
		mock.Anything, mock.Anything, mock.Anything,
	).Return(tempErr)

	// Mock the control tower to return the mocked payment.
	payment := &mockMPPayment{}
	controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()

	// Mock the payment to return nil failure reason.
	payment.On("TerminalInfo").Return(nil, nil)

	// Return a nil reason to mock a temporary failure.
	missionControl.On("ReportPaymentFail",
		mock.Anything, rt, mock.Anything, mock.Anything,
	).Return(nil, nil)

	// Expect a failed send to route.
	attempt, err := router.SendToRoute(payHash, rt, nil)
	require.Equal(t, tempErr, err)
	require.Equal(t, testAttempt, attempt)

	// Assert the above methods are called as expected.
	controlTower.AssertExpectations(t)
	payer.AssertExpectations(t)
	missionControl.AssertExpectations(t)
	payment.AssertExpectations(t)
}

// TestNewRouteRequest tests creation of route requests for blinded and
// unblinded routes.
func TestNewRouteRequest(t *testing.T) {
	t.Parallel()

	//nolint:lll
	source, err := route.NewVertexFromStr("0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6")
	require.NoError(t, err)
	sourcePubkey, err := btcec.ParsePubKey(source[:])
	require.NoError(t, err)

	//nolint:lll
	v1, err := route.NewVertexFromStr("026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318")
	require.NoError(t, err)
	pubkey1, err := btcec.ParsePubKey(v1[:])
	require.NoError(t, err)

	//nolint:lll
	v2, err := route.NewVertexFromStr("03c19f0027ffbb0ae0e14a4d958788793f9d74e107462473ec0c3891e4feb12e99")
	require.NoError(t, err)
	pubkey2, err := btcec.ParsePubKey(v2[:])
	require.NoError(t, err)

	var (
		unblindedCltv uint16 = 500
		blindedCltv   uint16 = 1000
	)

	blindedSelfIntro := &BlindedPayment{
		CltvExpiryDelta: blindedCltv,
		BlindedPath: &sphinx.BlindedPath{
			IntroductionPoint: sourcePubkey,
			BlindedHops:       []*sphinx.BlindedHopInfo{{}},
		},
	}

	blindedOtherIntro := &BlindedPayment{
		CltvExpiryDelta: blindedCltv,
		BlindedPath: &sphinx.BlindedPath{
			IntroductionPoint: pubkey1,
			BlindedHops: []*sphinx.BlindedHopInfo{
				{},
			},
		},
	}

	blindedMultiHop := &BlindedPayment{
		CltvExpiryDelta: blindedCltv,
		BlindedPath: &sphinx.BlindedPath{
			IntroductionPoint: pubkey1,
			BlindedHops: []*sphinx.BlindedHopInfo{
				{},
				{
					BlindedNodePub: pubkey2,
				},
			},
		},
	}

	testCases := []struct {
		name           string
		target         *route.Vertex
		routeHints     RouteHints
		blindedPayment *BlindedPayment
		finalExpiry    uint16

		expectedTarget route.Vertex
		expectedCltv   uint16
		err            error
	}{
		{
			name:           "blinded and target",
			target:         &v1,
			blindedPayment: blindedOtherIntro,
			err:            ErrTargetAndBlinded,
		},
		{
			// For single-hop blinded we have a final cltv.
			name:           "blinded intro node only",
			blindedPayment: blindedOtherIntro,
			expectedTarget: v1,
			expectedCltv:   blindedCltv,
			err:            nil,
		},
		{
			// For multi-hop blinded, we have no final cltv.
			name:           "blinded multi-hop",
			blindedPayment: blindedMultiHop,
			expectedTarget: v2,
			expectedCltv:   0,
			err:            nil,
		},
		{
			name:           "unblinded",
			target:         &v2,
			finalExpiry:    unblindedCltv,
			expectedTarget: v2,
			expectedCltv:   unblindedCltv,
			err:            nil,
		},
		{
			name:           "source node intro",
			blindedPayment: blindedSelfIntro,
			err:            ErrSelfIntro,
		},
		{
			name:           "hints and blinded",
			blindedPayment: blindedMultiHop,
			routeHints: make(
				map[route.Vertex][]AdditionalEdge,
			),
			err: ErrHintsAndBlinded,
		},
		{
			name:           "expiry and blinded",
			blindedPayment: blindedMultiHop,
			finalExpiry:    unblindedCltv,
			err:            ErrExpiryAndBlinded,
		},
	}

	for _, testCase := range testCases {
		testCase := testCase

		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()

			var (
				blindedPathInfo *BlindedPaymentPathSet
				expectedTarget  = testCase.expectedTarget
				err             error
			)
			if testCase.blindedPayment != nil {
				blindedPathInfo, err = NewBlindedPaymentPathSet(
					[]*BlindedPayment{
						testCase.blindedPayment,
					},
				)
				require.NoError(t, err)

				expectedTarget = route.NewVertex(
					blindedPathInfo.TargetPubKey(),
				)
			}

			req, err := NewRouteRequest(
				source, testCase.target, 1000, 0, nil, nil,
				testCase.routeHints, blindedPathInfo,
				testCase.finalExpiry,
			)
			require.ErrorIs(t, err, testCase.err)

			// Skip request validation if we got a non-nil error.
			if err != nil {
				return
			}

			require.Equal(t, req.Target, expectedTarget)
			require.Equal(
				t, req.FinalExpiry, testCase.expectedCltv,
			)
		})
	}
}

// TestAddEdgeUnknownVertexes tests that if an edge is added that contains two
// vertexes which we don't know of, the edge should be available for use
// regardless. This is due to the fact that we don't actually need node
// announcements for the channel vertexes to be able to use the channel.
func TestAddEdgeUnknownVertexes(t *testing.T) {
	t.Parallel()

	const startingBlockHeight = 101
	ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)

	var pub1 [33]byte
	copy(pub1[:], priv1.PubKey().SerializeCompressed())

	var pub2 [33]byte
	copy(pub2[:], priv2.PubKey().SerializeCompressed())

	// The two nodes we are about to add should not exist yet.
	_, exists1, err := ctx.graph.HasLightningNode(pub1)
	require.NoError(t, err, "unable to query graph")
	require.False(t, exists1)

	_, exists2, err := ctx.graph.HasLightningNode(pub2)
	require.NoError(t, err, "unable to query graph")
	require.False(t, exists2)

	// Add the edge between the two unknown nodes to the graph, and check
	// that the nodes are found after the fact.
	_, _, chanID, err := createChannelEdge(
		bitcoinKey1.SerializeCompressed(),
		bitcoinKey2.SerializeCompressed(),
		10000, 500,
	)
	require.NoError(t, err, "unable to create channel edge")

	edge := &models.ChannelEdgeInfo{
		ChannelID:        chanID.ToUint64(),
		NodeKey1Bytes:    pub1,
		NodeKey2Bytes:    pub2,
		BitcoinKey1Bytes: pub1,
		BitcoinKey2Bytes: pub2,
		AuthProof:        nil,
	}
	require.NoError(t, ctx.graph.AddChannelEdge(edge))

	// We must add the edge policy to be able to use the edge for route
	// finding.
	edgePolicy := &models.ChannelEdgePolicy{
		SigBytes:                  testSig.Serialize(),
		ChannelID:                 edge.ChannelID,
		LastUpdate:                testTime,
		TimeLockDelta:             10,
		MinHTLC:                   1,
		FeeBaseMSat:               10,
		FeeProportionalMillionths: 10000,
		ToNode:                    edge.NodeKey2Bytes,
	}
	edgePolicy.ChannelFlags = 0

	require.NoError(t, ctx.graph.UpdateEdgePolicy(edgePolicy))

	// Create edge in the other direction as well.
	edgePolicy = &models.ChannelEdgePolicy{
		SigBytes:                  testSig.Serialize(),
		ChannelID:                 edge.ChannelID,
		LastUpdate:                testTime,
		TimeLockDelta:             10,
		MinHTLC:                   1,
		FeeBaseMSat:               10,
		FeeProportionalMillionths: 10000,
		ToNode:                    edge.NodeKey1Bytes,
	}
	edgePolicy.ChannelFlags = 1

	require.NoError(t, ctx.graph.UpdateEdgePolicy(edgePolicy))

	// After adding the edge between the two previously unknown nodes, they
	// should have been added to the graph.
	_, exists1, err = ctx.graph.HasLightningNode(pub1)
	require.NoError(t, err, "unable to query graph")
	require.True(t, exists1)

	_, exists2, err = ctx.graph.HasLightningNode(pub2)
	require.NoError(t, err, "unable to query graph")
	require.True(t, exists2)

	// We will connect node1 to the rest of the test graph, and make sure
	// we can find a route to node2, which will use the just added channel
	// edge.

	// We will connect node 1 to "sophon"
	connectNode := ctx.aliases["sophon"]
	connectNodeKey, err := btcec.ParsePubKey(connectNode[:])
	require.NoError(t, err)

	var (
		pubKey1 *btcec.PublicKey
		pubKey2 *btcec.PublicKey
	)
	node1Bytes := priv1.PubKey().SerializeCompressed()
	node2Bytes := connectNode
	if bytes.Compare(node1Bytes, node2Bytes[:]) == -1 {
		pubKey1 = priv1.PubKey()
		pubKey2 = connectNodeKey
	} else {
		pubKey1 = connectNodeKey
		pubKey2 = priv1.PubKey()
	}

	_, _, chanID, err = createChannelEdge(
		pubKey1.SerializeCompressed(), pubKey2.SerializeCompressed(),
		10000, 510)
	require.NoError(t, err, "unable to create channel edge")

	edge = &models.ChannelEdgeInfo{
		ChannelID: chanID.ToUint64(),
		AuthProof: nil,
	}
	copy(edge.NodeKey1Bytes[:], node1Bytes)
	edge.NodeKey2Bytes = node2Bytes
	copy(edge.BitcoinKey1Bytes[:], node1Bytes)
	edge.BitcoinKey2Bytes = node2Bytes

	require.NoError(t, ctx.graph.AddChannelEdge(edge))

	edgePolicy = &models.ChannelEdgePolicy{
		SigBytes:                  testSig.Serialize(),
		ChannelID:                 edge.ChannelID,
		LastUpdate:                testTime,
		TimeLockDelta:             10,
		MinHTLC:                   1,
		FeeBaseMSat:               10,
		FeeProportionalMillionths: 10000,
		ToNode:                    edge.NodeKey2Bytes,
	}
	edgePolicy.ChannelFlags = 0

	require.NoError(t, ctx.graph.UpdateEdgePolicy(edgePolicy))

	edgePolicy = &models.ChannelEdgePolicy{
		SigBytes:                  testSig.Serialize(),
		ChannelID:                 edge.ChannelID,
		LastUpdate:                testTime,
		TimeLockDelta:             10,
		MinHTLC:                   1,
		FeeBaseMSat:               10,
		FeeProportionalMillionths: 10000,
		ToNode:                    edge.NodeKey1Bytes,
	}
	edgePolicy.ChannelFlags = 1

	require.NoError(t, ctx.graph.UpdateEdgePolicy(edgePolicy))

	// We should now be able to find a route to node 2.
	paymentAmt := lnwire.NewMSatFromSatoshis(100)
	targetNode := priv2.PubKey()
	var targetPubKeyBytes route.Vertex
	copy(targetPubKeyBytes[:], targetNode.SerializeCompressed())

	req, err := NewRouteRequest(
		ctx.router.cfg.SelfNode, &targetPubKeyBytes,
		paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
	)
	require.NoError(t, err, "invalid route request")
	_, _, err = ctx.router.FindRoute(req)
	require.NoError(t, err, "unable to find any routes")

	// Now check that we can update the node info for the partial node
	// without messing up the channel graph.
	n1 := &channeldb.LightningNode{
		HaveNodeAnnouncement: true,
		LastUpdate:           time.Unix(123, 0),
		Addresses:            testAddrs,
		Color:                color.RGBA{1, 2, 3, 0},
		Alias:                "node11",
		AuthSigBytes:         testSig.Serialize(),
		Features:             testFeatures,
	}
	copy(n1.PubKeyBytes[:], priv1.PubKey().SerializeCompressed())

	require.NoError(t, ctx.graph.AddLightningNode(n1))

	n2 := &channeldb.LightningNode{
		HaveNodeAnnouncement: true,
		LastUpdate:           time.Unix(123, 0),
		Addresses:            testAddrs,
		Color:                color.RGBA{1, 2, 3, 0},
		Alias:                "node22",
		AuthSigBytes:         testSig.Serialize(),
		Features:             testFeatures,
	}
	copy(n2.PubKeyBytes[:], priv2.PubKey().SerializeCompressed())

	require.NoError(t, ctx.graph.AddLightningNode(n2))

	// Should still be able to find the route, and the info should be
	// updated.
	req, err = NewRouteRequest(
		ctx.router.cfg.SelfNode, &targetPubKeyBytes,
		paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
	)
	require.NoError(t, err, "invalid route request")

	_, _, err = ctx.router.FindRoute(req)
	require.NoError(t, err, "unable to find any routes")

	copy1, err := ctx.graph.FetchLightningNode(pub1)
	require.NoError(t, err, "unable to fetch node")

	require.Equal(t, n1.Alias, copy1.Alias)

	copy2, err := ctx.graph.FetchLightningNode(pub2)
	require.NoError(t, err, "unable to fetch node")

	require.Equal(t, n2.Alias, copy2.Alias)
}

func createDummyLightningPayment(t *testing.T,
	target route.Vertex, amt lnwire.MilliSatoshi) *LightningPayment {

	var preImage lntypes.Preimage
	_, err := rand.Read(preImage[:])
	require.NoError(t, err, "unable to generate preimage")

	payHash := preImage.Hash()

	return &LightningPayment{
		Target:      target,
		Amount:      amt,
		FeeLimit:    noFeeLimit,
		paymentHash: &payHash,
	}
}

type mockGraphBuilder struct {
	rejectUpdate bool
	updateEdge   func(update *models.ChannelEdgePolicy) error
}

func newMockGraphBuilder(graph graph.DB) *mockGraphBuilder {
	return &mockGraphBuilder{
		updateEdge: func(update *models.ChannelEdgePolicy) error {
			return graph.UpdateEdgePolicy(update)
		},
	}
}

func (m *mockGraphBuilder) setNextReject(reject bool) {
	m.rejectUpdate = reject
}

func (m *mockGraphBuilder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
	if m.rejectUpdate {
		return false
	}

	err := m.updateEdge(&models.ChannelEdgePolicy{
		SigBytes:                  msg.Signature.ToSignatureBytes(),
		ChannelID:                 msg.ShortChannelID.ToUint64(),
		LastUpdate:                time.Unix(int64(msg.Timestamp), 0),
		MessageFlags:              msg.MessageFlags,
		ChannelFlags:              msg.ChannelFlags,
		TimeLockDelta:             msg.TimeLockDelta,
		MinHTLC:                   msg.HtlcMinimumMsat,
		MaxHTLC:                   msg.HtlcMaximumMsat,
		FeeBaseMSat:               lnwire.MilliSatoshi(msg.BaseFee),
		FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate),
		ExtraOpaqueData:           msg.ExtraOpaqueData,
	})

	return err == nil
}

type mockChain struct {
	lnwallet.BlockChainIO

	blocks           map[chainhash.Hash]*wire.MsgBlock
	blockIndex       map[uint32]chainhash.Hash
	blockHeightIndex map[chainhash.Hash]uint32

	utxos map[wire.OutPoint]wire.TxOut

	bestHeight int32

	sync.RWMutex
}

func newMockChain(currentHeight uint32) *mockChain {
	return &mockChain{
		bestHeight:       int32(currentHeight),
		blocks:           make(map[chainhash.Hash]*wire.MsgBlock),
		utxos:            make(map[wire.OutPoint]wire.TxOut),
		blockIndex:       make(map[uint32]chainhash.Hash),
		blockHeightIndex: make(map[chainhash.Hash]uint32),
	}
}

func (m *mockChain) GetBestBlock() (*chainhash.Hash, int32, error) {
	m.RLock()
	defer m.RUnlock()

	blockHash := m.blockIndex[uint32(m.bestHeight)]

	return &blockHash, m.bestHeight, nil
}

func createChannelEdge(bitcoinKey1, bitcoinKey2 []byte,
	chanValue btcutil.Amount, fundingHeight uint32) (*wire.MsgTx,
	*wire.OutPoint, *lnwire.ShortChannelID, error) {

	fundingTx := wire.NewMsgTx(2)
	_, tx, err := input.GenFundingPkScript(
		bitcoinKey1,
		bitcoinKey2,
		int64(chanValue),
	)
	if err != nil {
		return nil, nil, nil, err
	}

	fundingTx.TxOut = append(fundingTx.TxOut, tx)
	chanUtxo := wire.OutPoint{
		Hash:  fundingTx.TxHash(),
		Index: 0,
	}

	// Our fake channel will be "confirmed" at height 101.
	chanID := &lnwire.ShortChannelID{
		BlockHeight: fundingHeight,
		TxIndex:     0,
		TxPosition:  0,
	}

	return fundingTx, &chanUtxo, chanID, nil
}

// TestFindBlindedPathsWithMC tests that the FindBlindedPaths method correctly
// selects a set of blinded paths by using mission control data to select the
// paths with the highest success probability.
func TestFindBlindedPathsWithMC(t *testing.T) {
	t.Parallel()

	rbFeatureBits := []lnwire.FeatureBit{
		lnwire.RouteBlindingOptional,
	}

	// Create the following graph and let all the nodes advertise support
	// for blinded paths.
	//
	//			  C
	//			/   \
	//		       /     \
	//		E -- A -- F -- D
	//		       \     /
	//			\   /
	//		          B
	//
	featuresWithRouteBlinding := lnwire.NewFeatureVector(
		lnwire.NewRawFeatureVector(rbFeatureBits...), lnwire.Features,
	)

	policyWithRouteBlinding := &testChannelPolicy{
		Expiry:   144,
		FeeRate:  400,
		MinHTLC:  1,
		MaxHTLC:  100000000,
		Features: featuresWithRouteBlinding,
	}

	testChannels := []*testChannel{
		symmetricTestChannel(
			"eve", "alice", 100000, policyWithRouteBlinding, 1,
		),
		symmetricTestChannel(
			"alice", "charlie", 100000, policyWithRouteBlinding, 2,
		),
		symmetricTestChannel(
			"alice", "bob", 100000, policyWithRouteBlinding, 3,
		),
		symmetricTestChannel(
			"charlie", "dave", 100000, policyWithRouteBlinding, 4,
		),
		symmetricTestChannel(
			"bob", "dave", 100000, policyWithRouteBlinding, 5,
		),
		symmetricTestChannel(
			"alice", "frank", 100000, policyWithRouteBlinding, 6,
		),
		symmetricTestChannel(
			"frank", "dave", 100000, policyWithRouteBlinding, 7,
		),
	}

	testGraph, err := createTestGraphFromChannels(
		t, true, testChannels, "dave", rbFeatureBits...,
	)
	require.NoError(t, err)

	ctx := createTestCtxFromGraphInstance(t, 101, testGraph)

	var (
		alice   = ctx.aliases["alice"]
		bob     = ctx.aliases["bob"]
		charlie = ctx.aliases["charlie"]
		dave    = ctx.aliases["dave"]
		eve     = ctx.aliases["eve"]
		frank   = ctx.aliases["frank"]
	)

	// Create a mission control store which initially sets the success
	// probability of each node pair to 1.
	missionControl := map[route.Vertex]map[route.Vertex]float64{
		eve: {alice: 1},
		alice: {
			charlie: 1,
			bob:     1,
			frank:   1,
		},
		charlie: {dave: 1},
		bob:     {dave: 1},
		frank:   {dave: 1},
	}

	// probabilitySrc is a helper that returns the mission control success
	// probability of a forward between two vertices.
	probabilitySrc := func(from route.Vertex, to route.Vertex,
		amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {

		return missionControl[from][to]
	}

	// All the probabilities are set to 1. So if we restrict the path length
	// to 2 and allow a max of 3 routes, then we expect three paths here.
	routes, err := ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              3,
		},
	)
	require.NoError(t, err)
	require.Len(t, routes, 3)

	// assertPaths checks that the resulting set of paths is equal to the
	// expected set and that the order of the paths is correct.
	assertPaths := func(paths []*route.Route, expectedPaths []string) {
		require.Len(t, paths, len(expectedPaths))

		var actualPaths []string
		for _, path := range paths {
			label := getAliasFromPubKey(
				path.SourcePubKey, ctx.aliases,
			) + ","

			for _, hop := range path.Hops {
				label += getAliasFromPubKey(
					hop.PubKeyBytes, ctx.aliases,
				) + ","
			}

			actualPaths = append(
				actualPaths, strings.TrimRight(label, ","),
			)
		}

		for i, path := range expectedPaths {
			require.Equal(t, path, actualPaths[i])
		}
	}

	// Now, let's lower the MC probability of the B-D to 0.5 and F-D link to
	// 0.25. We will leave the MaxNumPaths as 3 and so all paths should
	// still be returned but the order should be:
	// 1) A -> C -> D
	// 2) A -> B -> D
	// 3) A -> F -> D
	missionControl[bob][dave] = 0.5
	missionControl[frank][dave] = 0.25
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              3,
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"alice,charlie,dave",
		"alice,bob,dave",
		"alice,frank,dave",
	})

	// Just to show that the above result was not a fluke, let's change
	// the C->D link to be the weak one.
	missionControl[charlie][dave] = 0.125
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              3,
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"alice,bob,dave",
		"alice,frank,dave",
		"alice,charlie,dave",
	})

	// Change the MaxNumPaths to 1 to assert that only the best route is
	// returned.
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              1,
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"alice,bob,dave",
	})

	// Test the edge case where Dave, the recipient, is also the
	// introduction node.
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 0,
			NumHops:                  0,
			MaxNumPaths:              1,
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"dave",
	})

	// Finally, we make one of the routes have a probability less than the
	// minimum. This means we expect that route not to be chosen.
	missionControl[charlie][dave] = DefaultMinRouteProbability
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              3,
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"alice,bob,dave",
		"alice,frank,dave",
	})

	// Test that if the user explicitly indicates that we should ignore
	// the Frank node during path selection, then this is done.
	routes, err = ctx.router.FindBlindedPaths(
		dave, 1000, probabilitySrc, &BlindedPathRestrictions{
			MinDistanceFromIntroNode: 2,
			NumHops:                  2,
			MaxNumPaths:              3,
			NodeOmissionSet:          fn.NewSet(frank),
		},
	)
	require.NoError(t, err)
	assertPaths(routes, []string{
		"alice,bob,dave",
	})
}
