package sweepbatcher

import (
	"context"
	"fmt"
	"sync"
	"testing"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/stretchr/testify/require"
)

// TestOrderedSweeps checks that methods batch.getOrderedSweeps and
// batch.getSweepsGroups works properly.
func TestOrderedSweeps(t *testing.T) {
	// Prepare the necessary data for test cases.
	op1 := wire.OutPoint{Hash: chainhash.Hash{1, 1, 1}, Index: 1}
	op2 := wire.OutPoint{Hash: chainhash.Hash{2, 2, 2}, Index: 2}
	op3 := wire.OutPoint{Hash: chainhash.Hash{3, 3, 3}, Index: 3}
	op4 := wire.OutPoint{Hash: chainhash.Hash{4, 4, 4}, Index: 4}
	op5 := wire.OutPoint{Hash: chainhash.Hash{5, 5, 5}, Index: 5}
	op6 := wire.OutPoint{Hash: chainhash.Hash{6, 6, 6}, Index: 6}

	swapHash1 := lntypes.Hash{1, 1}
	swapHash2 := lntypes.Hash{2, 2}
	swapHash3 := lntypes.Hash{3, 3}

	ctx := context.Background()

	cases := []struct {
		name        string
		sweeps      []sweep
		skippedTxns map[chainhash.Hash]struct{}

		// Testing errors.
		skipStore     bool
		reverseStore  bool
		replaceSweeps map[wire.OutPoint]sweep

		wantGroups [][]sweep
		wantErr1   string
		wantErr2   string
	}{
		{
			name:       "no sweeps",
			sweeps:     []sweep{},
			wantGroups: [][]sweep{},
		},

		{
			name: "one sweep",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
			},
			wantGroups: [][]sweep{
				{
					{
						outpoint: op1,
						swapHash: swapHash1,
					},
				},
			},
		},

		{
			name: "one sweep, skipped",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
			},
			skippedTxns: map[chainhash.Hash]struct{}{
				op1.Hash: {},
			},
			wantGroups: [][]sweep{},
		},

		{
			name: "two sweeps, one swap",
			sweeps: []sweep{
				{
					outpoint: op2,
					swapHash: swapHash1,
				},
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
			},
			wantGroups: [][]sweep{
				{
					{
						outpoint: op2,
						swapHash: swapHash1,
					},
					{
						outpoint: op1,
						swapHash: swapHash1,
					},
				},
			},
		},

		{
			name: "two sweeps, one swap, one skipped",
			sweeps: []sweep{
				{
					outpoint: op2,
					swapHash: swapHash1,
				},
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
			},
			skippedTxns: map[chainhash.Hash]struct{}{
				op1.Hash: {},
			},
			wantGroups: [][]sweep{
				{
					{
						outpoint: op2,
						swapHash: swapHash1,
					},
				},
			},
		},

		{
			name: "two sweeps, two swap",
			sweeps: []sweep{
				{
					outpoint: op2,
					swapHash: swapHash1,
				},
				{
					outpoint: op1,
					swapHash: swapHash2,
				},
			},
			wantGroups: [][]sweep{
				{
					{
						outpoint: op2,
						swapHash: swapHash1,
					},
				},
				{
					{
						outpoint: op1,
						swapHash: swapHash2,
					},
				},
			},
		},

		{
			name: "many sweeps and swaps",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
				{
					outpoint: op2,
					swapHash: swapHash1,
				},
				{
					outpoint: op3,
					swapHash: swapHash1,
				},
				{
					outpoint: op4,
					swapHash: swapHash2,
				},
				{
					outpoint: op5,
					swapHash: swapHash3,
				},
				{
					outpoint: op6,
					swapHash: swapHash3,
				},
			},
			wantGroups: [][]sweep{
				{
					{
						outpoint: op1,
						swapHash: swapHash1,
					},
					{
						outpoint: op2,
						swapHash: swapHash1,
					},
					{
						outpoint: op3,
						swapHash: swapHash1,
					},
				},
				{
					{
						outpoint: op4,
						swapHash: swapHash2,
					},
				},
				{
					{
						outpoint: op5,
						swapHash: swapHash3,
					},
					{
						outpoint: op6,
						swapHash: swapHash3,
					},
				},
			},
		},

		{
			name: "error: sweeps not stored in DB",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
			},
			skipStore: true,
			wantErr1:  "returned 0 sweeps, len(b.sweeps) is 1",
		},

		{
			name: "error: wrong order in DB",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
				{
					outpoint: op2,
					swapHash: swapHash2,
				},
			},
			reverseStore: true,
		},

		{
			name: "error: extra sweep in DB",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
				{
					outpoint: op2,
					swapHash: swapHash2,
				},
			},
			replaceSweeps: map[wire.OutPoint]sweep{
				op2: {
					outpoint: op3,
					swapHash: swapHash3,
				},
			},
			wantErr1: "returned unknown sweep",
		},

		{
			name: "error: swaps interleaved",
			sweeps: []sweep{
				{
					outpoint: op1,
					swapHash: swapHash1,
				},
				{
					outpoint: op2,
					swapHash: swapHash2,
				},
				{
					outpoint: op3,
					swapHash: swapHash1,
				},
			},
			wantErr2: "3 groups of sweeps and 2 distinct swaps",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			// Create a map of sweeps.
			m := make(map[wire.OutPoint]sweep, len(tc.sweeps))
			for _, s := range tc.sweeps {
				require.NotContains(t, m, s.outpoint)
				m[s.outpoint] = s
			}

			// Create a batch.
			b := &batch{
				sweeps: m,
				store:  NewStoreMock(),
				cfg: &batchConfig{
					skippedTxns: tc.skippedTxns,
				},
			}

			// Store the sweeps in mock store.
			switch {
			// Don't create DB sweeps at all.
			case tc.skipStore:

			// Reverse the order of DB sweeps to ID incorrect IDs.
			case tc.reverseStore:
				for i := len(tc.sweeps) - 1; i >= 0; i-- {
					s := tc.sweeps[i]
					const completed = false
					err := b.persistSweep(ctx, s, completed)
					require.NoError(t, err)
				}

			// Working DB sweeps.
			default:
				for _, s := range tc.sweeps {
					const completed = false
					err := b.persistSweep(ctx, s, completed)
					require.NoError(t, err)
				}
			}

			// Replace some sweeps to test an error.
			for removed, added := range tc.replaceSweeps {
				require.Contains(t, m, removed)
				delete(m, removed)
				require.NotContains(t, m, added.outpoint)
				m[added.outpoint] = added
			}

			// Remove skipped sweeps from the batch to make it
			// match with what is read from DB after filtering.
			for op := range m {
				if _, has := tc.skippedTxns[op.Hash]; has {
					delete(m, op)
				}
			}

			// Now run the tested functions.
			orderedSweeps, err := b.getOrderedSweeps(ctx)
			if tc.wantErr1 != "" {
				require.ErrorContains(t, err, tc.wantErr1)
				return
			}
			require.NoError(t, err)

			if tc.reverseStore {
				require.NotEqual(t, tc.sweeps, orderedSweeps)
				return
			}

			// The wanted list of sweeps matches the input order.
			notSkipped := make([]sweep, 0, len(tc.sweeps))
			for _, s := range tc.sweeps {
				_, has := tc.skippedTxns[s.outpoint.Hash]
				if has {
					continue
				}
				notSkipped = append(notSkipped, s)
			}
			require.Equal(t, notSkipped, orderedSweeps)

			groups, err := b.getSweepsGroups(ctx)
			if tc.wantErr2 != "" {
				require.ErrorContains(t, err, tc.wantErr2)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tc.wantGroups, groups)
		})
	}
}

// mockPresignedTxChecker is an implementation of presignedTxChecker used in
// TestEnsurePresigned.
type mockPresignedTxChecker struct {
	// primarySweepID is the value the mock matches this argument against.
	primarySweepID wire.OutPoint

	// destPkScript is the value returned by mock.DestPkScript.
	destPkScript []byte

	// signTxCalls is the number of SignTx calls.
	signTxCalls int

	// recordedInputAmt is the saved value of inputAmt argument of SignTx.
	recordedInputAmt btcutil.Amount

	// recordedMinRelayFee is the saved value of minRelayFee argument of
	// SignTx.
	recordedMinRelayFee chainfee.SatPerKWeight

	// recordedFeeRate is the saved value of feeRate argument of SignTx.
	recordedFeeRate chainfee.SatPerKWeight

	// recordedLoadOnly is the saved value of loadOnly argument of SignTx.
	recordedLoadOnly bool

	// destPkScriptErr is the error returned by DestPkScript (if any).
	destPkScriptErr error

	// signedTxErr is the error returned by SignTx (if any).
	signedTxErr error
}

// DestPkScript returns destination pkScript used by the sweep batch.
func (m *mockPresignedTxChecker) DestPkScript(ctx context.Context,
	primarySweepID wire.OutPoint) ([]byte, error) {

	if primarySweepID != m.primarySweepID {
		return nil, fmt.Errorf("primarySweepID mismatch")
	}

	if m.destPkScriptErr != nil {
		return nil, m.destPkScriptErr
	}

	return m.destPkScript, nil
}

// SignTx records all its argumentd and returned a "presigned" tx.
func (m *mockPresignedTxChecker) SignTx(ctx context.Context,
	primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
	minRelayFee, feeRate chainfee.SatPerKWeight,
	loadOnly bool) (*wire.MsgTx, error) {

	m.signTxCalls++

	if primarySweepID != m.primarySweepID {
		return nil, fmt.Errorf("primarySweepID mismatch")
	}

	m.recordedInputAmt = inputAmt
	m.recordedMinRelayFee = minRelayFee
	m.recordedFeeRate = feeRate
	m.recordedLoadOnly = loadOnly

	if m.signedTxErr != nil {
		return nil, m.signedTxErr
	}

	// Pretend that we have a presigned transaction.
	tx = tx.Copy()
	for i := range tx.TxIn {
		tx.TxIn[i].Witness = wire.TxWitness{
			make([]byte, 64),
		}
	}

	return tx, nil
}

// TestEnsurePresigned checks that function ensurePresigned works correctly.
func TestEnsurePresigned(t *testing.T) {
	// Prepare the necessary data for test cases.
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2, 2},
		Index: 2,
	}

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

	ctx := context.Background()

	cases := []struct {
		name            string
		primarySweepID  wire.OutPoint
		sweeps          []*sweep
		destPkScript    []byte
		minRelayFeeRate chainfee.SatPerKWeight
		wantInputAmt    btcutil.Amount
		destPkScriptErr error
		signedTxErr     error
	}{
		{
			name:           "one input",
			primarySweepID: op1,
			sweeps: []*sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destPkScript:    batchPkScript,
			minRelayFeeRate: chainfee.FeePerKwFloor,
			wantInputAmt:    1_000_000,
		},

		{
			name:           "one input, higher minRelayFeeRate",
			primarySweepID: op1,
			sweeps: []*sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destPkScript:    batchPkScript,
			minRelayFeeRate: 1000,
			wantInputAmt:    1_000_000,
		},

		{
			name:           "two inputs",
			primarySweepID: op1,
			sweeps: []*sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1000,
				},
			},
			destPkScript:    batchPkScript,
			minRelayFeeRate: chainfee.FeePerKwFloor,
			wantInputAmt:    3_000_000,
		},

		{
			name:           "error: DestPkScript fails",
			primarySweepID: op1,
			sweeps: []*sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			minRelayFeeRate: chainfee.FeePerKwFloor,
			destPkScriptErr: fmt.Errorf("test DestPkScript error"),
		},

		{
			name:           "error: SignTx fails",
			primarySweepID: op1,
			sweeps: []*sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destPkScript:    batchPkScript,
			minRelayFeeRate: chainfee.FeePerKwFloor,
			signedTxErr:     fmt.Errorf("test SignTx error"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			c := &mockPresignedTxChecker{
				primarySweepID:  tc.primarySweepID,
				destPkScript:    tc.destPkScript,
				destPkScriptErr: tc.destPkScriptErr,
				signedTxErr:     tc.signedTxErr,
			}

			err := ensurePresigned(
				ctx, tc.sweeps, c, tc.minRelayFeeRate,
				&chaincfg.RegressionNetParams,
			)
			switch {
			case tc.destPkScriptErr != nil:
				require.ErrorIs(t, err, tc.destPkScriptErr)
			case tc.signedTxErr != nil:
				require.ErrorIs(t, err, tc.signedTxErr)
			default:
				require.NoError(t, err)
				require.Equal(t, 1, c.signTxCalls)
				require.Equal(
					t, tc.wantInputAmt, c.recordedInputAmt,
				)
				require.Equal(
					t, tc.minRelayFeeRate,
					c.recordedMinRelayFee,
				)
				require.Equal(
					t, tc.minRelayFeeRate,
					c.recordedFeeRate,
				)
				require.True(t, c.recordedLoadOnly)
			}
		})
	}
}

// hasInput returns if the transaction spends the UTXO.
func hasInput(tx *wire.MsgTx, utxo wire.OutPoint) bool {
	for _, txIn := range tx.TxIn {
		if txIn.PreviousOutPoint == utxo {
			return true
		}
	}

	return false
}

// mockPresigner is an implementation of Presigner used in TestPresign.
type mockPresigner struct {
	// outputs collects outputs of presigned transactions.
	outputs []btcutil.Amount

	// lockTimes collects LockTime's of presigned transactions.
	lockTimes []uint32

	// failAt is optional index of a call at which it fails, 1 based.
	failAt int

	// mu protects the state.
	mu sync.Mutex
}

// SignTx memorizes the value of the output and fails if the number of
// calls previously made is failAt.
func (p *mockPresigner) SignTx(ctx context.Context,
	primarySweepID wire.OutPoint, tx *wire.MsgTx, inputAmt btcutil.Amount,
	minRelayFee, feeRate chainfee.SatPerKWeight,
	loadOnly bool) (*wire.MsgTx, error) {

	p.mu.Lock()
	defer p.mu.Unlock()

	if ctx.Err() != nil {
		return nil, ctx.Err()
	}

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

	if len(p.outputs)+1 == p.failAt {
		return nil, fmt.Errorf("test error in SignTx")
	}

	p.outputs = append(p.outputs, btcutil.Amount(tx.TxOut[0].Value))
	p.lockTimes = append(p.lockTimes, tx.LockTime)

	return tx, nil
}

// TestPresign checks that function presign presigns correct set of transactions
// and handles edge cases properly.
func TestPresign(t *testing.T) {
	// Prepare the necessary data for test cases.
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2, 2},
		Index: 2,
	}

	ctx := context.Background()

	cases := []struct {
		name             string
		presigner        presigner
		primarySweepID   wire.OutPoint
		sweeps           []sweep
		destAddr         btcutil.Address
		nextBlockFeeRate chainfee.SatPerKWeight
		minRelayFeeRate  chainfee.SatPerKWeight
		wantErr          string
		wantOutputs      []btcutil.Amount
		wantLockTimes    []uint32
	}{
		{
			name:           "error: no presigner",
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "presigner is not installed",
		},

		{
			name:             "error: no sweeps",
			primarySweepID:   op1,
			presigner:        &mockPresigner{},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "there are no sweeps",
		},

		{
			name:           "error: no destAddr",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "unsupported address type <nil>",
		},

		{
			name:           "error: zero nextBlockFeeRate",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1000,
				},
			},
			destAddr:        destAddr,
			minRelayFeeRate: chainfee.FeePerKwFloor,
			wantErr:         "nextBlockFeeRate is not set",
		},

		{
			name:           "error: zero minRelayFeeRate",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			wantErr:          "minRelayFeeRate is not set",
		},

		{
			name:           "error: timeout is not set",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "timeout is invalid: 0",
		},

		{
			name:      "error: primary not set",
			presigner: &mockPresigner{},
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1100,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "not in tx",
		},

		{
			name:           "error: primary not in tx",
			presigner:      &mockPresigner{},
			primarySweepID: op2,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "not in tx",
		},

		{
			name:           "one sweep",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				999900, 999880, 999856, 999827, 999793, 999752,
				999702, 999643, 999572, 999486, 999384, 999260,
				999113, 998935, 998723, 998467, 998161, 997793,
				997352, 996823, 996187, 995425, 994510, 993413,
				992096, 990515, 988618, 986342, 983610, 980332,
				976399, 971679, 966015, 959218, 951062, 941274,
				929530, 915435, 898523, 878227, 853873, 824648,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
				950, 950, 950, 950, 950, 950, 950, 950, 950,
				950, 950, 950, 950,
			},
		},

		{
			name:           "higher minRelayFeeRate, fewer txns",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: 10 * chainfee.FeePerKwFloor,
			minRelayFeeRate:  10 * chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				998998, 998797, 998557, 998269, 997923, 997507,
				997009, 996411, 995694, 994833, 993800, 992560,
				991072, 989286, 987144, 984573, 981488, 977786,
				973343, 968012, 961614, 953937, 944725, 933670,
				920405, 904486, 885383, 862460, 834952,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950,
				950, 950, 950, 950, 950, 950, 950, 950, 950,
				950, 950, 950, 950, 950,
			},
		},

		{
			name:           "two sweeps",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1100,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				2999841, 2999810, 2999773, 2999728, 2999673,
				2999608, 2999530, 2999436, 2999323, 2999188,
				2999026, 2998831, 2998598, 2998317, 2997981,
				2997577, 2997093, 2996512, 2995814, 2994977,
				2993973, 2992768, 2991322, 2989587, 2987505,
				2985006, 2982007, 2978409, 2974091, 2968910,
				2962691, 2955230, 2946276, 2935532, 2922639,
				2907167, 2888600, 2866320, 2839584, 2807501,
				2769001, 2722802,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
				950, 950, 950, 950, 950, 950, 950, 950, 950,
				950, 950, 950, 950,
			},
		},

		{
			name:           "two sweeps, another primary",
			presigner:      &mockPresigner{},
			primarySweepID: op2,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1100,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				2999841, 2999810, 2999773, 2999728, 2999673,
				2999608, 2999530, 2999436, 2999323, 2999188,
				2999026, 2998831, 2998598, 2998317, 2997981,
				2997577, 2997093, 2996512, 2995814, 2994977,
				2993973, 2992768, 2991322, 2989587, 2987505,
				2985006, 2982007, 2978409, 2974091, 2968910,
				2962691, 2955230, 2946276, 2935532, 2922639,
				2907167, 2888600, 2866320, 2839584, 2807501,
				2769001, 2722802,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 950, 950, 950,
				950, 950, 950, 950, 950, 950, 950, 950, 950,
				950, 950, 950, 950,
			},
		},

		{
			name:           "timeout < 50",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  40,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  40,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				2999841, 2999810, 2999773, 2999728, 2999673,
				2999608, 2999530, 2999436, 2999323, 2999188,
				2999026, 2998831, 2998598, 2998317, 2997981,
				2997577, 2997093, 2996512, 2995814, 2994977,
				2993973, 2992768, 2991322, 2989587, 2987505,
				2985006, 2982007, 2978409, 2974091, 2968910,
				2962691, 2955230, 2946276, 2935532, 2922639,
				2907167, 2888600, 2866320, 2839584, 2807501,
				2769001, 2722802,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
			},
		},

		{
			name:           "high current feerate => locktime later",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: 50 * chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				2999841, 2999810, 2999773, 2999728, 2999673,
				2999608, 2999530, 2999436, 2999323, 2999188,
				2999026, 2998831, 2998598, 2998317, 2997981,
				2997577, 2997093, 2996512, 2995814, 2994977,
				2993973, 2992768, 2991322, 2989587, 2987505,
				2985006, 2982007, 2978409, 2974091, 2968910,
				2962691, 2955230, 2946276, 2935532, 2922639,
				2907167, 2888600, 2866320, 2839584, 2807501,
				2769001, 2722802,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
				0, 0, 0, 950, 950, 950, 950, 950, 950, 950,
			},
		},

		{
			name: "small amount => fewer steps until " +
				"clamped",
			presigner:      &mockPresigner{},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantOutputs: []btcutil.Amount{
				2841, 2810, 2773, 2728, 2673, 2608, 2530, 2436,
				2400,
			},
			wantLockTimes: []uint32{
				0, 0, 0, 0, 0, 0, 0, 0, 0,
			},
		},

		{
			name: "third signing fails",
			presigner: &mockPresigner{
				failAt: 3,
			},
			primarySweepID: op1,
			sweeps: []sweep{
				{
					outpoint: op1,
					value:    1_000,
					timeout:  1000,
				},
				{
					outpoint: op2,
					value:    2_000,
					timeout:  1000,
				},
			},
			destAddr:         destAddr,
			nextBlockFeeRate: chainfee.FeePerKwFloor,
			minRelayFeeRate:  chainfee.FeePerKwFloor,
			wantErr:          "test error in SignTx",
		},
	}

	type pair struct {
		output   btcutil.Amount
		lockTime uint32
	}

	zip := func(outputs []btcutil.Amount, lockTimes []uint32) []pair {
		require.Equal(t, len(outputs), len(lockTimes))

		pairs := make([]pair, len(outputs))
		for i, output := range outputs {
			pairs[i] = pair{
				output:   output,
				lockTime: lockTimes[i],
			}
		}

		return pairs
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := presign(
				ctx, tc.presigner, tc.destAddr,
				tc.primarySweepID, tc.sweeps,
				tc.nextBlockFeeRate, tc.minRelayFeeRate,
			)
			if tc.wantErr != "" {
				require.Error(t, err)
				require.ErrorContains(t, err, tc.wantErr)
			} else {
				require.NoError(t, err)
				p := tc.presigner.(*mockPresigner)
				require.ElementsMatch(
					t,
					zip(tc.wantOutputs, tc.wantLockTimes),
					zip(p.outputs, p.lockTimes),
				)
			}
		})
	}
}

// TestCheckSignedTx tests that function CheckSignedTx checks all the criteria
// of PresignedHelper.SignTx correctly.
func TestCheckSignedTx(t *testing.T) {
	// Prepare the necessary data for test cases.
	op1 := wire.OutPoint{
		Hash:  chainhash.Hash{1, 1, 1},
		Index: 1,
	}
	op2 := wire.OutPoint{
		Hash:  chainhash.Hash{2, 2, 2},
		Index: 2,
	}

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

	cases := []struct {
		name        string
		unsignedTx  *wire.MsgTx
		signedTx    *wire.MsgTx
		inputAmt    btcutil.Amount
		minRelayFee chainfee.SatPerKWeight
		wantErr     string
	}{
		{
			name: "success",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "",
		},

		{
			name: "unsigned input in signedTx",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "is not signed",
		},

		{
			name: "bad locktime",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_001,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "locktime",
		},

		{
			name: "bad version",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 3,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "version",
		},

		{
			name: "missing input",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "is missing in signed tx",
		},

		{
			name: "extra input",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "is new in signed tx",
		},

		{
			name: "mismatch of sequence numbers",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         3,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "sequence mismatch",
		},

		{
			name: "extra output in unsignedTx",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr: "unsigned tx has 2 outputs, signed tx " +
				"has 1 outputs, should be equal",
		},

		{
			name: "extra output in signedTx",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr: "unsigned tx has 1 outputs, signed tx " +
				"has 2 outputs, should be equal",
		},

		{
			name: "pkscript mismatch",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: []byte{0xaf, 0xfe}, // Just to make it different.
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "mismatch of output pkScript",
		},

		{
			name: "value mismatch, first output",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    1_337_000, // Just to make it different.
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "",
		},

		{
			name: "value mismatch, change output",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
					{
						PreviousOutPoint: op1,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
					{
						Value:    1_337_000,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2_493_300,
						PkScript: batchPkScript,
					},
					{
						Value:    1_338, // Just to make it different.
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "mismatch of output value",
		},

		{
			name: "mismatch of output pk_script",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript[1:],
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 253,
			wantErr:     "mismatch of output pkScript",
		},

		{
			name: "too low feerate in signedTx",
			unsignedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op1,
						Sequence:         1,
					},
					{
						PreviousOutPoint: op2,
						Sequence:         2,
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 800_000,
			},
			signedTx: &wire.MsgTx{
				Version: 2,
				TxIn: []*wire.TxIn{
					{
						PreviousOutPoint: op2,
						Sequence:         2,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
					{
						PreviousOutPoint: op1,
						Sequence:         1,
						Witness: wire.TxWitness{
							[]byte("test"),
						},
					},
				},
				TxOut: []*wire.TxOut{
					{
						Value:    2999374,
						PkScript: batchPkScript,
					},
				},
				LockTime: 799_999,
			},
			inputAmt:    3_000_000,
			minRelayFee: 250_000,
			wantErr:     "is lower than minRelayFee",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := CheckSignedTx(
				tc.unsignedTx, tc.signedTx, tc.inputAmt,
				tc.minRelayFee,
			)
			if tc.wantErr != "" {
				require.Error(t, err)
				require.ErrorContains(t, err, tc.wantErr)
			} else {
				require.NoError(t, err)
			}
		})
	}
}
