package itest

import (
	"context"
	"encoding/hex"
	"fmt"
	"os"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcwallet/wallet"
	"github.com/lightningnetwork/lnd/chainreg"
	"github.com/lightningnetwork/lnd/funding"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/lncfg"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
	"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lntest"
	"github.com/lightningnetwork/lnd/lntest/node"
	"github.com/lightningnetwork/lnd/lntest/wait"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/stretchr/testify/require"
)

// testDisconnectingTargetPeer performs a test which disconnects Alice-peer
// from Bob-peer and then re-connects them again. We expect Alice to be able to
// disconnect at any point.
//
// TODO(yy): move to lnd_network_test.
func testDisconnectingTargetPeer(ht *lntest.HarnessTest) {
	// We'll start both nodes with a high backoff so that they don't
	// reconnect automatically during our test.
	args := []string{
		"--minbackoff=1m",
		"--maxbackoff=1m",
	}

	alice, bob := ht.Alice, ht.Bob
	ht.RestartNodeWithExtraArgs(alice, args)
	ht.RestartNodeWithExtraArgs(bob, args)

	// Start by connecting Alice and Bob with no channels.
	ht.EnsureConnected(alice, bob)

	chanAmt := funding.MaxBtcFundingAmount
	pushAmt := btcutil.Amount(0)

	// Create a new channel that requires 1 confs before it's considered
	// open, then broadcast the funding transaction
	const numConfs = 1
	p := lntest.OpenChannelParams{
		Amt:     chanAmt,
		PushAmt: pushAmt,
	}
	stream := ht.OpenChannelAssertStream(alice, bob, p)

	// At this point, the channel's funding transaction will have been
	// broadcast, but not confirmed. Alice and Bob's nodes should reflect
	// this when queried via RPC.
	ht.AssertNumPendingOpenChannels(alice, 1)
	ht.AssertNumPendingOpenChannels(bob, 1)

	// Disconnect Alice-peer from Bob-peer should have no error.
	ht.DisconnectNodes(alice, bob)

	// Assert that the connection was torn down.
	ht.AssertNotConnected(alice, bob)

	// Mine a block, then wait for Alice's node to notify us that the
	// channel has been opened.
	ht.MineBlocksAndAssertNumTxes(numConfs, 1)

	// At this point, the channel should be fully opened and there should
	// be no pending channels remaining for either node.
	ht.AssertNumPendingOpenChannels(alice, 0)
	ht.AssertNumPendingOpenChannels(bob, 0)

	// Reconnect the nodes so that the channel can become active.
	ht.ConnectNodes(alice, bob)

	// The channel should be listed in the peer information returned by
	// both peers.
	chanPoint := ht.WaitForChannelOpenEvent(stream)

	// Check both nodes to ensure that the channel is ready for operation.
	ht.AssertChannelExists(alice, chanPoint)
	ht.AssertChannelExists(bob, chanPoint)

	// Disconnect Alice-peer from Bob-peer should have no error.
	ht.DisconnectNodes(alice, bob)

	// Check existing connection.
	ht.AssertNotConnected(alice, bob)

	// Reconnect both nodes before force closing the channel.
	ht.ConnectNodes(alice, bob)

	// Finally, immediately close the channel. This function will also
	// block until the channel is closed and will additionally assert the
	// relevant channel closing post conditions.
	ht.ForceCloseChannel(alice, chanPoint)

	// Disconnect Alice-peer from Bob-peer should have no error.
	ht.DisconnectNodes(alice, bob)

	// Check that the nodes not connected.
	ht.AssertNotConnected(alice, bob)

	// Finally, re-connect both nodes.
	ht.ConnectNodes(alice, bob)

	// Check existing connection.
	ht.AssertConnected(alice, bob)
}

// testSphinxReplayPersistence verifies that replayed onion packets are
// rejected by a remote peer after a restart. We use a combination of unsafe
// configuration arguments to force Carol to replay the same sphinx packet
// after reconnecting to Dave, and compare the returned failure message with
// what we expect for replayed onion packets.
func testSphinxReplayPersistence(ht *lntest.HarnessTest) {
	// Open a channel with 100k satoshis between Carol and Dave with Carol
	// being the sole funder of the channel.
	chanAmt := btcutil.Amount(100000)

	// First, we'll create Dave, the receiver, and start him in hodl mode.
	dave := ht.NewNode("Dave", []string{"--hodl.exit-settle"})

	// Next, we'll create Carol and establish a channel to from her to
	// Dave. Carol is started in both unsafe-replay which will cause her to
	// replay any pending Adds held in memory upon reconnection.
	carol := ht.NewNode("Carol", []string{"--unsafe-replay"})
	ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)

	ht.ConnectNodes(carol, dave)
	chanPoint := ht.OpenChannel(
		carol, dave, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Next, we'll create Fred who is going to initiate the payment and
	// establish a channel to from him to Carol. We can't perform this test
	// by paying from Carol directly to Dave, because the '--unsafe-replay'
	// setup doesn't apply to locally added htlcs. In that case, the
	// mailbox, that is responsible for generating the replay, is bypassed.
	fred := ht.NewNode("Fred", nil)
	ht.FundCoins(btcutil.SatoshiPerBitcoin, fred)

	ht.ConnectNodes(fred, carol)
	chanPointFC := ht.OpenChannel(
		fred, carol, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)
	defer ht.CloseChannel(fred, chanPointFC)

	// Now that the channel is open, create an invoice for Dave which
	// expects a payment of 1000 satoshis from Carol paid via a particular
	// preimage.
	const paymentAmt = 1000
	preimage := ht.Random32Bytes()
	invoice := &lnrpc.Invoice{
		Memo:      "testing",
		RPreimage: preimage,
		Value:     paymentAmt,
	}
	invoiceResp := dave.RPC.AddInvoice(invoice)

	// Wait for all channels to be recognized and advertized.
	ht.AssertTopologyChannelOpen(carol, chanPoint)
	ht.AssertTopologyChannelOpen(dave, chanPoint)
	ht.AssertTopologyChannelOpen(carol, chanPointFC)
	ht.AssertTopologyChannelOpen(fred, chanPointFC)

	// With the invoice for Dave added, send a payment from Fred paying
	// to the above generated invoice.
	req := &routerrpc.SendPaymentRequest{
		PaymentRequest: invoiceResp.PaymentRequest,
		TimeoutSeconds: 60,
		FeeLimitMsat:   noFeeLimitMsat,
	}
	payStream := fred.RPC.SendPayment(req)

	// Dave's invoice should not be marked as settled.
	msg := &invoicesrpc.LookupInvoiceMsg{
		InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{
			PaymentAddr: invoiceResp.PaymentAddr,
		},
	}
	dbInvoice := dave.RPC.LookupInvoiceV2(msg)
	require.NotEqual(ht, lnrpc.InvoiceHTLCState_SETTLED, dbInvoice.State,
		"dave's invoice should not be marked as settled")

	// With the payment sent but hedl, all balance related stats should not
	// have changed.
	ht.AssertAmountPaid("carol => dave", carol, chanPoint, 0, 0)
	ht.AssertAmountPaid("dave <= carol", dave, chanPoint, 0, 0)

	// Before we restart Dave, make sure both Carol and Dave have added the
	// HTLC.
	ht.AssertNumActiveHtlcs(carol, 2)
	ht.AssertNumActiveHtlcs(dave, 1)

	// With the first payment sent, restart dave to make sure he is
	// persisting the information required to detect replayed sphinx
	// packets.
	ht.RestartNode(dave)

	// Carol should retransmit the Add hedl in her mailbox on startup. Dave
	// should not accept the replayed Add, and actually fail back the
	// pending payment. Even though he still holds the original settle, if
	// he does fail, it is almost certainly caused by the sphinx replay
	// protection, as it is the only validation we do in hodl mode.
	//
	// Assert that Fred receives the expected failure after Carol sent a
	// duplicate packet that fails due to sphinx replay detection.
	ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_FAILED)
	ht.AssertLastHTLCError(fred, lnrpc.Failure_INVALID_ONION_VERSION)

	// Since the payment failed, the balance should still be left
	// unaltered.
	ht.AssertAmountPaid("carol => dave", carol, chanPoint, 0, 0)
	ht.AssertAmountPaid("dave <= carol", dave, chanPoint, 0, 0)

	// Cleanup by mining the force close and sweep transaction.
	ht.ForceCloseChannel(carol, chanPoint)
}

// testListChannels checks that the response from ListChannels is correct. It
// tests the values in all ChannelConstraints are returned as expected. Once
// ListChannels becomes mature, a test against all fields in ListChannels
// should be performed.
func testListChannels(ht *lntest.HarnessTest) {
	const aliceRemoteMaxHtlcs = 50
	const bobRemoteMaxHtlcs = 100

	// Get the standby nodes and open a channel between them.
	alice, bob := ht.Alice, ht.Bob

	args := []string{fmt.Sprintf(
		"--default-remote-max-htlcs=%v",
		bobRemoteMaxHtlcs,
	)}
	ht.RestartNodeWithExtraArgs(bob, args)

	// Connect Alice to Bob.
	ht.EnsureConnected(alice, bob)

	// Open a channel with 100k satoshis between Alice and Bob with Alice
	// being the sole funder of the channel. The minial HTLC amount is set
	// to 4200 msats.
	const customizedMinHtlc = 4200

	chanAmt := btcutil.Amount(100000)
	pushAmt := btcutil.Amount(1000)
	p := lntest.OpenChannelParams{
		Amt:            chanAmt,
		PushAmt:        pushAmt,
		MinHtlc:        customizedMinHtlc,
		RemoteMaxHtlcs: aliceRemoteMaxHtlcs,
	}
	chanPoint := ht.OpenChannel(alice, bob, p)
	defer ht.CloseChannel(alice, chanPoint)

	// Alice should have one channel opened with Bob.
	ht.AssertNodeNumChannels(alice, 1)
	// Bob should have one channel opened with Alice.
	ht.AssertNodeNumChannels(bob, 1)

	// Check the returned response is correct.
	aliceChannel := ht.QueryChannelByChanPoint(alice, chanPoint)

	// Query the channel again, this time with peer alias lookup.
	aliceChannelWithAlias := ht.QueryChannelByChanPoint(
		alice, chanPoint, lntest.WithPeerAliasLookup(),
	)

	// Since Alice is the initiator, she pays the commit fee.
	aliceBalance := int64(chanAmt) - aliceChannel.CommitFee - int64(pushAmt)

	bobAlias := bob.RPC.GetInfo().Alias

	// Check the balance related fields are correct.
	require.Equal(ht, aliceBalance, aliceChannel.LocalBalance)
	require.Empty(ht, aliceChannel.PeerAlias)
	require.Equal(ht, bobAlias, aliceChannelWithAlias.PeerAlias)
	require.EqualValues(ht, pushAmt, aliceChannel.RemoteBalance)
	require.EqualValues(ht, pushAmt, aliceChannel.PushAmountSat)

	// Calculate the dust limit we'll use for the test.
	dustLimit := lnwallet.DustLimitForSize(input.UnknownWitnessSize)

	// defaultConstraints is a ChannelConstraints with default values. It
	// is used to test against Alice's local channel constraints.
	defaultConstraints := &lnrpc.ChannelConstraints{
		CsvDelay:          4,
		ChanReserveSat:    1000,
		DustLimitSat:      uint64(dustLimit),
		MaxPendingAmtMsat: 99000000,
		MinHtlcMsat:       1,
		MaxAcceptedHtlcs:  bobRemoteMaxHtlcs,
	}
	assertChannelConstraintsEqual(
		ht, defaultConstraints, aliceChannel.LocalConstraints,
	)

	// customizedConstraints is a ChannelConstraints with customized
	// values. Ideally, all these values can be passed in when creating the
	// channel. Currently, only the MinHtlcMsat is customized. It is used
	// to check against Alice's remote channel constratins.
	customizedConstraints := &lnrpc.ChannelConstraints{
		CsvDelay:          4,
		ChanReserveSat:    1000,
		DustLimitSat:      uint64(dustLimit),
		MaxPendingAmtMsat: 99000000,
		MinHtlcMsat:       customizedMinHtlc,
		MaxAcceptedHtlcs:  aliceRemoteMaxHtlcs,
	}
	assertChannelConstraintsEqual(
		ht, customizedConstraints, aliceChannel.RemoteConstraints,
	)

	// Get the ListChannel response for Bob.
	bobChannel := ht.QueryChannelByChanPoint(bob, chanPoint)
	require.Equal(ht, aliceChannel.ChannelPoint, bobChannel.ChannelPoint,
		"Bob's channel point mismatched")

	// Query the channel again, this time with node alias lookup.
	bobChannelWithAlias := ht.QueryChannelByChanPoint(
		bob, chanPoint, lntest.WithPeerAliasLookup(),
	)

	aliceAlias := alice.RPC.GetInfo().Alias

	// Check the balance related fields are correct.
	require.Equal(ht, aliceBalance, bobChannel.RemoteBalance)
	require.Empty(ht, bobChannel.PeerAlias)
	require.Equal(ht, aliceAlias, bobChannelWithAlias.PeerAlias)
	require.EqualValues(ht, pushAmt, bobChannel.LocalBalance)
	require.EqualValues(ht, pushAmt, bobChannel.PushAmountSat)

	// Check channel constraints match. Alice's local channel constraint
	// should be equal to Bob's remote channel constraint, and her remote
	// one should be equal to Bob's local one.
	assertChannelConstraintsEqual(
		ht, aliceChannel.LocalConstraints, bobChannel.RemoteConstraints,
	)
	assertChannelConstraintsEqual(
		ht, aliceChannel.RemoteConstraints, bobChannel.LocalConstraints,
	)
}

// testMaxPendingChannels checks that error is returned from remote peer if
// max pending channel number was exceeded and that '--maxpendingchannels' flag
// exists and works properly.
func testMaxPendingChannels(ht *lntest.HarnessTest) {
	maxPendingChannels := lncfg.DefaultMaxPendingChannels + 1
	amount := funding.MaxBtcFundingAmount

	// Create a new node (Carol) with greater number of max pending
	// channels.
	args := []string{
		fmt.Sprintf("--maxpendingchannels=%v", maxPendingChannels),
	}
	carol := ht.NewNode("Carol", args)

	alice := ht.Alice
	ht.ConnectNodes(alice, carol)

	carolBalance := btcutil.Amount(maxPendingChannels) * amount
	ht.FundCoins(carolBalance, carol)

	// Send open channel requests without generating new blocks thereby
	// increasing pool of pending channels. Then check that we can't open
	// the channel if the number of pending channels exceed max value.
	openStreams := make(
		[]lnrpc.Lightning_OpenChannelClient, maxPendingChannels,
	)
	for i := 0; i < maxPendingChannels; i++ {
		stream := ht.OpenChannelAssertStream(
			alice, carol, lntest.OpenChannelParams{
				Amt: amount,
			},
		)
		openStreams[i] = stream
	}

	// Carol exhausted available amount of pending channels, next open
	// channel request should cause ErrorGeneric to be sent back to Alice.
	ht.OpenChannelAssertErr(
		alice, carol, lntest.OpenChannelParams{
			Amt: amount,
		}, lnwire.ErrMaxPendingChannels,
	)

	// For now our channels are in pending state, in order to not interfere
	// with other tests we should clean up - complete opening of the
	// channel and then close it.

	// Mine 6 blocks, then wait for node's to notify us that the channel
	// has been opened. The funding transactions should be found within the
	// first newly mined block. 6 blocks make sure the funding transaction
	// has enough confirmations to be announced publicly.
	block := ht.MineBlocksAndAssertNumTxes(6, maxPendingChannels)[0]

	chanPoints := make([]*lnrpc.ChannelPoint, maxPendingChannels)
	for i, stream := range openStreams {
		fundingChanPoint := ht.WaitForChannelOpenEvent(stream)

		fundingTxID := ht.GetChanPointFundingTxid(fundingChanPoint)

		// Ensure that the funding transaction enters a block, and is
		// properly advertised by Alice.
		ht.AssertTxInBlock(block, fundingTxID)
		ht.AssertTopologyChannelOpen(alice, fundingChanPoint)

		// The channel should be listed in the peer information
		// returned by both peers.
		ht.AssertChannelExists(alice, fundingChanPoint)

		chanPoints[i] = fundingChanPoint
	}

	// Next, close the channel between Alice and Carol, asserting that the
	// channel has been properly closed on-chain.
	for _, chanPoint := range chanPoints {
		ht.CloseChannel(alice, chanPoint)
	}
}

// testGarbageCollectLinkNodes tests that we properly garbage collect link
// nodes from the database and the set of persistent connections within the
// server.
func testGarbageCollectLinkNodes(ht *lntest.HarnessTest) {
	const chanAmt = 1000000

	alice, bob := ht.Alice, ht.Bob

	// Open a channel between Alice and Bob which will later be
	// cooperatively closed.
	coopChanPoint := ht.OpenChannel(
		alice, bob, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Create Carol's node and connect Alice to her.
	carol := ht.NewNode("Carol", nil)
	ht.ConnectNodes(alice, carol)

	// Open a channel between Alice and Carol which will later be force
	// closed.
	forceCloseChanPoint := ht.OpenChannel(
		alice, carol, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Now, create Dave's a node and also open a channel between Alice and
	// him. This link will serve as the only persistent link throughout
	// restarts in this test.
	dave := ht.NewNode("Dave", nil)

	ht.ConnectNodes(alice, dave)
	persistentChanPoint := ht.OpenChannel(
		alice, dave, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Restart both Bob and Carol to ensure Alice is able to reconnect to
	// them.
	ht.RestartNode(bob)
	ht.RestartNode(carol)

	ht.AssertConnected(alice, bob)
	ht.AssertConnected(alice, carol)

	// We'll also restart Alice to ensure she can reconnect to her peers
	// with open channels.
	ht.RestartNode(alice)

	ht.AssertConnected(alice, bob)
	ht.AssertConnected(alice, carol)
	ht.AssertConnected(alice, dave)

	// testReconnection is a helper closure that restarts the nodes at both
	// ends of a channel to ensure they do not reconnect after restarting.
	// When restarting Alice, we'll first need to ensure she has
	// reestablished her connection with Dave, as they still have an open
	// channel together.
	testReconnection := func(node *node.HarnessNode) {
		// Restart both nodes, to trigger the pruning logic.
		ht.RestartNode(node)
		ht.RestartNode(alice)

		// Now restart both nodes and make sure they don't reconnect.
		ht.RestartNode(node)
		ht.AssertNotConnected(alice, node)

		ht.RestartNode(alice)
		ht.AssertConnected(alice, dave)
		ht.AssertNotConnected(alice, node)
	}

	// Now, we'll close the channel between Alice and Bob and ensure there
	// is no reconnection logic between the both once the channel is fully
	// closed.
	ht.CloseChannel(alice, coopChanPoint)

	testReconnection(bob)

	// We'll do the same with Alice and Carol, but this time we'll force
	// close the channel instead.
	ht.ForceCloseChannel(alice, forceCloseChanPoint)

	// We'll need to mine some blocks in order to mark the channel fully
	// closed.
	ht.MineBlocks(
		chainreg.DefaultBitcoinTimeLockDelta - defaultCSV,
	)

	// Before we test reconnection, we'll ensure that the channel has been
	// fully cleaned up for both Carol and Alice.
	ht.AssertNumPendingForceClose(alice, 0)
	ht.AssertNumPendingForceClose(carol, 0)

	testReconnection(carol)

	// Finally, we'll ensure that Bob and Carol no longer show in Alice's
	// channel graph.
	req := &lnrpc.ChannelGraphRequest{IncludeUnannounced: true}
	channelGraph := alice.RPC.DescribeGraph(req)
	require.NotContains(ht, channelGraph.Nodes, bob.PubKeyStr,
		"did not expect to find bob in the channel graph, but did")
	require.NotContains(ht, channelGraph.Nodes, carol.PubKeyStr,
		"did not expect to find carol in the channel graph, but did")

	// Now that the test is done, we can also close the persistent link.
	ht.CloseChannel(alice, persistentChanPoint)
}

// testRejectHTLC tests that a node can be created with the flag --rejecthtlc.
// This means that the node will reject all forwarded HTLCs but can still
// accept direct HTLCs as well as send HTLCs.
func testRejectHTLC(ht *lntest.HarnessTest) {
	//             RejectHTLC
	// Alice ------> Carol ------> Bob
	//
	const chanAmt = btcutil.Amount(1000000)
	alice, bob := ht.Alice, ht.Bob

	// Create Carol with reject htlc flag.
	carol := ht.NewNode("Carol", []string{"--rejecthtlc"})

	// Connect Alice to Carol.
	ht.ConnectNodes(alice, carol)

	// Connect Carol to Bob.
	ht.ConnectNodes(carol, bob)

	// Send coins to Carol.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)

	// Open a channel between Alice and Carol.
	chanPointAlice := ht.OpenChannel(
		alice, carol, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Open a channel between Carol and Bob.
	chanPointCarol := ht.OpenChannel(
		carol, bob, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Channel should be ready for payments.
	const payAmt = 100

	// Create an invoice from Carol of 100 satoshis.
	// We expect Alice to be able to pay this invoice.
	carolInvoice := &lnrpc.Invoice{
		Memo:      "testing - alice should pay carol",
		RPreimage: ht.Random32Bytes(),
		Value:     payAmt,
	}

	// Carol adds the invoice to her database.
	resp := carol.RPC.AddInvoice(carolInvoice)

	// Alice pays Carols invoice.
	ht.CompletePaymentRequests(alice, []string{resp.PaymentRequest})

	// Create an invoice from Bob of 100 satoshis.
	// We expect Carol to be able to pay this invoice.
	bobInvoice := &lnrpc.Invoice{
		Memo:      "testing - carol should pay bob",
		RPreimage: ht.Random32Bytes(),
		Value:     payAmt,
	}

	// Bob adds the invoice to his database.
	resp = bob.RPC.AddInvoice(bobInvoice)

	// Carol pays Bobs invoice.
	ht.CompletePaymentRequests(carol, []string{resp.PaymentRequest})

	// Create an invoice from Bob of 100 satoshis.
	// Alice attempts to pay Bob but this should fail, since we are
	// using Carol as a hop and her node will reject onward HTLCs.
	bobInvoice = &lnrpc.Invoice{
		Memo:      "testing - alice tries to pay bob",
		RPreimage: ht.Random32Bytes(),
		Value:     payAmt,
	}

	// Bob adds the invoice to his database.
	resp = bob.RPC.AddInvoice(bobInvoice)

	// Alice attempts to pay Bobs invoice. This payment should be rejected
	// since we are using Carol as an intermediary hop, Carol is running
	// lnd with --rejecthtlc.
	paymentReq := &routerrpc.SendPaymentRequest{
		PaymentRequest: resp.PaymentRequest,
		TimeoutSeconds: 60,
		FeeLimitMsat:   noFeeLimitMsat,
	}
	payStream := alice.RPC.SendPayment(paymentReq)
	ht.AssertPaymentStatusFromStream(payStream, lnrpc.Payment_FAILED)

	ht.AssertLastHTLCError(alice, lnrpc.Failure_CHANNEL_DISABLED)

	// Close all channels.
	ht.CloseChannel(alice, chanPointAlice)
	ht.CloseChannel(carol, chanPointCarol)
}

// testNodeSignVerify checks that only connected nodes are allowed to perform
// signing and verifying messages.
func testNodeSignVerify(ht *lntest.HarnessTest) {
	chanAmt := funding.MaxBtcFundingAmount
	pushAmt := btcutil.Amount(100000)
	alice, bob := ht.Alice, ht.Bob

	// Create a channel between alice and bob.
	aliceBobCh := ht.OpenChannel(
		alice, bob, lntest.OpenChannelParams{
			Amt:     chanAmt,
			PushAmt: pushAmt,
		},
	)

	// alice signs "alice msg" and sends her signature to bob.
	aliceMsg := []byte("alice msg")
	sigResp := alice.RPC.SignMessage(aliceMsg)
	aliceSig := sigResp.Signature

	// bob verifying alice's signature should succeed since alice and bob
	// are connected.
	verifyResp := bob.RPC.VerifyMessage(aliceMsg, aliceSig)
	require.True(ht, verifyResp.Valid, "alice's signature didn't validate")
	require.Equal(ht, verifyResp.Pubkey, alice.PubKeyStr,
		"alice's signature doesn't contain alice's pubkey.")

	// carol is a new node that is unconnected to alice or bob.
	carol := ht.NewNode("Carol", nil)

	// carol signs "carol msg" and sends her signature to bob.
	carolMsg := []byte("carol msg")
	sigResp = carol.RPC.SignMessage(carolMsg)
	carolSig := sigResp.Signature

	// bob verifying carol's signature should fail since they are not
	// connected.
	verifyResp = bob.RPC.VerifyMessage(carolMsg, carolSig)
	require.False(ht, verifyResp.Valid, "carol's signature didn't validate")
	require.Equal(ht, verifyResp.Pubkey, carol.PubKeyStr,
		"carol's signature doesn't contain alice's pubkey.")

	// Close the channel between alice and bob.
	ht.CloseChannel(alice, aliceBobCh)
}

// testAbandonChannel abandons a channel and asserts that it is no longer open
// and not in one of the pending closure states. It also verifies that the
// abandoned channel is reported as closed with close type 'abandoned'.
func testAbandonChannel(ht *lntest.HarnessTest) {
	alice, bob := ht.Alice, ht.Bob

	// First establish a channel between Alice and Bob.
	channelParam := lntest.OpenChannelParams{
		Amt:     funding.MaxBtcFundingAmount,
		PushAmt: btcutil.Amount(100000),
	}
	chanPoint := ht.OpenChannel(alice, bob, channelParam)

	// Now that the channel is open, we'll obtain its channel ID real quick
	// so we can use it to query the graph below.
	chanID := ht.QueryChannelByChanPoint(alice, chanPoint).ChanId

	// To make sure the channel is removed from the backup file as well
	// when being abandoned, grab a backup snapshot so we can compare it
	// with the later state.
	bkupBefore, err := os.ReadFile(alice.Cfg.ChanBackupPath())
	require.NoError(ht, err, "channel backup before abandoning channel")

	// Send request to abandon channel.
	abandonChannelRequest := &lnrpc.AbandonChannelRequest{
		ChannelPoint: chanPoint,
	}
	alice.RPC.AbandonChannel(abandonChannelRequest)

	// Assert that channel in no longer open.
	ht.AssertNodeNumChannels(alice, 0)

	// Assert that channel is not pending closure.
	ht.AssertNumWaitingClose(alice, 0)

	// Assert that channel is listed as abandoned.
	req := &lnrpc.ClosedChannelsRequest{Abandoned: true}
	aliceClosedList := alice.RPC.ClosedChannels(req)
	require.Len(ht, aliceClosedList.Channels, 1, "alice closed channels")

	// Ensure that the channel can no longer be found in the channel graph.
	ht.AssertNotInGraph(alice, chanID)

	// Make sure the channel is no longer in the channel backup list.
	err = wait.NoError(func() error {
		bkupAfter, err := os.ReadFile(alice.Cfg.ChanBackupPath())
		if err != nil {
			return fmt.Errorf("could not get channel backup "+
				"before abandoning channel: %v", err)
		}

		if len(bkupAfter) >= len(bkupBefore) {
			return fmt.Errorf("expected backups after to be less "+
				"than %d but was %d", bkupBefore, bkupAfter)
		}

		return nil
	}, defaultTimeout)
	require.NoError(ht, err, "channel removed from backup file")

	// Calling AbandonChannel again, should result in no new errors, as the
	// channel has already been removed.
	alice.RPC.AbandonChannel(abandonChannelRequest)

	// Now that we're done with the test, the channel can be closed. This
	// is necessary to avoid unexpected outcomes of other tests that use
	// Bob's lnd instance.
	ht.ForceCloseChannel(bob, chanPoint)
}

// testSendAllCoins tests that we're able to properly sweep all coins from the
// wallet into a single target address at the specified fee rate.
func testSendAllCoins(ht *lntest.HarnessTest) {
	// First, we'll make a new node, Ainz who'll we'll use to test wallet
	// sweeping.
	//
	// NOTE: we won't use standby nodes here since the test will change
	// each of the node's wallet state.
	ainz := ht.NewNode("Ainz", nil)

	// Next, we'll give Ainz exactly 2 utxos of 1 BTC each, with one of
	// them being p2wkh and the other being a n2wpkh address.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, ainz)
	ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, ainz)

	// Create a label that will be used to label the transaction with.
	sendCoinsLabel := "send all coins"

	// Ensure that we can't send coins to our own Pubkey.
	err := ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:       ainz.RPC.GetInfo().IdentityPubkey,
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})
	require.ErrorContains(ht, err, "cannot send coins to pubkeys")

	// Ensure that we can't send coins to another user's Pubkey.
	err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:       ht.Alice.RPC.GetInfo().IdentityPubkey,
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})
	require.ErrorContains(ht, err, "cannot send coins to pubkey")

	// With the two coins above mined, we'll now instruct Ainz to sweep all
	// the coins to an external address not under its control. We will first
	// attempt to send the coins to addresses that are not compatible
	// with the current network. This is to test that the wallet will
	// prevent any on-chain transactions to addresses that are not on the
	// same network as the user.

	// Send coins to a testnet3 address.
	err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:       "tb1qfc8fusa98jx8uvnhzavxccqlzvg749tvjw82tg",
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})
	require.ErrorContains(ht, err, "not valid for this network")

	// Send coins to a mainnet address.
	err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:       "1MPaXKp5HhsLNjVSqaL7fChE3TVyrTMRT3",
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})
	// TODO(yy): should instead return "not valid for this network".
	require.ErrorContains(ht, err, "unknown address type")

	// TODO(yy): we still allow default values to be used when neither conf
	// target or fee rate is set in 0.18.0. When future release forbidden
	// this behavior, we should revive the test below, which asserts either
	// conf target or fee rate is set.
	//
	// Send coins to a compatible address without specifying fee rate or
	// conf target.
	// ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
	// 	Addr:    ht.NewMinerAddress().String(),
	// 	SendAll: true,
	// 	Label:   sendCoinsLabel,
	// })

	// Send coins to a compatible address.
	ainz.RPC.SendCoins(&lnrpc.SendCoinsRequest{
		Addr:       ht.NewMinerAddress().String(),
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})

	// We'll mine a block which should include the sweep transaction we
	// generated above.
	block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]

	// The sweep transaction should have exactly two inputs as we only had
	// two UTXOs in the wallet.
	sweepTx := block.Transactions[1]
	require.Len(ht, sweepTx.TxIn, 2, "expected 2 inputs")

	// assertTxLabel is a helper function which finds a target tx in our
	// set of transactions and checks that it has the desired label.
	assertTxLabel := func(targetTx, label string) error {
		// Get the transaction from our wallet so that we can check
		// that the correct label has been set.
		txResp := ainz.RPC.GetTransaction(
			&walletrpc.GetTransactionRequest{
				Txid: targetTx,
			},
		)
		require.NotNilf(ht, txResp, "target tx %v not found", targetTx)

		// Make sure the labels match.
		if txResp.Label == label {
			return nil
		}

		return fmt.Errorf("labels not match, want: "+
			"%v, got %v", label, txResp.Label)
	}

	// waitTxLabel waits until the desired tx label is found or timeout.
	waitTxLabel := func(targetTx, label string) {
		err := wait.NoError(func() error {
			return assertTxLabel(targetTx, label)
		}, defaultTimeout)

		require.NoError(ht, err, "timeout assertTxLabel")
	}

	sweepTxStr := sweepTx.TxHash().String()
	waitTxLabel(sweepTxStr, sendCoinsLabel)

	// While we are looking at labels, we test our label transaction
	// command to make sure it is behaving as expected. First, we try to
	// label our transaction with an empty label, and check that we fail as
	// expected.
	sweepHash := sweepTx.TxHash()
	err = ainz.RPC.LabelTransactionAssertErr(
		&walletrpc.LabelTransactionRequest{
			Txid:      sweepHash[:],
			Label:     "",
			Overwrite: false,
		},
	)

	// Our error will be wrapped in a rpc error, so we check that it
	// contains the error we expect.
	errZeroLabel := "cannot label transaction with empty label"
	require.Contains(ht, err.Error(), errZeroLabel)

	// Next, we try to relabel our transaction without setting the overwrite
	// boolean. We expect this to fail, because the wallet requires setting
	// of this param to prevent accidental overwrite of labels.
	err = ainz.RPC.LabelTransactionAssertErr(
		&walletrpc.LabelTransactionRequest{
			Txid:      sweepHash[:],
			Label:     "label that will not work",
			Overwrite: false,
		},
	)

	// Our error will be wrapped in a rpc error, so we check that it
	// contains the error we expect.
	require.Contains(ht, err.Error(), wallet.ErrTxLabelExists.Error())

	// Finally, we overwrite our label with a new label, which should not
	// fail.
	newLabel := "new sweep tx label"
	ainz.RPC.LabelTransaction(&walletrpc.LabelTransactionRequest{
		Txid:      sweepHash[:],
		Label:     newLabel,
		Overwrite: true,
	})

	waitTxLabel(sweepTxStr, newLabel)

	// Finally, Ainz should now have no coins at all within his wallet.
	resp := ainz.RPC.WalletBalance()
	require.Zero(ht, resp.ConfirmedBalance, "wrong confirmed balance")
	require.Zero(ht, resp.UnconfirmedBalance, "wrong unconfirmed balance")

	// If we try again, but this time specifying an amount, then the call
	// should fail.
	err = ainz.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:       ht.NewMinerAddress().String(),
		Amount:     10000,
		SendAll:    true,
		Label:      sendCoinsLabel,
		TargetConf: 6,
	})
	require.ErrorContains(ht, err, "amount set while SendAll is active")

	// With all the edge cases tested, we'll now test the happy paths of
	// change output types.
	// We'll be using a "main" address where we send the funds to and from
	// several times.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, ainz)
	mainAddrResp := ainz.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
	})
	spendAddrTypes := []lnrpc.AddressType{
		lnrpc.AddressType_NESTED_PUBKEY_HASH,
		lnrpc.AddressType_WITNESS_PUBKEY_HASH,
		lnrpc.AddressType_TAPROOT_PUBKEY,
	}
	for _, addrType := range spendAddrTypes {
		ht.Logf("testing with address type %s", addrType)

		// First, spend all the coins in the wallet to an address of the
		// given type so that UTXO will be picked when sending coins.
		sendAllCoinsToAddrType(ht, ainz, addrType)

		// Let's send some coins to the main address.
		const amt = 123456
		resp := ainz.RPC.SendCoins(&lnrpc.SendCoinsRequest{
			Addr:       mainAddrResp.Address,
			Amount:     amt,
			TargetConf: 6,
		})
		block := ht.MineBlocksAndAssertNumTxes(1, 1)[0]
		sweepTx := block.Transactions[1]
		require.Equal(ht, sweepTx.TxHash().String(), resp.Txid)

		// Find the change output, it will be the one with an amount
		// different from the amount we sent.
		var changeOutput *wire.TxOut
		for idx := range sweepTx.TxOut {
			txOut := sweepTx.TxOut[idx]
			if txOut.Value != amt {
				changeOutput = txOut
				break
			}
		}
		require.NotNil(ht, changeOutput)

		// Assert the type of change output to be p2tr.
		pkScript, err := txscript.ParsePkScript(changeOutput.PkScript)
		require.NoError(ht, err)
		require.Equal(ht, txscript.WitnessV1TaprootTy, pkScript.Class())
	}
}

// testListAddresses tests that we get all the addresses and their
// corresponding balance correctly.
func testListAddresses(ht *lntest.HarnessTest) {
	// First, we'll make a new node - Alice, which will be generating
	// new addresses.
	alice := ht.NewNode("Alice", nil)

	// Next, we'll give Alice exactly 1 utxo of 1 BTC.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)

	type addressDetails struct {
		Balance int64
		Type    walletrpc.AddressType
	}

	// A map of generated address and its balance.
	generatedAddr := make(map[string]addressDetails)

	// Create an address generated from internal keys.
	keyLoc := &walletrpc.KeyReq{KeyFamily: 123}
	keyDesc := alice.RPC.DeriveNextKey(keyLoc)

	// Hex Encode the public key.
	pubkeyString := hex.EncodeToString(keyDesc.RawKeyBytes)

	// Create a p2tr address.
	resp := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_TAPROOT_PUBKEY,
	})
	generatedAddr[resp.Address] = addressDetails{
		Balance: 200_000,
		Type:    walletrpc.AddressType_TAPROOT_PUBKEY,
	}

	// Create a p2wkh address.
	resp = alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
	})
	generatedAddr[resp.Address] = addressDetails{
		Balance: 300_000,
		Type:    walletrpc.AddressType_WITNESS_PUBKEY_HASH,
	}

	// Create a np2wkh address.
	resp = alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_NESTED_PUBKEY_HASH,
	})
	generatedAddr[resp.Address] = addressDetails{
		Balance: 400_000,
		Type: walletrpc.
			AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH,
	}

	for addr, addressDetail := range generatedAddr {
		alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
			Addr:             addr,
			Amount:           addressDetail.Balance,
			SpendUnconfirmed: true,
			TargetConf:       6,
		})
	}

	ht.MineBlocksAndAssertNumTxes(1, 3)

	// Get all the accounts except LND's custom accounts.
	addressLists := alice.RPC.ListAddresses(
		&walletrpc.ListAddressesRequest{},
	)

	foundAddresses := 0
	for _, addressList := range addressLists.AccountWithAddresses {
		addresses := addressList.Addresses
		derivationPath, err := lntest.ParseDerivationPath(
			addressList.DerivationPath,
		)
		require.NoError(ht, err)

		// Should not get an account with KeyFamily - 123.
		require.NotEqual(
			ht, uint32(keyLoc.KeyFamily), derivationPath[2],
		)

		for _, address := range addresses {
			if _, ok := generatedAddr[address.Address]; ok {
				addrDetails := generatedAddr[address.Address]
				require.Equal(
					ht, addrDetails.Balance,
					address.Balance,
				)
				require.Equal(
					ht, addrDetails.Type,
					addressList.AddressType,
				)
				foundAddresses++
			}
		}
	}

	require.Equal(ht, len(generatedAddr), foundAddresses)
	foundAddresses = 0

	// Get all the accounts (including LND's custom accounts).
	addressLists = alice.RPC.ListAddresses(
		&walletrpc.ListAddressesRequest{
			ShowCustomAccounts: true,
		},
	)

	for _, addressList := range addressLists.AccountWithAddresses {
		addresses := addressList.Addresses
		derivationPath, err := lntest.ParseDerivationPath(
			addressList.DerivationPath,
		)
		require.NoError(ht, err)

		for _, address := range addresses {
			// Check if the KeyFamily in derivation path is 123.
			if uint32(keyLoc.KeyFamily) == derivationPath[2] {
				// For LND's custom accounts, the address
				// represents the public key.
				pubkey := address.Address
				require.Equal(ht, pubkeyString, pubkey)
			} else if _, ok := generatedAddr[address.Address]; ok {
				addrDetails := generatedAddr[address.Address]
				require.Equal(
					ht, addrDetails.Balance,
					address.Balance,
				)
				require.Equal(
					ht, addrDetails.Type,
					addressList.AddressType,
				)
				foundAddresses++
			}
		}
	}

	require.Equal(ht, len(generatedAddr), foundAddresses)
}

func assertChannelConstraintsEqual(ht *lntest.HarnessTest,
	want, got *lnrpc.ChannelConstraints) {

	require.Equal(ht, want.CsvDelay, got.CsvDelay, "CsvDelay mismatched")

	require.Equal(ht, want.ChanReserveSat, got.ChanReserveSat,
		"ChanReserveSat mismatched")

	require.Equal(ht, want.DustLimitSat, got.DustLimitSat,
		"DustLimitSat mismatched")

	require.Equal(ht, want.MaxPendingAmtMsat, got.MaxPendingAmtMsat,
		"MaxPendingAmtMsat mismatched")

	require.Equal(ht, want.MinHtlcMsat, got.MinHtlcMsat,
		"MinHtlcMsat mismatched")

	require.Equal(ht, want.MaxAcceptedHtlcs, got.MaxAcceptedHtlcs,
		"MaxAcceptedHtlcs mismatched")
}

// testSignVerifyMessageWithAddr tests signing and also verifying a signature
// on a message with a provided address.
func testSignVerifyMessageWithAddr(ht *lntest.HarnessTest) {
	// Using different nodes to sign the message and verify the signature.
	alice, bob := ht.Alice, ht.Bob

	// Test an lnd wallet created P2WKH address.
	respAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
	})

	aliceMsg := []byte("alice msg")

	respSig := alice.RPC.SignMessageWithAddr(
		&walletrpc.SignMessageWithAddrRequest{
			Msg:  aliceMsg,
			Addr: respAddr.Address,
		},
	)

	respValid := bob.RPC.VerifyMessageWithAddr(
		&walletrpc.VerifyMessageWithAddrRequest{
			Msg:       aliceMsg,
			Signature: respSig.Signature,
			Addr:      respAddr.Address,
		},
	)

	require.True(ht, respValid.Valid, "alice's signature didn't validate")

	// Test an lnd wallet created NP2WKH address.
	respAddr = alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_NESTED_PUBKEY_HASH,
	})

	respSig = alice.RPC.SignMessageWithAddr(
		&walletrpc.SignMessageWithAddrRequest{
			Msg:  aliceMsg,
			Addr: respAddr.Address,
		},
	)

	respValid = bob.RPC.VerifyMessageWithAddr(
		&walletrpc.VerifyMessageWithAddrRequest{
			Msg:       aliceMsg,
			Signature: respSig.Signature,
			Addr:      respAddr.Address,
		},
	)

	require.True(ht, respValid.Valid, "alice's signature didn't validate")

	// Test an lnd wallet created P2TR address.
	respAddr = alice.RPC.NewAddress(&lnrpc.NewAddressRequest{
		Type: lnrpc.AddressType_TAPROOT_PUBKEY,
	})

	respSig = alice.RPC.SignMessageWithAddr(
		&walletrpc.SignMessageWithAddrRequest{
			Msg:  aliceMsg,
			Addr: respAddr.Address,
		},
	)

	respValid = bob.RPC.VerifyMessageWithAddr(
		&walletrpc.VerifyMessageWithAddrRequest{
			Msg:       aliceMsg,
			Signature: respSig.Signature,
			Addr:      respAddr.Address,
		},
	)

	require.True(ht, respValid.Valid, "alice's signature didn't validate")

	// Test verifying a signature with an external P2PKH address.
	// P2PKH address type is not supported by the lnd wallet therefore
	// using an external source (bitcoin-core) for address and
	// signature creation.
	externalMsg := []byte("external msg")
	externalAddr := "msS5c4VihSiJ64QzvMMEmWh6rYBnuWo2xH"

	// Base64 encoded signature created with bitcoin-core regtest.
	externalSig := "H5DqqM7Cc8xZnYBr7j3gD4XD+AuQsim9Un/IxBrrhBA7I9//" +
		"3exuQRg+u7HpwG65yobPsew6RMUteyuxyNkLF5E="

	respValid = alice.RPC.VerifyMessageWithAddr(
		&walletrpc.VerifyMessageWithAddrRequest{
			Msg:       externalMsg,
			Signature: externalSig,
			Addr:      externalAddr,
		},
	)

	require.True(ht, respValid.Valid, "external signature didn't validate")

	// Test verifying a signature with a different address which
	// initially was used to create the following signature.
	// externalAddr is a valid legacy P2PKH bitcoin address created
	// with bitcoin-core.
	externalAddr = "mugbg8CqFe9CbdrYjFTkMhmL3JxuEXkNbY"

	// Base64 encoded signature created with bitcoin-core regtest but with
	// the address msS5c4VihSiJ64QzvMMEmWh6rYBnuWo2xH.
	externalSig = "H5DqqM7Cc8xZnYBr7j3gD4XD+AuQsim9Un/IxBrrhBA7I9//" +
		"3exuQRg+u7HpwG65yobPsew6RMUteyuxyNkLF5E="

	respValid = alice.RPC.VerifyMessageWithAddr(
		&walletrpc.VerifyMessageWithAddrRequest{
			Msg:       externalMsg,
			Signature: externalSig,
			Addr:      externalAddr,
		},
	)

	require.False(ht, respValid.Valid, "external signature did validate")
}

// testNativeSQLNoMigration tests that nodes that have invoices would not start
// up with native SQL enabled, as we don't currently support migration of KV
// invoices to the new SQL schema.
func testNativeSQLNoMigration(ht *lntest.HarnessTest) {
	alice := ht.Alice

	// Make sure we run the test with SQLite or Postgres.
	if alice.Cfg.DBBackend != node.BackendSqlite &&
		alice.Cfg.DBBackend != node.BackendPostgres {

		ht.Skip("node not running with SQLite or Postgres")
	}

	// Skip the test if the node is already running with native SQL.
	if alice.Cfg.NativeSQL {
		ht.Skip("node already running with native SQL")
	}

	alice.RPC.AddInvoice(&lnrpc.Invoice{
		Value: 10_000,
	})

	alice.SetExtraArgs([]string{"--db.use-native-sql"})

	// Restart the node manually as we're really only interested in the
	// startup error.
	require.NoError(ht, alice.Stop())
	require.NoError(ht, alice.StartLndCmd(context.Background()))

	// We expect the node to fail to start up with native SQL enabled, as we
	// have an invoice in the KV store.
	require.Error(ht, alice.WaitForProcessExit())

	// Reset the extra args and restart alice.
	alice.SetExtraArgs(nil)
	require.NoError(ht, alice.Start(ht.Context()))
}

// testSendSelectedCoins tests that we're able to properly send the selected
// coins from the wallet to a single target address.
func testSendSelectedCoins(ht *lntest.HarnessTest) {
	// First, we'll make a new node, Alice who'll we'll use to test wallet
	// sweeping.
	alice := ht.NewNode("Alice", nil)

	// Next, we'll give Alice exactly 3 utxos of 1 BTC of different address
	// types.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)
	ht.FundCoinsNP2WKH(btcutil.SatoshiPerBitcoin, alice)
	ht.FundCoinsP2TR(btcutil.SatoshiPerBitcoin, alice)

	// Get all the utxos in the wallet and assert there are three.
	utxos := ht.AssertNumUTXOs(alice, 3)

	// Ensure that we can't send duplicate coins.
	//
	// Create duplciate outpoints.
	dupOutpoints := []*lnrpc.OutPoint{
		utxos[0].Outpoint,
		utxos[0].Outpoint,
	}

	// Send the duplicate outpoints and assert there's an error.
	err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:      ht.NewMinerAddress().String(),
		Outpoints: dupOutpoints,
	})
	require.ErrorContains(ht, err, "selected outpoints contain duplicate")

	// Send a selected coin with a specific amount.
	//
	// We'll send the first utxo with an amount of 0.5 BTC.
	amt := btcutil.Amount(0.5 * btcutil.SatoshiPerBitcoin)
	alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
		Addr: ht.NewMinerAddress().String(),
		Outpoints: []*lnrpc.OutPoint{
			utxos[0].Outpoint,
		},
		Amount: int64(amt),
	})

	// We expect to see the above tx in the mempool.
	tx := ht.GetNumTxsFromMempool(1)[0]

	// Assert the tx has the expected shape. It should have 1 input and 2
	// outputs - the input is the selected UTXO, and the outputs are the
	// specified amount and the change amount.
	require.Len(ht, tx.TxIn, 1)
	require.Len(ht, tx.TxOut, 2)

	// Check it's using the selected UTXO as input.
	require.Equal(ht, utxos[0].Outpoint.TxidStr,
		tx.TxIn[0].PreviousOutPoint.Hash.String())

	// Mine a block to confirm the above tx.
	ht.MineBlocksAndAssertNumTxes(1, 1)

	// We now test we can send the selected coins to a single target
	// address with `SendAll` flag.
	//
	// Get all the utxos in the wallet and assert there are three.
	utxos = ht.AssertNumUTXOs(alice, 3)

	// Select the first two coins and send them to a single target address.
	alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
		Addr: ht.NewMinerAddress().String(),
		Outpoints: []*lnrpc.OutPoint{
			utxos[0].Outpoint,
			utxos[1].Outpoint,
		},
		SendAll: true,
	})

	// We expect to see the above tx in the mempool.
	tx = ht.GetNumTxsFromMempool(1)[0]

	// Assert the tx has the expected shape. It should have 2 inputs and 1
	// output.
	require.Len(ht, tx.TxIn, 2)
	require.Len(ht, tx.TxOut, 1)

	// Check it's using the selected UTXOs as inputs.
	prevOutpoint1 := tx.TxIn[0].PreviousOutPoint.Hash.String()
	prevOutpoint2 := tx.TxIn[1].PreviousOutPoint.Hash.String()

	if prevOutpoint1 == utxos[0].Outpoint.TxidStr {
		require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint2)
	} else {
		require.Equal(ht, utxos[1].Outpoint.TxidStr, prevOutpoint1)
		require.Equal(ht, utxos[0].Outpoint.TxidStr, prevOutpoint2)
	}

	// Mine a block to confirm the above tx.
	ht.MineBlocksAndAssertNumTxes(1, 1)

	// Since the first two UTXOs have been sent to an address outside her
	// wallet, Alice should see a single UTXO now.
	ht.AssertNumUTXOs(alice, 1)
}

// testSendSelectedCoinsChannelReserve tests that if sending selected coins
// would violate the channel reserve requirement the RPC call will fail. It
// also checks that change outputs will be created automatically if `SendAll`
// flag is set.
func testSendSelectedCoinsChannelReserve(ht *lntest.HarnessTest) {
	chanAmt := btcutil.Amount(100_000)

	// Create a two-hop network: Alice -> Bob.
	//
	// NOTE: Alice will have one UTXO after the funding.
	cfg := []string{"--protocol.anchors"}
	cfgs := [][]string{cfg, cfg}
	_, nodes := ht.CreateSimpleNetwork(
		cfgs, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	alice := nodes[0]

	// Fund Alice one more UTXO.
	ht.FundCoins(btcutil.SatoshiPerBitcoin, alice)

	// Get all the utxos in the wallet and assert there are two:
	// - one from the above `FundCoins`.
	// - one from the change output after the channel funding.
	utxos := ht.AssertNumUTXOs(alice, 2)

	// Get all the outpoints.
	outpoints := []*lnrpc.OutPoint{
		utxos[0].Outpoint,
		utxos[1].Outpoint,
	}

	// Calculate the total amount of the two UTXOs.
	totalAmt := utxos[0].AmountSat + utxos[1].AmountSat

	// Send the total amount of the two UTXOs and expect an error since
	// fees cannot be covered.
	err := alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:      ht.NewMinerAddress().String(),
		Outpoints: outpoints,
		Amount:    totalAmt,
	})
	require.ErrorContains(ht, err, "insufficient funds available to "+
		"construct transaction")

	// Get the required reserve amount.
	resp := alice.RPC.RequiredReserve(
		&walletrpc.RequiredReserveRequest{
			AdditionalPublicChannels: 1,
		},
	)

	// Calculate an amount which, after sending it, would violate the
	// channel reserve requirement.
	amt := totalAmt - resp.RequiredReserve

	// Send the amount and expect an error since the channel reserve cannot
	// be covered.
	err = alice.RPC.SendCoinsAssertErr(&lnrpc.SendCoinsRequest{
		Addr:      ht.NewMinerAddress().String(),
		Outpoints: outpoints,
		Amount:    amt,
	})
	require.ErrorContains(ht, err, "reserved wallet balance invalidated")

	// Finally, check that we can send all the selected coins with the help
	// of the `SendAll` flag as it will automatically handle reserving
	// change outputs based on the channel reserve requirements.
	alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{
		Addr:      ht.NewMinerAddress().String(),
		Outpoints: outpoints,
		SendAll:   true,
	})

	// We expect to see the above tx in the mempool.
	tx := ht.GetNumTxsFromMempool(1)[0]

	// Assert the tx has the expected shape. It should have 2 inputs and 2
	// outputs.
	require.Len(ht, tx.TxIn, 2)
	require.Len(ht, tx.TxOut, 2)

	// Mine a block to confirm the above tx.
	ht.MineBlocksAndAssertNumTxes(1, 1)

	// Alice should have one reserved UTXO now.
	ht.AssertNumUTXOs(alice, 1)
}
