package itest

import (
	"fmt"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
	"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
	"github.com/lightningnetwork/lnd/lntest"
	"github.com/lightningnetwork/lnd/lntest/rpc"
	"github.com/lightningnetwork/lnd/lntest/wait"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/stretchr/testify/require"
)

// testHoldInvoicePersistence tests that a sender to a hold-invoice, can be
// restarted before the payment gets settled, and still be able to receive the
// preimage.
func testHoldInvoicePersistence(ht *lntest.HarnessTest) {
	const (
		chanAmt     = btcutil.Amount(1000000)
		numPayments = 10
		reason      = lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS //nolint:lll
	)

	// Create carol, and clean up when the test finishes.
	carol := ht.NewNode("Carol", nil)

	// Connect Alice to Carol.
	alice, bob := ht.Alice, ht.Bob
	ht.ConnectNodes(alice, carol)

	// Open a channel between Alice and Carol which is private so that we
	// cover the addition of hop hints for hold invoices.
	chanPointAlice := ht.OpenChannel(
		alice, carol, lntest.OpenChannelParams{
			Amt:     chanAmt,
			Private: true,
		},
	)

	// For Carol to include her private channel with Alice as a hop hint,
	// we need Alice to be perceived as a "public" node, meaning that she
	// has at least one public channel in the graph. We open a public
	// channel from Alice -> Bob and wait for Carol to see it.
	chanPointBob := ht.OpenChannel(
		alice, bob, lntest.OpenChannelParams{
			Amt: chanAmt,
		},
	)

	// Wait for Carol to see the open channel Alice-Bob.
	ht.AssertTopologyChannelOpen(carol, chanPointBob)

	// Create preimages for all payments we are going to initiate.
	var preimages []lntypes.Preimage
	for i := 0; i < numPayments; i++ {
		var preimage lntypes.Preimage
		copy(preimage[:], ht.Random32Bytes())
		preimages = append(preimages, preimage)
	}

	// Let Carol create hold-invoices for all the payments.
	var (
		payAmt         = btcutil.Amount(4)
		payReqs        []string
		invoiceStreams []rpc.SingleInvoiceClient
	)

	assertInvoiceState := func(state lnrpc.Invoice_InvoiceState) {
		for _, client := range invoiceStreams {
			ht.AssertInvoiceState(client, state)
		}
	}

	for _, preimage := range preimages {
		payHash := preimage.Hash()

		// Make our invoices private so that we get coverage for adding
		// hop hints.
		invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
			Memo:    "testing",
			Value:   int64(payAmt),
			Hash:    payHash[:],
			Private: true,
		}
		resp := carol.RPC.AddHoldInvoice(invoiceReq)
		payReqs = append(payReqs, resp.PaymentRequest)

		// We expect all of our invoices to have hop hints attached,
		// since Carol and Alice are connected with a private channel.
		// We assert that we have one hop hint present to ensure that
		// we've got coverage for hop hints.
		invoice := alice.RPC.DecodePayReq(resp.PaymentRequest)
		require.Len(ht, invoice.RouteHints, 1)

		stream := carol.RPC.SubscribeSingleInvoice(payHash[:])
		invoiceStreams = append(invoiceStreams, stream)
	}

	// Wait for all the invoices to reach the OPEN state.
	assertInvoiceState(lnrpc.Invoice_OPEN)

	// Let Alice initiate payments for all the created invoices.
	for _, payReq := range payReqs {
		req := &routerrpc.SendPaymentRequest{
			PaymentRequest: payReq,
			TimeoutSeconds: 60,
			FeeLimitSat:    1000000,
		}

		// Wait for inflight status update.
		ht.SendPaymentAndAssertStatus(
			alice, req, lnrpc.Payment_IN_FLIGHT,
		)
	}

	// The payments should now show up in Alice's ListPayments, with a zero
	// preimage, indicating they are not yet settled.
	var zeroPreimg lntypes.Preimage
	err := wait.NoError(func() error {
		payments := ht.AssertNumPayments(alice, numPayments)

		// Gather the payment hashes we are looking for in the
		// response.
		payHashes := make(map[string]struct{})
		for _, preimg := range preimages {
			payHashes[preimg.Hash().String()] = struct{}{}
		}

		for _, payment := range payments {
			_, ok := payHashes[payment.PaymentHash]
			if !ok {
				continue
			}

			// The preimage should NEVER be non-zero at this point.
			require.Equal(ht, zeroPreimg.String(),
				payment.PaymentPreimage,
				"expected zero preimage")

			// We wait for the payment attempt to have been
			// properly recorded in the DB.
			if len(payment.Htlcs) == 0 {
				return fmt.Errorf("no attempt recorded")
			}

			delete(payHashes, payment.PaymentHash)
		}

		if len(payHashes) != 0 {
			return fmt.Errorf("payhash not found in response")
		}

		return nil
	}, defaultTimeout)
	require.NoError(ht, err, "timeout checking alice's payments")

	// Wait for all invoices to be accepted.
	assertInvoiceState(lnrpc.Invoice_ACCEPTED)

	// Restart alice. This to ensure she will still be able to handle
	// settling the invoices after a restart.
	ht.RestartNode(alice)

	// Ensure the connections are made.
	//
	// TODO(yy): we shouldn't need these two lines since the connections
	// are permanent, they'd reconnect automatically upon Alice's restart.
	// However, we'd sometimes see the error `unable to gracefully close
	// channel while peer is offline (try force closing it instead):
	// channel link not found` from closing the channels in the end,
	// indicating there's something wrong with the peer conn. We need to
	// investigate and fix it in peer conn management.
	ht.EnsureConnected(alice, bob)
	ht.EnsureConnected(alice, carol)

	// Now after a restart, we must re-track the payments. We set up a
	// goroutine for each to track their status updates.
	for _, preimg := range preimages {
		hash := preimg.Hash()

		payStream := alice.RPC.TrackPaymentV2(hash[:])
		ht.ReceiveTrackPayment(payStream)

		ht.AssertPaymentStatus(
			alice, preimg, lnrpc.Payment_IN_FLIGHT,
		)
	}

	// Settle invoices half the invoices, cancel the rest.
	for i, preimage := range preimages {
		if i%2 == 0 {
			carol.RPC.SettleInvoice(preimage[:])
			ht.AssertInvoiceState(
				invoiceStreams[i], lnrpc.Invoice_SETTLED,
			)
		} else {
			hash := preimage.Hash()
			carol.RPC.CancelInvoice(hash[:])
			ht.AssertInvoiceState(
				invoiceStreams[i], lnrpc.Invoice_CANCELED,
			)
		}
	}

	// Check that Alice's invoices to be shown as settled and failed
	// accordingly, and preimages matching up.
	for i, preimg := range preimages {
		if i%2 == 0 {
			ht.AssertPaymentStatus(
				alice, preimg, lnrpc.Payment_SUCCEEDED,
			)
		} else {
			payment := ht.AssertPaymentStatus(
				alice, preimg, lnrpc.Payment_FAILED,
			)
			require.Equal(ht, reason, payment.FailureReason,
				"wrong failure reason")
		}
	}

	// Finally, close all channels.
	ht.CloseChannel(alice, chanPointBob)
	ht.CloseChannel(alice, chanPointAlice)
}
