package loopin

import (
	"bytes"
	"context"
	"testing"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/staticaddr/deposit"
	"github.com/lightninglabs/loop/staticaddr/script"
	"github.com/lightninglabs/loop/staticaddr/version"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/stretchr/testify/require"
)

// noopSigner is a minimal SignerClient mock that returns dummy signatures
// without blocking on channels.
type noopSigner struct {
	lndclient.SignerClient
}

// SignOutputRaw returns dummy 64-byte signatures for each sign descriptor.
func (s *noopSigner) SignOutputRaw(_ context.Context, _ *wire.MsgTx,
	descs []*lndclient.SignDescriptor, _ []*wire.TxOut) ([][]byte, error) {

	sigs := make([][]byte, len(descs))
	for i := range descs {
		sigs[i] = make([]byte, 64)
	}

	return sigs, nil
}

// TestCreateHtlcSweepTxSweepValue verifies that createHtlcSweepTx derives the
// sweep output value from the HTLC output, not the change output. When a change
// output is present, the sweep must reference the HTLC output value.
func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
	t.Parallel()

	clientKey, err := btcec.NewPrivateKey()
	require.NoError(t, err)
	serverKey, err := btcec.NewPrivateKey()
	require.NoError(t, err)

	network := &chaincfg.RegressionNetParams
	swapHash := lntypes.Hash{1, 2, 3}

	// Create a static address to derive PkScript.
	staticAddr, err := newStaticAddress(
		clientKey.PubKey(), serverKey.PubKey(), 4032,
	)
	require.NoError(t, err)

	pkScript, err := staticAddr.StaticAddressScript()
	require.NoError(t, err)

	addrParams := &script.Parameters{
		ClientPubkey:    clientKey.PubKey(),
		ServerPubkey:    serverKey.PubKey(),
		PkScript:        pkScript,
		Expiry:          4032,
		ProtocolVersion: version.ProtocolVersion_V0,
	}

	depositValue := btcutil.Amount(500_000)
	deposits := []*deposit.Deposit{
		{
			OutPoint: wire.OutPoint{
				Hash:  chainhash.Hash{0xaa},
				Index: 0,
			},
			Value: depositValue,
		},
	}

	feeRate := chainfee.SatPerKWeight(253)
	maxFeePercentage := 0.2

	// SelectedAmount < total triggers a change output.
	selectedAmount := btcutil.Amount(300_000)

	loopIn := &StaticAddressLoopIn{
		SwapHash:              swapHash,
		HtlcCltvExpiry:        800,
		InitiationHeight:      100,
		InitiationTime:        time.Now(),
		ProtocolVersion:       version.ProtocolVersion_V0,
		ClientPubkey:          clientKey.PubKey(),
		ServerPubkey:          serverKey.PubKey(),
		Deposits:              deposits,
		AddressParams:         addrParams,
		HtlcTxFeeRate:         feeRate,
		SelectedAmount:        selectedAmount,
		PaymentTimeoutSeconds: 3600,
	}

	sweepAddr, err := btcutil.NewAddressTaproot(
		make([]byte, 32), network,
	)
	require.NoError(t, err)

	signer := &noopSigner{}

	// Build the HTLC transaction once. It has two outputs with distinct
	// values: the HTLC output and a change output.
	htlcTx, err := loopIn.createHtlcTx(network, feeRate, maxFeePercentage)
	require.NoError(t, err)
	require.Len(t, htlcTx.TxOut, 2, "expected HTLC + change outputs")

	// Identify which output is change and which is HTLC.
	var htlcIdx int
	if bytes.Equal(htlcTx.TxOut[0].PkScript, pkScript) {
		htlcIdx = 1
	}

	htlcValue := htlcTx.TxOut[htlcIdx].Value
	changeValue := htlcTx.TxOut[1-htlcIdx].Value
	require.NotEqual(t, htlcValue, changeValue,
		"HTLC and change values must differ for this test to be "+
			"meaningful")

	// Call createHtlcSweepTx and verify that the sweep output is derived
	// from the HTLC value, not the change.
	sweepTx, err := loopIn.createHtlcSweepTx(
		t.Context(), signer, sweepAddr, feeRate,
		network, uint32(loopIn.HtlcCltvExpiry)+1,
		maxFeePercentage,
	)
	require.NoError(t, err)
	require.Len(t, sweepTx.TxOut, 1)

	sweepValue := sweepTx.TxOut[0].Value
	require.Greater(t, sweepValue, int64(0))
	require.LessOrEqual(t, sweepValue, htlcValue,
		"sweep value must not exceed HTLC output value")
	require.Greater(t, sweepValue, changeValue,
		"sweep value should be greater than change "+
			"value, confirming it was derived from "+
			"the HTLC output")
}

// TestPaymentTimeoutDuration verifies that zero timeout values fall back to the
// default payment timeout duration.
func TestPaymentTimeoutDuration(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name                  string
		paymentTimeoutSeconds uint32
		expected              time.Duration
	}{
		{
			name:     "default",
			expected: time.Duration(DefaultPaymentTimeoutSeconds) * time.Second,
		},
		{
			name:                  "configured",
			paymentTimeoutSeconds: 42,
			expected:              42 * time.Second,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			loopIn := &StaticAddressLoopIn{
				PaymentTimeoutSeconds: test.paymentTimeoutSeconds,
			}

			require.Equal(
				t, test.expected, loopIn.PaymentTimeoutDuration(),
			)
		})
	}
}

// newStaticAddress creates a StaticAddress for testing.
func newStaticAddress(clientKey, serverKey *btcec.PublicKey,
	csvExpiry int64) (*script.StaticAddress, error) {

	return script.NewStaticAddress(
		input.MuSig2Version100RC2, csvExpiry, clientKey, serverKey,
	)
}
