package sweepbatcher

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"sync"
	"testing"

	"github.com/btcsuite/btcd/blockchain"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btclog/v2"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightninglabs/loop/test"
	"github.com/lightninglabs/loop/utils"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// mockPresignedHelper implements PresignedHelper interface and stores arguments
// passed in its methods to validate correctness of function publishPresigned.
type mockPresignedHelper struct {
	// onlineOutpoints specifies which outpoints are capable of
	// participating in presigning.
	onlineOutpoints map[wire.OutPoint]bool

	// changeOutputs is a map of change outputs for a given primary deposit.
	changeOutputs map[wire.OutPoint]*wire.TxOut

	// presignedBatches is the collection of presigned batches. The key is
	// primarySweepID.
	presignedBatches map[wire.OutPoint][]*wire.MsgTx

	// mu should be hold by all the public methods of this type.
	mu sync.Mutex

	// cleanupCalled is a channel where an element is sent every time
	// CleanupTransactions is called.
	cleanupCalled chan struct{}
}

// newMockPresignedHelper returns new instance of mockPresignedHelper.
func newMockPresignedHelper() *mockPresignedHelper {
	return &mockPresignedHelper{
		onlineOutpoints:  make(map[wire.OutPoint]bool),
		changeOutputs:    make(map[wire.OutPoint]*wire.TxOut),
		presignedBatches: make(map[wire.OutPoint][]*wire.MsgTx),
		cleanupCalled:    make(chan struct{}),
	}
}

// SetOutpointOnline changes the online status of an outpoint.
func (h *mockPresignedHelper) SetOutpointOnline(op wire.OutPoint, online bool) {
	h.mu.Lock()
	defer h.mu.Unlock()

	h.onlineOutpoints[op] = online
}

// setChangeForPrimaryDeposit sets the change output of a primary deposit sweep.
func (h *mockPresignedHelper) setChangeForPrimaryDeposit(op wire.OutPoint,
	change *wire.TxOut) {

	h.mu.Lock()
	defer h.mu.Unlock()

	h.changeOutputs[op] = change
}

// offlineInputs returns inputs of a tx which are offline.
func (h *mockPresignedHelper) offlineInputs(tx *wire.MsgTx) []wire.OutPoint {
	offline := make([]wire.OutPoint, 0, len(tx.TxIn))
	for _, txIn := range tx.TxIn {
		if !h.onlineOutpoints[txIn.PreviousOutPoint] {
			offline = append(offline, txIn.PreviousOutPoint)
		}
	}

	return offline
}

// sign signs the transaction.
func (h *mockPresignedHelper) sign(tx *wire.MsgTx) {
	// Sign all the inputs.
	for i := range tx.TxIn {
		tx.TxIn[i].Witness = wire.TxWitness{
			make([]byte, 64),
		}
	}
}

// getTxFeerate returns fee rate of a transaction.
func (h *mockPresignedHelper) getTxFeerate(tx *wire.MsgTx,
	inputAmt btcutil.Amount) chainfee.SatPerKWeight {

	// "Sign" tx's copy to assess the weight.
	tx2 := tx.Copy()
	h.sign(tx2)
	weight := lntypes.WeightUnit(
		blockchain.GetTransactionWeight(btcutil.NewTx(tx2)),
	)
	fee := inputAmt - btcutil.Amount(tx.TxOut[0].Value)

	return chainfee.NewSatPerKWeight(fee, weight)
}

// DestPkScript returns destination pkScript used in presigned tx sweeping
// these inputs.
func (h *mockPresignedHelper) DestPkScript(ctx context.Context,
	primarySweepID wire.OutPoint) ([]byte, error) {

	h.mu.Lock()
	defer h.mu.Unlock()

	for _, tx := range h.presignedBatches[primarySweepID] {
		return tx.TxOut[0].PkScript, nil
	}

	return nil, fmt.Errorf("tx with primarySweepID %v not found",
		primarySweepID)
}

// SignTx tries to sign the transaction. If all the inputs are online, it signs
// the exact transaction passed and adds it to presignedBatches. Otherwise, it
// looks for a transaction in presignedBatches satisfying the criteria.
func (h *mockPresignedHelper) SignTx(ctx context.Context,
	primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
	minRelayFee, feeRate chainfee.SatPerKWeight,
	loadOnly bool) (*wire.MsgTx, error) {

	h.mu.Lock()
	defer h.mu.Unlock()

	if feeRate < minRelayFee {
		return nil, fmt.Errorf("feeRate (%v) is below minRelayFee (%v)",
			feeRate, minRelayFee)
	}

	if !hasInput(tx, primarySweepID) {
		return nil, fmt.Errorf("primarySweepID %v not in tx",
			primarySweepID)
	}

	// If all the inputs are online and loadOnly is not set, sign this exact
	// transaction.
	if offline := h.offlineInputs(tx); len(offline) == 0 && !loadOnly {
		tx = tx.Copy()
		h.sign(tx)

		// Add to the collection.
		h.presignedBatches[primarySweepID] = append(
			h.presignedBatches[primarySweepID], tx,
		)

		return tx, nil
	}

	// Try to find a transaction in the collection satisfying all the
	// criteria of PresignedHelper.SignTx. If there are many such
	// transactions, select a transaction with feerate which is the closest
	// to the feerate of the input tx.
	var (
		bestTx              *wire.MsgTx
		bestFeerateDistance chainfee.SatPerKWeight
	)

	for _, candidate := range h.presignedBatches[primarySweepID] {
		err := CheckSignedTx(tx, candidate, inputAmt, minRelayFee)
		if err != nil {
			continue
		}

		feeRateDistance := h.getTxFeerate(candidate, inputAmt) - feeRate
		if feeRateDistance < 0 {
			feeRateDistance = -feeRateDistance
		}

		if bestTx == nil || feeRateDistance < bestFeerateDistance {
			bestTx = candidate
			bestFeerateDistance = feeRateDistance
		}
	}

	if bestTx == nil {
		return nil, fmt.Errorf("some outpoint is offline and no " +
			"suitable presigned tx found")
	}

	return bestTx.Copy(), nil
}

// CleanupTransactions removes all transactions related to any of the outpoints.
func (h *mockPresignedHelper) CleanupTransactions(ctx context.Context,
	inputs []wire.OutPoint) error {

	h.mu.Lock()
	defer h.mu.Unlock()

	for _, primarySweepID := range inputs {
		delete(h.presignedBatches, primarySweepID)
	}

	h.cleanupCalled <- struct{}{}

	return nil
}

// sweepTimeout is swap timeout block height used in tests of presigned mode.
const sweepTimeout = 1000

// FetchSweep returns blank SweepInfo.
// This method implements SweepFetcher interface.
func (h *mockPresignedHelper) FetchSweep(_ context.Context,
	_ lntypes.Hash, utxo wire.OutPoint) (*SweepInfo, error) {

	h.mu.Lock()
	defer h.mu.Unlock()

	// Find IsPresigned.
	_, isPresigned := h.onlineOutpoints[utxo]

	// Find change.
	change := h.changeOutputs[utxo]

	return &SweepInfo{
		// Set Timeout to prevent warning messages about timeout=0.
		Timeout: sweepTimeout,

		IsPresigned: isPresigned,

		HTLC: swap.Htlc{
			PkScript: []byte{10, 11, 12},
		},
		Change: change,
	}, nil
}

// testPresigned_forgotten_presign checks that adding sweeps causes the batcher
// to fail if the sweeps were not presigned with PresignSweepsGroup. In addition
// to that it checks that PresignSweepsGroup fails if the outpoint is offline.
func testPresigned_forgotten_presign(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	lnd := test.NewMockLnd()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper))

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}

	// This should fail, because the input is offline.
	presignedHelper.SetOutpointOnline(op1, false)
	err := batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.Error(t, err)
	require.ErrorContains(t, err, "offline")

	// Make sure that the batcher crashes if AddSweep is called before
	// PresignSweepsGroup even if the input is online.
	presignedHelper.SetOutpointOnline(op1, true)
	err = batcher.AddSweep(ctx, &sweepReq1)
	require.ErrorContains(t, err, "were not presigned")
}

// testPresigned_input1_offline_then_input2 tests presigned mode for the
// following scenario: first input is added, then goes offline, then feerate
// grows, one of presigned transactions is published, and then another online
// input is added and is assigned to another batch.
func testPresigned_input1_offline_then_input2(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const (
		feeRateLow    = chainfee.SatPerKWeight(10_000)
		feeRateMedium = chainfee.SatPerKWeight(30_000)
		feeRateHigh   = chainfee.SatPerKWeight(31_000)
	)

	currentFeeRate := feeRateLow
	setFeeRate := func(feeRate chainfee.SatPerKWeight) {
		currentFeeRate = feeRate
	}
	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return currentFeeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper))
	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	setFeeRate(feeRateLow)

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}

	// Enable the input and presign.
	presignedHelper.SetOutpointOnline(op1, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Increase fee rate and turn off the input, so it can't sign updated
	// tx. The feerate is close to the feerate of one of presigned txs.
	setFeeRate(feeRateMedium)
	presignedHelper.SetOutpointOnline(op1, false)

	// Deliver sweep request to batcher.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 1)
	require.Len(t, tx.TxOut, 1)
	require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
	require.Equal(t, int64(988618), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)

	// Make sure the fee rate is feeRateMedium.
	batch := getOnlyBatch(t, ctx, batcher)
	var (
		numSweeps     int
		cachedFeeRate chainfee.SatPerKWeight
	)
	batch.testRunInEventLoop(ctx, func() {
		numSweeps = len(batch.sweeps)
		cachedFeeRate = batch.rbfCache.FeeRate
	})
	require.Equal(t, 1, numSweeps)
	require.Equal(t, feeRateMedium, cachedFeeRate)

	// Raise feerate and trigger new publishing. The tx should be the same.
	setFeeRate(feeRateHigh)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
	require.NoError(t, lnd.NotifyHeight(601))

	tx2 := <-lnd.TxPublishChannel
	require.Equal(t, tx.TxHash(), tx2.TxHash())

	// Now add another input. It is online, but the first input is still
	// offline, so another input should go to another batch.
	swapHash2 := lntypes.Hash{2, 2, 2}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	sweepReq2 := SweepRequest{
		SwapHash: swapHash2,
		Inputs: []Input{{
			Value:    2_000_000,
			Outpoint: op2,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op2, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Deliver sweep request to batcher.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	batch2 := <-lnd.TxPublishChannel
	require.Len(t, batch2.TxIn, 1)
	require.Len(t, batch2.TxOut, 1)
	require.Equal(t, op2, batch2.TxIn[0].PreviousOutPoint)
	require.Equal(t, int64(1987724), batch2.TxOut[0].Value)
	require.Equal(t, batchPkScript, batch2.TxOut[0].PkScript)

	// Now confirm the first batch. Make sure its presigned transactions
	// were removed, but not the transactions of the second batch.
	presignedSize1 := len(presignedHelper.presignedBatches)

	tx2hash := tx2.TxHash()
	spendDetail := &chainntnfs.SpendDetail{
		SpentOutPoint:     &op1,
		SpendingTx:        tx2,
		SpenderTxHash:     &tx2hash,
		SpenderInputIndex: 0,
		SpendingHeight:    601,
	}
	lnd.SpendChannel <- spendDetail
	<-lnd.RegisterConfChannel
	require.NoError(t, lnd.NotifyHeight(604))
	lnd.ConfChannel <- &chainntnfs.TxConfirmation{
		Tx: tx2,
	}

	<-presignedHelper.cleanupCalled

	presignedSize2 := len(presignedHelper.presignedBatches)
	require.Greater(t, presignedSize2, 0)
	require.Greater(t, presignedSize1, presignedSize2)

	// Make sure we still have presigned transactions for the second batch.
	presignedHelper.SetOutpointOnline(op2, false)
	const loadOnly = true
	_, err = presignedHelper.SignTx(
		ctx, op2, batch2, 2_000_000, chainfee.FeePerKwFloor,
		chainfee.FeePerKwFloor, loadOnly,
	)
	require.NoError(t, err)
}

// testPresigned_min_relay_fee tests that online and presigned transactions
// comply with min_relay_fee.
func testPresigned_min_relay_fee(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	lnd := test.NewMockLnd()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const inputAmt = 1_000_000

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.FeePerKwFloor, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper))
	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Set high min_relay_fee.
	lnd.SetMinRelayFee(252)

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    inputAmt,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}

	// Enable the input and presign.
	presignedHelper.SetOutpointOnline(op1, true)
	err := batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: inputAmt}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Deliver sweep request to batcher.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	gotFeeRate := presignedHelper.getTxFeerate(tx, inputAmt)
	require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)

	// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
	// The only difference of tx2 is a higher lock_time.
	lnd.SetMinRelayFee(150)
	require.NoError(t, lnd.NotifyHeight(601))
	tx2 := <-lnd.TxPublishChannel
	require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)
	gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
	require.Equal(t, chainfee.SatPerKWeight(253), gotFeeRate)
	require.Equal(t, uint32(601), tx2.LockTime)

	// Set a higher min_relay_fee, turn off the client and try presigned tx.
	lnd.SetMinRelayFee(500)
	presignedHelper.SetOutpointOnline(op1, false)

	// Check fee rate of the presigned tx broadcasted.
	require.NoError(t, lnd.NotifyHeight(602))
	tx = <-lnd.TxPublishChannel
	gotFeeRate = presignedHelper.getTxFeerate(tx, inputAmt)
	require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
	// LockTime of a presigned tx is 0.
	require.Equal(t, uint32(0), tx.LockTime)

	// Now decrease min_relay_fee and make sure fee rate doesn't decrease.
	// It should re-broadcast the same presigned tx.
	lnd.SetMinRelayFee(450)
	require.NoError(t, lnd.NotifyHeight(603))
	tx2 = <-lnd.TxPublishChannel
	require.Equal(t, tx.TxHash(), tx2.TxHash())
	gotFeeRate = presignedHelper.getTxFeerate(tx2, inputAmt)
	require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
	// LockTime of a presigned tx is 0.
	require.Equal(t, uint32(0), tx2.LockTime)

	// Even if the client is back online, fee rate doesn't decrease.
	presignedHelper.SetOutpointOnline(op1, true)
	require.NoError(t, lnd.NotifyHeight(604))
	tx3 := <-lnd.TxPublishChannel
	require.Equal(t, tx2.TxOut[0].Value, tx3.TxOut[0].Value)
	gotFeeRate = presignedHelper.getTxFeerate(tx3, inputAmt)
	require.Equal(t, chainfee.SatPerKWeight(520), gotFeeRate)
	require.Equal(t, uint32(604), tx3.LockTime)
}

// testPresigned_two_inputs_one_goes_offline tests presigned mode for the
// following scenario: two online inputs are added, then one of them goes
// offline, then feerate grows and a presigned transaction is used.
func testPresigned_two_inputs_one_goes_offline(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const (
		feeRateLow    = chainfee.SatPerKWeight(10_000)
		feeRateMedium = chainfee.SatPerKWeight(30_000)
		feeRateHigh   = chainfee.SatPerKWeight(40_000)
	)

	currentFeeRate := feeRateLow
	setFeeRate := func(feeRate chainfee.SatPerKWeight) {
		currentFeeRate = feeRate
	}
	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return currentFeeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	setFeeRate(feeRateLow)

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op1, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Add second sweep.
	swapHash2 := lntypes.Hash{2, 2, 2}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	sweepReq2 := SweepRequest{
		SwapHash: swapHash2,
		Inputs: []Input{{
			Value:    2_000_000,
			Outpoint: op2,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op2, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 2)
	require.Len(t, tx.TxOut, 1)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(2993740), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)

	// Now turn off the second input, raise feerate and trigger new
	// publishing. The feerate is close to one of the presigned feerates,
	// so this should result in RBF.
	presignedHelper.SetOutpointOnline(op2, false)
	setFeeRate(feeRateMedium)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))
	require.NoError(t, lnd.NotifyHeight(601))

	tx2 := <-lnd.TxPublishChannel
	require.NotEqual(t, tx.TxHash(), tx2.TxHash())
	require.Len(t, tx2.TxIn, 2)
	require.Len(t, tx2.TxOut, 1)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(2982007), tx2.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx2.TxOut[0].PkScript)
}

// testPresigned_first_publish_fails tests presigned mode for the following
// scenario: one input is added and goes offline, feerate grows a transaction is
// attempted to be published, but fails. Then the input goes online and is
// published being signed online.
func testPresigned_first_publish_fails(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const (
		feeRateLow    = chainfee.SatPerKWeight(10_000)
		feeRateMedium = chainfee.SatPerKWeight(30_000)
		feeRateHigh   = chainfee.SatPerKWeight(40_000)
	)

	currentFeeRate := feeRateLow
	setFeeRate := func(feeRate chainfee.SatPerKWeight) {
		currentFeeRate = feeRate
	}
	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return currentFeeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	setFeeRate(feeRateLow)

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op1, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	presignedHelper.SetOutpointOnline(op1, false)

	// Make sure that publish attempt fails.
	lnd.PublishHandler = func(ctx context.Context, tx *wire.MsgTx,
		label string) error {

		return fmt.Errorf("test error")
	}

	// Add the sweep, triggering the publish attempt.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Replace the logger in the batch with wrappedLogger to watch messages.
	batch := getOnlyBatch(t, ctx, batcher)
	testLogger := &wrappedLogger{
		Logger: batch.log(),
	}
	batch.setLog(testLogger)

	// Trigger another publish attempt in case the publish error was logged
	// before we installed the logger watcher.
	require.NoError(t, lnd.NotifyHeight(601))

	// Wait for batcher to log the publish error. It is logged with
	// publishErrorHandler, so the format is "%s: %v".
	require.EventuallyWithT(t, func(c *assert.CollectT) {
		testLogger.mu.Lock()
		defer testLogger.mu.Unlock()

		assert.Contains(c, testLogger.warnMessages, "%s: %v")
	}, test.Timeout, eventuallyCheckFrequency)

	// Now turn on the first input, raise feerate and trigger new
	// publishing, which should succeed.
	lnd.PublishHandler = nil
	setFeeRate(feeRateMedium)
	presignedHelper.SetOutpointOnline(op1, true)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))
	require.NoError(t, lnd.NotifyHeight(602))

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 1)
	require.Len(t, tx.TxOut, 1)
	require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
	require.Equal(t, int64(988120), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
}

// testPresigned_locktime tests presigned mode for the following scenario: one
// input is added and goes offline, feerate grows, but this is constrainted by
// locktime logic, so the published transaction has medium feerate (maximum
// feerate among transactions without locktime protection). Then blocks are
// mined and a transaction with a higher feerate is published.
func testPresigned_locktime(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const (
		feeRateLow  = chainfee.SatPerKWeight(10_000)
		feeRateHigh = chainfee.SatPerKWeight(10_000_000)
	)

	currentFeeRate := feeRateLow
	setFeeRate := func(feeRate chainfee.SatPerKWeight) {
		currentFeeRate = feeRate
	}
	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return currentFeeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	setFeeRate(feeRateLow)

	// Create the first sweep.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op1, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op1, Value: 1_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	presignedHelper.SetOutpointOnline(op1, false)

	setFeeRate(feeRateHigh)

	// Add the sweep, triggering the publish attempt.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 1)
	require.Len(t, tx.TxOut, 1)
	require.Equal(t, op1, tx.TxIn[0].PreviousOutPoint)
	require.Equal(t, int64(966015), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)

	// Mine blocks to overcome the locktime constraint.
	require.NoError(t, lnd.NotifyHeight(950))

	tx2 := <-lnd.TxPublishChannel
	require.Equal(t, int64(824648), tx2.TxOut[0].Value)
}

// testPresigned_presigned_group tests passing multiple sweeps to the method
// PresignSweepsGroup.
func testPresigned_presigned_group(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create a swap of two sweeps.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
		{
			Outpoint: op2,
			Value:    2_000_000,
		},
	}

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)
	presignedHelper.SetOutpointOnline(op2, false)

	// An attempt to presign must fail.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, nil,
	)
	require.ErrorContains(t, err, "some outpoint is offline")

	// Enable both outpoints.
	presignedHelper.SetOutpointOnline(op2, true)

	// An attempt to presign must succeed.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Add the sweep, triggering the publish attempt.
	require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	}))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 2)
	require.Len(t, tx.TxOut, 1)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(2993740), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)

	// Add another group of sweeps.
	swapHash2 := lntypes.Hash{2, 2, 2}
	op3 := wire.OutPoint{
		Hash:  chainhash.Hash{3, 3},
		Index: 3,
	}
	op4 := wire.OutPoint{
		Hash:  chainhash.Hash{4, 4},
		Index: 4,
	}
	group2 := []Input{
		{
			Outpoint: op3,
			Value:    3_000_000,
		},
		{
			Outpoint: op4,
			Value:    4_000_000,
		},
	}
	presignedHelper.SetOutpointOnline(op3, true)
	presignedHelper.SetOutpointOnline(op4, true)

	// An attempt to presign must succeed.
	err = batcher.PresignSweepsGroup(
		ctx, group2, sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Add the sweep. It should go to the same batch.
	require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash2,
		Inputs:   group2,
		Notifier: &dummyNotifier,
	}))

	// Mine a blocks to trigger republishing.
	require.NoError(t, lnd.NotifyHeight(601))

	tx = <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 4)
	require.Len(t, tx.TxOut, 1)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2, op3, op4},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
			tx.TxIn[2].PreviousOutPoint,
			tx.TxIn[3].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(9989140), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)

	// Turn off one of existing outpoints and add another group.
	presignedHelper.SetOutpointOnline(op1, false)

	swapHash3 := lntypes.Hash{3, 3, 3}
	op5 := wire.OutPoint{
		Hash:  chainhash.Hash{5, 5},
		Index: 5,
	}
	op6 := wire.OutPoint{
		Hash:  chainhash.Hash{6, 6},
		Index: 6,
	}
	group3 := []Input{
		{
			Outpoint: op5,
			Value:    5_000_000,
		},
		{
			Outpoint: op6,
			Value:    6_000_000,
		},
	}
	presignedHelper.SetOutpointOnline(op5, true)
	presignedHelper.SetOutpointOnline(op6, true)

	// An attempt to presign must succeed.
	err = batcher.PresignSweepsGroup(
		ctx, group3, sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)

	// Add the sweep. It should go to the same batch.
	require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash3,
		Inputs:   group3,
		Notifier: &dummyNotifier,
	}))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	tx = <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 2)
	require.Len(t, tx.TxOut, 1)
	require.ElementsMatch(
		t, []wire.OutPoint{op5, op6},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(10993740), tx.TxOut[0].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
}

// testPresigned_presigned_group_with_change tests passing multiple sweeps to
// the method PresignSweepsGroup. It tests that a change output of a primary
// deposit sweep is properly added to the presigned transaction.
func testPresigned_presigned_group_with_change(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create a swap of two sweeps.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
		{
			Outpoint: op2,
			Value:    2_000_000,
		},
	}
	change := &wire.TxOut{
		Value:    500_000,
		PkScript: []byte{0xaf, 0xfe},
	}

	presignedHelper.setChangeForPrimaryDeposit(op1, change)

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)
	presignedHelper.SetOutpointOnline(op2, true)

	// An attempt to presign shouldn't fail.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.NoError(t, err)

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.NoError(t, err)

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 2)
	require.Len(t, tx.TxOut, 2)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(2_493_300), tx.TxOut[0].Value)
	require.Equal(t, change.Value, tx.TxOut[1].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
	require.Equal(t, change.PkScript, tx.TxOut[1].PkScript)

	// Mine a blocks to trigger republishing.
	require.NoError(t, lnd.NotifyHeight(601))
}

// testPresigned_fee_portion_with_change ensures that the fee portion reported
// to clients accounts for change outputs in the presigned transaction. It also
// is a regression test for feerate overestimation when tx is published.
func testPresigned_fee_portion_with_change(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	swapHash := lntypes.Hash{2, 2, 2}
	op := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	group := []Input{
		{
			Outpoint: op,
			Value:    1_000_000,
		},
	}
	change := &wire.TxOut{
		Value:    250_000,
		PkScript: []byte{0xca, 0xfe},
	}

	presignedHelper.setChangeForPrimaryDeposit(op, change)
	presignedHelper.SetOutpointOnline(op, true)

	require.NoError(t, batcher.PresignSweepsGroup(
		ctx, group, sweepTimeout, destAddr, change,
	))

	spendChan := make(chan *SpendDetail, 1)
	confChan := make(chan *ConfDetail, 1)
	notifier := &SpendNotifier{
		SpendChan:    spendChan,
		SpendErrChan: make(chan error, 1),
		ConfChan:     confChan,
		ConfErrChan:  make(chan error, 1),
		QuitChan:     make(chan bool, 1),
	}

	require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash,
		Inputs:   group,
		Notifier: notifier,
	}))

	spendReg := <-lnd.RegisterSpendChannel
	require.NotNil(t, spendReg)
	require.NotNil(t, spendReg.Outpoint)
	require.Equal(t, op, *spendReg.Outpoint)

	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 1)
	require.Len(t, tx.TxOut, 2)

	// Mine a blocks to trigger republishing.
	require.NoError(t, lnd.NotifyHeight(601))

	// Make sure it is the same tx.
	tx2 := <-lnd.TxPublishChannel
	require.Len(t, tx2.TxOut, len(tx.TxOut))
	require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value)

	var (
		outputSum   int64
		foundChange bool
	)
	for _, txOut := range tx.TxOut {
		outputSum += txOut.Value
		if txOut.Value != change.Value {
			continue
		}

		if !bytes.Equal(txOut.PkScript, change.PkScript) {
			continue
		}

		foundChange = true
	}

	require.True(t, foundChange)

	totalInput := int64(group[0].Value)
	require.LessOrEqual(t, outputSum, totalInput)

	expectedFee := btcutil.Amount(totalInput - outputSum)
	require.Greater(t, expectedFee, btcutil.Amount(0))

	txHash := tx.TxHash()
	spendDetail := &chainntnfs.SpendDetail{
		SpentOutPoint:     &op,
		SpendingTx:        tx,
		SpenderTxHash:     &txHash,
		SpenderInputIndex: 0,
		SpendingHeight:    spendReg.HeightHint + 1,
	}
	lnd.SpendChannel <- spendDetail

	spend := <-spendChan
	require.Equal(t, expectedFee, spend.OnChainFeePortion)

	confReg := <-lnd.RegisterConfChannel
	require.True(t, bytes.Equal(tx.TxOut[0].PkScript, confReg.PkScript) ||
		bytes.Equal(tx.TxOut[1].PkScript, confReg.PkScript))

	require.NoError(
		t, lnd.NotifyHeight(spendReg.HeightHint+batchConfHeight+1),
	)
	lnd.ConfChannel <- &chainntnfs.TxConfirmation{Tx: tx}

	require.Eventually(t, func() bool {
		select {
		case <-presignedHelper.cleanupCalled:
			return true
		default:
			return false
		}
	}, test.Timeout, eventuallyCheckFrequency)

	conf := <-confChan
	require.Equal(t, expectedFee, conf.OnChainFeePortion)
}

// testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to
// the method PresignSweepsGroup. It tests that a change output of a primary
// deposit sweep is properly added to the presigned transaction.
func testPresigned_presigned_group_with_identical_change_pkscript(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create two swaps of a single sweep
	swapHash1 := lntypes.Hash{1, 1, 1}
	swapHash2 := lntypes.Hash{2, 2, 2}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
	}
	change1 := &wire.TxOut{
		Value:    500_000,
		PkScript: []byte{0xaf, 0xfe},
	}
	group2 := []Input{
		{
			Outpoint: op2,
			Value:    2_000_000,
		},
	}
	change2 := &wire.TxOut{
		Value:    600_000,
		PkScript: []byte{0xaf, 0xfe},
	}

	presignedHelper.setChangeForPrimaryDeposit(op1, change1)
	presignedHelper.setChangeForPrimaryDeposit(op2, change2)

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)
	presignedHelper.SetOutpointOnline(op2, true)

	// An attempt to presign group1 shouldn't fail.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change1,
	)
	require.NoError(t, err)

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.NoError(t, err)

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// An attempt to presign group2 shouldn't fail.
	err = batcher.PresignSweepsGroup(
		ctx, group2, sweepTimeout, destAddr, change2,
	)
	require.NoError(t, err)

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash2,
		Inputs:   group2,
		Notifier: &dummyNotifier,
	})
	require.NoError(t, err)

	// Wait for a transactions to be published.
	tx := <-lnd.TxPublishChannel
	require.Len(t, tx.TxIn, 2)
	require.Len(t, tx.TxOut, 2)
	require.ElementsMatch(
		t, []wire.OutPoint{op1, op2},
		[]wire.OutPoint{
			tx.TxIn[0].PreviousOutPoint,
			tx.TxIn[1].PreviousOutPoint,
		},
	)
	require.Equal(t, int64(1_893_300), tx.TxOut[0].Value)
	require.Equal(t, change1.Value+change2.Value, tx.TxOut[1].Value)
	require.Equal(t, batchPkScript, tx.TxOut[0].PkScript)
	require.Equal(t, change1.PkScript, tx.TxOut[1].PkScript)

	// Mine a blocks to trigger republishing.
	require.NoError(t, lnd.NotifyHeight(601))
}

// testPresigned_presigned_group_with_dust_main_output passes a dust main output
// and a change output to PresignSweepsGroup. It will fail because of the dust
// main output. Note that the min relay fee is set low enough to pass the
// clampBatchFee check, so the error is not related to the fee rate.
func testPresigned_presigned_group_with_dust_main_output(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(100_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create a swap of two sweeps.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	inputValue := int64(1_000_000)
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
	}
	dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
	mainOutput := dustLimit

	// Let's solve an equation of what the fee should be so it is 20%
	// (clamped fee) of fee (itself) + main output.
	// fee / (fee+main) = 0.2
	// fee = 0.2 fee + 0.2 main
	// 0.8 fee = 0.2 main
	// fee = 0.25 main
	clampedFee := mainOutput / 4

	// Set min relay fee low enough to pass the clampBatchFee check.
	lnd.SetMinRelayFee(166)

	change := &wire.TxOut{
		// If "+1" is removed, the group would be added successfully.
		Value:    inputValue - dustLimit - clampedFee + 1,
		PkScript: []byte{0xaf, 0xfe},
	}

	presignedHelper.setChangeForPrimaryDeposit(op1, change)

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)

	// An attempt to presign must fail.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.EqualError(t, err, "failed to construct unsigned tx for "+
		"feeRate 166 sat/kw: batch amount 0.01000000 BTC is < the "+
		"sum of change outputs 0.00999634 BTC plus fee "+
		"0.00000073 BTC and dust limit 0.00000294 BTC")

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.ErrorContains(t, err, "were not presigned")

	// Now let's verify that we found the edge value correctly.
	change.Value--

	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.NoError(t, err)

	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.NoError(t, err)

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel
}

// testPresigned_presigned_group_with_dust_below_relay_fee passes a tx with a
// dust main output and one change output to PresignSweepsGroup. The tx fee rate
// is below the min relay fee, hence clampBatchFee will return an error.
func testPresigned_presigned_group_with_dust_below_relay_fee(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	batchPkScript, err := txscript.PayToAddrScript(destAddr)
	require.NoError(t, err)

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(100_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create a swap of two sweeps.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	inputValue := int64(1_000_000)
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
	}
	dustLimit := int64(utils.DustLimitForPkScript(batchPkScript))
	change := &wire.TxOut{
		// Note that there is no space for fee.
		Value:    inputValue - dustLimit,
		PkScript: []byte{0xaf, 0xfe},
	}

	presignedHelper.setChangeForPrimaryDeposit(op1, change)

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)

	// An attempt to presign must fail.
	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.EqualError(t, err, "failed to construct unsigned tx for "+
		"feeRate 253 sat/kw: failed to clamp batch fee: clamped "+
		"fee rate 132 sat/kw is less than minimum relay fee 253 sat/kw")

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.ErrorContains(t, err, "were not presigned")
}

// testPresigned_presigned_group_with_dust_change tests passing multiple sweeps
// to the method PresignSweepsGroup. It tests that a dust change output of a
// primary deposit sweep is rejected by PresignSweepsGroup and AddSweep.
func testPresigned_presigned_group_with_dust_change(t *testing.T,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return chainfee.SatPerKWeight(10_000), nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	// Create a swap of two sweeps.
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}
	group1 := []Input{
		{
			Outpoint: op1,
			Value:    1_000_000,
		},
		{
			Outpoint: op2,
			Value:    2_000_000,
		},
	}
	changePkScript := []byte{0xaf, 0xfe}
	dustLimit := utils.DustLimitForPkScript(changePkScript)
	change := &wire.TxOut{
		// If "-1" is removed, the group would be added successfully.
		Value:    int64(dustLimit - 1),
		PkScript: changePkScript,
	}

	presignedHelper.setChangeForPrimaryDeposit(op1, change)

	// Enable only one of the sweeps.
	presignedHelper.SetOutpointOnline(op1, true)
	presignedHelper.SetOutpointOnline(op2, true)

	// An attempt to presign must fail.
	err := batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.EqualError(t, err, "failed to construct unsigned tx for "+
		"feeRate 253 sat/kw: output 0.00000476 BTC is below dust "+
		"limit 0.00000477 BTC")

	// Add the sweep, triggering the publishing attempt.
	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.ErrorContains(t, err, "were not presigned")

	// Now let's verify that we found the edge value correctly.
	change.Value++

	err = batcher.PresignSweepsGroup(
		ctx, group1, sweepTimeout, destAddr, change,
	)
	require.NoError(t, err)

	err = batcher.AddSweep(ctx, &SweepRequest{
		SwapHash: swapHash1,
		Inputs:   group1,
		Notifier: &dummyNotifier,
	})
	require.NoError(t, err)
}

// wrappedStoreWithPresignedFlag wraps a SweepFetcher store adding IsPresigned
// flag to the returned sweeps, taking it from mockPresignedHelper.
type wrappedStoreWithPresignedFlag struct {
	backend SweepFetcher
	helper  *mockPresignedHelper
}

// // FetchSweep returns details of the sweep.
func (s *wrappedStoreWithPresignedFlag) FetchSweep(ctx context.Context,
	swap lntypes.Hash, utxo wire.OutPoint) (*SweepInfo, error) {

	sweepInfo, err := s.backend.FetchSweep(ctx, swap, utxo)
	if err != nil {
		return nil, err
	}

	// Attach IsPresigned flag.
	s.helper.mu.Lock()
	defer s.helper.mu.Unlock()
	_, sweepInfo.IsPresigned = s.helper.onlineOutpoints[utxo]

	return sweepInfo, nil
}

// testPresigned_presigned_and_regular_sweeps tests a combination of presigned
// mode and regular mode for the following scenario: one regular input is added,
// then a presigned input is added and it goes to another batch, because they
// should not appear in the same batch. Then another regular and another
// presigned inputs are added and go to the existing batches of their types.
func testPresigned_presigned_and_regular_sweeps(t *testing.T, store testStore,
	batcherStore testBatcherStore) {

	defer test.Guard(t)()

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	const (
		feeRateLow    = chainfee.SatPerKWeight(10_000)
		feeRateMedium = chainfee.SatPerKWeight(30_000)
		feeRateHigh   = chainfee.SatPerKWeight(40_000)
	)

	currentFeeRate := feeRateLow
	setFeeRate := func(feeRate chainfee.SatPerKWeight) {
		currentFeeRate = feeRate
	}
	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return currentFeeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	sweepStore, err := NewSweepFetcherFromSwapStore(store, lnd.ChainParams)
	require.NoError(t, err)

	sweepFetcher := &wrappedStoreWithPresignedFlag{
		backend: sweepStore,
		helper:  presignedHelper,
	}

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, sweepFetcher,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	setFeeRate(feeRateLow)

	/////////////////////////////////////
	// Create the first regular sweep. //
	/////////////////////////////////////
	swapHash1 := lntypes.Hash{1, 1, 1}
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1},
		Index: 1,
	}
	sweepReq1 := SweepRequest{
		SwapHash: swapHash1,
		Inputs: []Input{{
			Value:    1_000_000,
			Outpoint: op1,
		}},
		Notifier: &dummyNotifier,
	}

	swap1 := &loopdb.LoopOutContract{
		SwapContract: loopdb.SwapContract{
			CltvExpiry:      111,
			AmountRequested: 1_000_000,
			ProtocolVersion: loopdb.ProtocolVersionMuSig2,
			HtlcKeys:        htlcKeys,

			// Make preimage unique to pass SQL constraints.
			Preimage: lntypes.Preimage{1},
		},

		DestAddr:        destAddr,
		SwapInvoice:     swapInvoice,
		SweepConfTarget: 111,
	}

	err = store.CreateLoopOut(ctx, swapHash1, swap1)
	require.NoError(t, err)
	store.AssertLoopOutStored()

	// Deliver sweep request to batcher.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq1))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx1 := <-lnd.TxPublishChannel
	require.Len(t, tx1.TxIn, 1)
	require.Len(t, tx1.TxOut, 1)

	///////////////////////////////////////
	// Create the first presigned sweep. //
	///////////////////////////////////////
	swapHash2 := lntypes.Hash{2, 2, 2}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2},
		Index: 2,
	}

	swap2 := &loopdb.LoopOutContract{
		SwapContract: loopdb.SwapContract{
			CltvExpiry:      111,
			AmountRequested: 2_000_000,
			ProtocolVersion: loopdb.ProtocolVersionMuSig2,
			HtlcKeys:        htlcKeys,

			// Make preimage unique to pass SQL constraints.
			Preimage: lntypes.Preimage{2},
		},

		DestAddr:        destAddr,
		SwapInvoice:     swapInvoice,
		SweepConfTarget: 111,
	}

	err = store.CreateLoopOut(ctx, swapHash2, swap2)
	require.NoError(t, err)
	store.AssertLoopOutStored()

	sweepReq2 := SweepRequest{
		SwapHash: swapHash2,
		Inputs: []Input{{
			Value:    2_000_000,
			Outpoint: op2,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op2, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op2, Value: 2_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq2))

	// Since a batch was created we check that it registered for its primary
	// sweep's spend.
	<-lnd.RegisterSpendChannel

	// Wait for a transactions to be published.
	tx2 := <-lnd.TxPublishChannel
	require.Len(t, tx2.TxIn, 1)
	require.Len(t, tx2.TxOut, 1)
	require.Equal(t, op2, tx2.TxIn[0].PreviousOutPoint)

	//////////////////////////////////////
	// Create the second regular sweep. //
	//////////////////////////////////////
	swapHash3 := lntypes.Hash{3, 3, 3}
	op3 := wire.OutPoint{
		Hash:  chainhash.Hash{3, 3},
		Index: 3,
	}
	sweepReq3 := SweepRequest{
		SwapHash: swapHash3,
		Inputs: []Input{{
			Value:    4_000_000,
			Outpoint: op3,
		}},
		Notifier: &dummyNotifier,
	}

	swap3 := &loopdb.LoopOutContract{
		SwapContract: loopdb.SwapContract{
			CltvExpiry:      111,
			AmountRequested: 4_000_000,
			ProtocolVersion: loopdb.ProtocolVersionMuSig2,
			HtlcKeys:        htlcKeys,

			// Make preimage unique to pass SQL constraints.
			Preimage: lntypes.Preimage{3},
		},

		DestAddr:        destAddr,
		SwapInvoice:     swapInvoice,
		SweepConfTarget: 111,
	}

	err = store.CreateLoopOut(ctx, swapHash3, swap3)
	require.NoError(t, err)
	store.AssertLoopOutStored()

	// Deliver sweep request to batcher.
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq3))

	////////////////////////////////////////
	// Create the second presigned sweep. //
	////////////////////////////////////////
	swapHash4 := lntypes.Hash{4, 4, 4}
	op4 := wire.OutPoint{
		Hash:  chainhash.Hash{4, 4},
		Index: 4,
	}

	swap4 := &loopdb.LoopOutContract{
		SwapContract: loopdb.SwapContract{
			CltvExpiry:      111,
			AmountRequested: 3_000_000,
			ProtocolVersion: loopdb.ProtocolVersionMuSig2,
			HtlcKeys:        htlcKeys,

			// Make preimage unique to pass SQL constraints.
			Preimage: lntypes.Preimage{4},
		},

		DestAddr:        destAddr,
		SwapInvoice:     swapInvoice,
		SweepConfTarget: 111,
	}

	err = store.CreateLoopOut(ctx, swapHash4, swap4)
	require.NoError(t, err)
	store.AssertLoopOutStored()

	sweepReq4 := SweepRequest{
		SwapHash: swapHash4,
		Inputs: []Input{{
			Value:    3_000_000,
			Outpoint: op4,
		}},
		Notifier: &dummyNotifier,
	}
	presignedHelper.SetOutpointOnline(op4, true)
	err = batcher.PresignSweepsGroup(
		ctx, []Input{{Outpoint: op4, Value: 3_000_000}},
		sweepTimeout, destAddr, nil,
	)
	require.NoError(t, err)
	require.NoError(t, batcher.AddSweep(ctx, &sweepReq4))

	// Wait for the both batches to have two sweeps.
	require.Eventually(t, func() bool {
		// Make sure there are two batches.
		batches := getBatches(ctx, batcher)
		if len(batches) != 2 {
			return false
		}

		// Make sure each batch has two sweeps.
		for _, batch := range batches {
			var numSweeps int
			batch.testRunInEventLoop(ctx, func() {
				numSweeps = len(batch.sweeps)
			})
			if numSweeps != 2 {
				return false
			}
		}

		return true
	}, test.Timeout, eventuallyCheckFrequency)

	// Mine a block to trigger both batches publishing.
	require.NoError(t, lnd.NotifyHeight(601))

	// Wait for a transactions to be published.
	tx3 := <-lnd.TxPublishChannel
	require.Len(t, tx3.TxIn, 2)
	require.Len(t, tx3.TxOut, 1)
	require.Equal(t, int64(4993740), tx3.TxOut[0].Value)

	tx4 := <-lnd.TxPublishChannel
	require.Len(t, tx4.TxIn, 2)
	require.Len(t, tx4.TxOut, 1)
	require.Equal(t, int64(4993740), tx4.TxOut[0].Value)
}

// testPresigned_purging tests what happens if a non-final version of the batch
// is confirmed. Missing sweeps may be online or offline at that moment, which
// depends on the last argument of the function. In online case they are added
// to another online batch. In offline case they must are added to a new batch
// having valid presigned transactions.
func testPresigned_purging(t *testing.T, numSwaps, numConfirmedSwaps int,
	batcherStore testBatcherStore, online bool) {

	defer test.Guard(t)()

	require.LessOrEqual(t, numConfirmedSwaps, numSwaps)

	const (
		sweepsPerSwap = 2
		feeRate       = chainfee.SatPerKWeight(10_000)
		swapAmount    = 3_000_001
	)
	sweepAmounts := []btcutil.Amount{1_000_001, 2_000_000}

	lnd := test.NewMockLnd()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	customFeeRate := func(_ context.Context, _ lntypes.Hash,
		_ wire.OutPoint) (chainfee.SatPerKWeight, error) {

		return feeRate, nil
	}

	presignedHelper := newMockPresignedHelper()

	batcher := NewBatcher(
		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer,
		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams,
		batcherStore, presignedHelper,
		WithCustomFeeRate(customFeeRate),
		WithPresignedHelper(presignedHelper),
	)

	go func() {
		err := batcher.Run(ctx)
		checkBatcherError(t, err)
	}()

	swapHashes := make([]lntypes.Hash, numSwaps)
	groups := make([][]Input, numSwaps)
	txs := make([]*wire.MsgTx, numSwaps)
	allOps := make([]wire.OutPoint, 0, numSwaps*sweepsPerSwap)
	spendChans := make([]<-chan *SpendDetail, numSwaps)
	confChans := make([]<-chan *ConfDetail, numSwaps)

	for i := range numSwaps {
		// Create a swap of sweepsPerSwap sweeps.
		swapHash := lntypes.Hash{byte(i + 1)}
		swapHashes[i] = swapHash
		ops := make([]wire.OutPoint, sweepsPerSwap)
		group := make([]Input, sweepsPerSwap)
		for j := range sweepsPerSwap {
			ops[j] = wire.OutPoint{
				Hash:  chainhash.Hash{byte(1 + i*2 + j)},
				Index: uint32(1 + i*2 + j),
			}
			allOps = append(allOps, ops[j])

			group[j] = Input{
				Outpoint: ops[j],
				Value:    sweepAmounts[j],
			}
		}
		groups[i] = group

		// Enable all the sweeps.
		for _, op := range ops {
			presignedHelper.SetOutpointOnline(op, true)
		}

		// An attempt to presign must succeed.
		err := batcher.PresignSweepsGroup(
			ctx, group, sweepTimeout, destAddr, nil,
		)
		require.NoError(t, err)

		// Create a spending notification channel.
		spendChan := make(chan *SpendDetail, 1)
		spendChans[i] = spendChan
		confChan := make(chan *ConfDetail, 1)
		confChans[i] = confChan
		notifier := &SpendNotifier{
			SpendChan:    spendChan,
			SpendErrChan: make(chan error, 1),
			ConfChan:     confChan,
			ConfErrChan:  make(chan error, 1),
			QuitChan:     make(chan bool, 1),
		}

		// Add the sweep, triggering the publish attempt.
		require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
			SwapHash: swapHash,
			Inputs:   group,
			Notifier: notifier,
		}))

		// For the first group it should register for the sweep's spend
		// and publish a transaction.
		if i == 0 {
			<-lnd.RegisterSpendChannel
		} else {
			// Trigger transaction publishing after each group.
			require.NoError(t, lnd.NotifyHeight(int32(601+i)))
		}

		// Wait for a transactions to be published.
		tx := <-lnd.TxPublishChannel
		txs[i] = tx
	}

	// Record batch ID of the first batch.
	batch1id := getOnlyBatch(t, ctx, batcher).id

	// Turn off all the sweeps.
	for _, op := range allOps {
		presignedHelper.SetOutpointOnline(op, false)
	}

	// In case we are testing the addition of the remaining sweeps to a
	// batch in online state, we need to create that batch now.
	opx := wire.OutPoint{Hash: chainhash.Hash{3, 2, 1}, Index: 1}
	if online && numConfirmedSwaps < numSwaps {
		swapHash := lntypes.Hash{1, 2, 3}
		const amount = 1_234_567
		group := []Input{
			{
				Outpoint: opx,
				Value:    amount,
			},
		}

		// Enable the sweep.
		presignedHelper.SetOutpointOnline(opx, true)

		// An attempt to presign must succeed.
		err := batcher.PresignSweepsGroup(
			ctx, group, sweepTimeout, destAddr, nil,
		)
		require.NoError(t, err)

		// Add the sweep, triggering the publishing attempt.
		require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
			SwapHash: swapHash,
			Inputs:   group,
			Notifier: &dummyNotifier,
		}))

		<-lnd.RegisterSpendChannel
		tx := <-lnd.TxPublishChannel
		require.Len(t, tx.TxIn, 1)
		require.Equal(t, opx, tx.TxIn[0].PreviousOutPoint)

		// Now enable our main sweeps again so the remaining ones are
		// added to this new batch.
		for _, op := range allOps {
			presignedHelper.SetOutpointOnline(op, true)
		}
	}

	// Now mine the transaction which includes first numConfirmedSwaps.
	tx := txs[numConfirmedSwaps-1]

	// Now confirm previously broadcasted transaction (op1 and op2).
	txHash := tx.TxHash()
	spendDetail := &chainntnfs.SpendDetail{
		SpentOutPoint:     &allOps[0],
		SpendingTx:        tx,
		SpenderTxHash:     &txHash,
		SpenderInputIndex: 0,
		SpendingHeight:    int32(601 + numSwaps + 1),
	}
	lnd.SpendChannel <- spendDetail

	// Calculate the expected on-chain fee of the swap.
	wantFee := make([]btcutil.Amount, numConfirmedSwaps)
	for i := range numConfirmedSwaps {
		batchAmount := swapAmount * btcutil.Amount(numConfirmedSwaps)
		txFee := batchAmount - btcutil.Amount(tx.TxOut[0].Value)
		numConfirmedSweeps := numConfirmedSwaps * sweepsPerSwap
		feePerSweep := txFee / btcutil.Amount(numConfirmedSweeps)
		roundingDiff := txFee - feePerSweep*btcutil.Amount(
			numConfirmedSweeps,
		)
		swapFee := feePerSweep * 2

		// Add rounding difference to the first swap.
		if i == 0 {
			swapFee += roundingDiff
		}

		wantFee[i] = swapFee
	}

	// Make sure that notifiers of confirmed sweeps received notifications.
	for i := range numConfirmedSwaps {
		spend := <-spendChans[i]
		require.Equal(t, txHash, spend.Tx.TxHash())
		require.Equal(t, wantFee[i], spend.OnChainFeePortion)
	}

	<-lnd.RegisterConfChannel
	require.NoError(t, lnd.NotifyHeight(
		int32(601+numSwaps+1+batchConfHeight),
	))
	lnd.ConfChannel <- &chainntnfs.TxConfirmation{
		Tx: tx,
	}

	// CleanupTransactions is called here.
	<-presignedHelper.cleanupCalled

	// Increasing block height caused the second batch to re-publish.
	if online && numConfirmedSwaps < numSwaps {
		<-lnd.TxPublishChannel
	}

	// Make sure that notifiers of confirmed sweeps received notifications.
	for i := range numConfirmedSwaps {
		conf := <-confChans[i]
		require.Equal(t, txHash, conf.Tx.TxHash())
		require.Equal(t, wantFee[i], conf.OnChainFeePortion)
	}

	if !online && numConfirmedSwaps != numSwaps {
		// If the sweeps are offline, the missing sweeps in the
		// confirmed transaction should be re-added to the batcher as
		// new batch. The groups are added incrementally, so we need
		// to wait until the batch reaches the expected size.
		<-lnd.RegisterSpendChannel
		<-lnd.TxPublishChannel
	}

	// Now make sure that a correct spend and conf contification is sent if
	// AddSweep is called after confirming the sweeps.
	for i := range numConfirmedSwaps {
		// Create a spending notification channel.
		spendChan := make(chan *SpendDetail, 1)
		confChan := make(chan *ConfDetail)
		notifier := &SpendNotifier{
			SpendChan:    spendChan,
			SpendErrChan: make(chan error, 1),
			ConfChan:     confChan,
			ConfErrChan:  make(chan error, 1),
			QuitChan:     make(chan bool, 1),
		}

		// Add the sweep, triggering the publish attempt.
		require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{
			SwapHash: swapHashes[i],
			Inputs:   groups[i],
			Notifier: notifier,
		}))

		spendReg := <-lnd.RegisterSpendChannel
		spendReg.SpendChannel <- spendDetail

		spend := <-spendChan
		require.Equal(t, txHash, spend.Tx.TxHash())
		require.Equal(t, wantFee[i], spend.OnChainFeePortion)

		<-lnd.RegisterConfChannel
		lnd.ConfChannel <- &chainntnfs.TxConfirmation{
			Tx: tx,
		}

		conf := <-confChan
		require.Equal(t, tx.TxHash(), conf.Tx.TxHash())
		require.Equal(t, wantFee[i], conf.OnChainFeePortion)
	}

	// If all the swaps were confirmed, stop.
	if numConfirmedSwaps == numSwaps {
		return
	}

	// Wait to new batch to appear and to have the expected size.
	wantSize := (numSwaps - numConfirmedSwaps) * sweepsPerSwap
	if online {
		// Add opx to the list of expected inputs.
		wantSize++
	}
	require.Eventually(t, func() bool {
		// Wait for a batch with new ID to appear.
		batches := getBatches(ctx, batcher)
		var batch2 *batch
		for _, b := range batches {
			if b.id != batch1id {
				batch2 = b
			}
		}
		if batch2 == nil {
			return false
		}

		// Check the size of the second batch.
		return batch2.numSweeps(ctx) == wantSize
	}, test.Timeout, eventuallyCheckFrequency)

	// Now trigger batch publishing and inspect the published tx.
	require.NoError(t, lnd.NotifyHeight(int32(
		601+numSwaps+1+batchConfHeight+1,
	)))
	tx2 := <-lnd.TxPublishChannel
	wantOps := allOps[numConfirmedSwaps*sweepsPerSwap:]
	if online {
		// Deep copy wantOps to unlink from allOps.
		wantOps = append([]wire.OutPoint{}, wantOps...)
		wantOps = append(wantOps, opx)
	}
	gotOps := make([]wire.OutPoint, 0, len(tx2.TxIn))
	for _, txIn := range tx2.TxIn {
		gotOps = append(gotOps, txIn.PreviousOutPoint)
	}

	require.ElementsMatch(t, wantOps, gotOps)
}

// TestPresigned tests presigned mode. Most sub-tests doesn't use loopdb.
func TestPresigned(t *testing.T) {
	logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout))
	logger.SetLevel(btclog.LevelTrace)
	UseLogger(logger.SubSystem("SWEEP"))

	t.Run("forgotten_presign", func(t *testing.T) {
		testPresigned_forgotten_presign(t, NewStoreMock())
	})

	t.Run("input1_offline_then_input2", func(t *testing.T) {
		testPresigned_input1_offline_then_input2(t, NewStoreMock())
	})

	t.Run("min_relay_fee", func(t *testing.T) {
		testPresigned_min_relay_fee(t, NewStoreMock())
	})

	t.Run("two_inputs_one_goes_offline", func(t *testing.T) {
		testPresigned_two_inputs_one_goes_offline(t, NewStoreMock())
	})

	t.Run("first_publish_fails", func(t *testing.T) {
		testPresigned_first_publish_fails(t, NewStoreMock())
	})

	t.Run("locktime", func(t *testing.T) {
		testPresigned_locktime(t, NewStoreMock())
	})

	t.Run("presigned_group", func(t *testing.T) {
		testPresigned_presigned_group(t, NewStoreMock())
	})

	t.Run("change", func(t *testing.T) {
		testPresigned_presigned_group_with_change(t, NewStoreMock())
	})

	t.Run("fee_portion_change", func(t *testing.T) {
		testPresigned_fee_portion_with_change(t, NewStoreMock())
	})

	t.Run("identical change pkscript", func(t *testing.T) {
		testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock())
	})

	t.Run("dust_main_output", func(t *testing.T) {
		testPresigned_presigned_group_with_dust_main_output(
			t, NewStoreMock(),
		)
	})

	t.Run("dust_main_output_below_min_relay_fee", func(t *testing.T) {
		testPresigned_presigned_group_with_dust_below_relay_fee(
			t, NewStoreMock(),
		)
	})

	t.Run("dust_change", func(t *testing.T) {
		testPresigned_presigned_group_with_dust_change(t, NewStoreMock())
	})

	t.Run("presigned_and_regular_sweeps", func(t *testing.T) {
		runTests(t, testPresigned_presigned_and_regular_sweeps)
	})

	t.Run("purging", func(t *testing.T) {
		testPurging := func(numSwaps, numConfirmedSwaps int,
			online bool) {

			name := fmt.Sprintf("%d of %d swaps confirmed",
				numConfirmedSwaps, numSwaps)
			if online {
				name += ", sweeps online"
			} else {
				name += ", sweeps offline"
			}

			t.Run(name, func(t *testing.T) {
				testPresigned_purging(
					t, numSwaps, numConfirmedSwaps,
					NewStoreMock(), online,
				)
			})
		}

		// Test cases in which the sweeps are offline.
		testPurging(1, 1, false)
		testPurging(2, 1, false)
		testPurging(2, 2, false)
		testPurging(3, 1, false)
		testPurging(3, 2, false)
		testPurging(5, 2, false)
		testPurging(5, 3, false)

		// Test cases in which the sweeps are online.
		testPurging(2, 1, true)
		testPurging(3, 1, true)
		testPurging(3, 2, true)
		testPurging(5, 2, true)
		testPurging(5, 3, true)
	})
}
