package routing

import (
	"bytes"
	"testing"

	"github.com/btcsuite/btcd/btcec/v2"
	sphinx "github.com/lightningnetwork/lightning-onion"
	"github.com/lightningnetwork/lnd/channeldb/models"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/stretchr/testify/require"
)

// TestBlindedPathValidation tests validation of blinded paths.
func TestBlindedPathValidation(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name    string
		payment *BlindedPayment
		err     error
	}{
		{
			name:    "no path",
			payment: &BlindedPayment{},
			err:     ErrNoBlindedPath,
		},
		{
			name: "insufficient hops",
			payment: &BlindedPayment{
				BlindedPath: &sphinx.BlindedPath{
					BlindedHops: []*sphinx.BlindedHopInfo{},
				},
			},
			err: ErrInsufficientBlindedHops,
		},
		{
			name: "maximum < minimum",
			payment: &BlindedPayment{
				BlindedPath: &sphinx.BlindedPath{
					BlindedHops: []*sphinx.BlindedHopInfo{
						{},
					},
				},
				HtlcMaximum: 10,
				HtlcMinimum: 20,
			},
			err: ErrHTLCRestrictions,
		},
		{
			name: "valid",
			payment: &BlindedPayment{
				BlindedPath: &sphinx.BlindedPath{
					BlindedHops: []*sphinx.BlindedHopInfo{
						{},
					},
				},
				HtlcMaximum: 15,
				HtlcMinimum: 5,
			},
		},
	}

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

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

			err := testCase.payment.Validate()
			require.ErrorIs(t, err, testCase.err)
		})
	}
}

// TestBlindedPaymentToHints tests conversion of a blinded path to a chain of
// route hints. As our function assumes that the blinded payment has already
// been validated.
func TestBlindedPaymentToHints(t *testing.T) {
	t.Parallel()

	var (
		_, pk1  = btcec.PrivKeyFromBytes([]byte{1})
		_, pkb1 = btcec.PrivKeyFromBytes([]byte{2})
		_, pkb2 = btcec.PrivKeyFromBytes([]byte{3})
		_, pkb3 = btcec.PrivKeyFromBytes([]byte{4})

		v1  = route.NewVertex(pk1)
		vb2 = route.NewVertex(pkb2)
		vb3 = route.NewVertex(pkb3)

		baseFee   uint32 = 1000
		ppmFee    uint32 = 500
		cltvDelta uint16 = 140
		htlcMin   uint64 = 100
		htlcMax   uint64 = 100_000_000

		sizeEncryptedData = 100
		cipherText        = bytes.Repeat(
			[]byte{1}, sizeEncryptedData,
		)
		_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})

		rawFeatures = lnwire.NewRawFeatureVector(
			lnwire.AMPOptional,
		)

		features = lnwire.NewFeatureVector(
			rawFeatures, lnwire.Features,
		)
	)

	// Create a blinded payment that's just to the introduction node and
	// assert that we get nil hints.
	blindedPayment := &BlindedPayment{
		BlindedPath: &sphinx.BlindedPath{
			IntroductionPoint: pk1,
			BlindingPoint:     blindedPoint,
			BlindedHops: []*sphinx.BlindedHopInfo{
				{},
			},
		},
		BaseFee:             baseFee,
		ProportionalFeeRate: ppmFee,
		CltvExpiryDelta:     cltvDelta,
		HtlcMinimum:         htlcMin,
		HtlcMaximum:         htlcMax,
		Features:            features,
	}
	require.Nil(t, blindedPayment.toRouteHints())

	// Populate the blinded payment with hops.
	blindedPayment.BlindedPath.BlindedHops = []*sphinx.BlindedHopInfo{
		{
			BlindedNodePub: pkb1,
			CipherText:     cipherText,
		},
		{
			BlindedNodePub: pkb2,
			CipherText:     cipherText,
		},
		{
			BlindedNodePub: pkb3,
			CipherText:     cipherText,
		},
	}

	expected := RouteHints{
		v1: {
			//nolint:lll
			&BlindedEdge{
				policy: &models.CachedEdgePolicy{
					TimeLockDelta: cltvDelta,
					MinHTLC:       lnwire.MilliSatoshi(htlcMin),
					MaxHTLC:       lnwire.MilliSatoshi(htlcMax),
					FeeBaseMSat:   lnwire.MilliSatoshi(baseFee),
					FeeProportionalMillionths: lnwire.MilliSatoshi(
						ppmFee,
					),
					ToNodePubKey: func() route.Vertex {
						return vb2
					},
					ToNodeFeatures: features,
				},
				blindingPoint: blindedPoint,
				cipherText:    cipherText,
			},
		},
		vb2: {
			&BlindedEdge{
				policy: &models.CachedEdgePolicy{
					ToNodePubKey: func() route.Vertex {
						return vb3
					},
					ToNodeFeatures: features,
				},
				cipherText: cipherText,
			},
		},
	}

	actual := blindedPayment.toRouteHints()

	require.Equal(t, len(expected), len(actual))
	for vertex, expectedHint := range expected {
		actualHint, ok := actual[vertex]
		require.True(t, ok, "node not found: %v", vertex)

		require.Len(t, expectedHint, 1)
		require.Len(t, actualHint, 1)

		// We can't assert that our functions are equal, so we check
		// their output and then mark them as nil so that we can use
		// require.Equal for all our other fields.
		require.Equal(t, expectedHint[0].EdgePolicy().ToNodePubKey(),
			actualHint[0].EdgePolicy().ToNodePubKey())

		actualHint[0].EdgePolicy().ToNodePubKey = nil
		expectedHint[0].EdgePolicy().ToNodePubKey = nil

		// The arguments we use for the payload do not matter as long as
		// both functions return the same payload.
		expectedPayloadSize := expectedHint[0].IntermediatePayloadSize(
			0, 0, false, 0,
		)
		actualPayloadSize := actualHint[0].IntermediatePayloadSize(
			0, 0, false, 0,
		)

		require.Equal(t, expectedPayloadSize, actualPayloadSize)

		require.Equal(t, expectedHint[0], actualHint[0])
	}
}
