package loop

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/test"
	invpkg "github.com/lightningnetwork/lnd/invoices"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/lightningnetwork/lnd/zpay32"
	"github.com/stretchr/testify/require"
)

var (
	testTime = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)

	testLoopOutMinOnChainCltvDelta = int32(30)
	testLoopOutMaxOnChainCltvDelta = int32(40)
	testChargeOnChainCltvDelta     = int32(100)
	testSwapFee                    = btcutil.Amount(210)
	testFixedPrepayAmount          = btcutil.Amount(100)
	testMinSwapAmount              = btcutil.Amount(10000)
	testMaxSwapAmount              = btcutil.Amount(1000000)
)

// serverMock is used in client unit tests to simulate swap server behaviour.
type serverMock struct {
	expectedSwapAmt  btcutil.Amount
	swapInvoiceAmt   btcutil.Amount
	prepayInvoiceAmt btcutil.Amount

	height int32

	swapInvoice string
	swapHash    lntypes.Hash
	prepayHash  lntypes.Hash

	// preimagePush is a channel that preimage pushes are sent into.
	preimagePush chan lntypes.Preimage

	// cancelSwap is a channel that swap cancellations are sent into.
	cancelSwap chan *outCancelDetails

	lnd *test.LndMockServices
}

var _ swapServerClient = (*serverMock)(nil)

func newServerMock(lnd *test.LndMockServices) *serverMock {
	return &serverMock{
		expectedSwapAmt: 50000,

		// Total swap fee: 1000 + 0.01 * 50000 = 1050
		swapInvoiceAmt:   50950,
		prepayInvoiceAmt: 100,

		height: 600,

		preimagePush: make(chan lntypes.Preimage),
		cancelSwap:   make(chan *outCancelDetails),

		lnd: lnd,
	}
}

func (s *serverMock) NewLoopOutSwap(_ context.Context, swapHash lntypes.Hash,
	amount btcutil.Amount, _ int32, _ [33]byte, _ time.Time,
	_ string) (*newLoopOutResponse, error) {

	_, senderKey := test.CreateKey(100)

	if amount != s.expectedSwapAmt {
		return nil, errors.New("unexpected test swap amount")
	}

	s.swapHash = swapHash
	swapPayReqString, err := getInvoice(swapHash, s.swapInvoiceAmt,
		swapInvoiceDesc)
	if err != nil {
		return nil, err
	}

	// Set the prepay hash to be different from the swap hash.
	s.prepayHash = swapHash
	s.prepayHash[0] ^= 1
	prePayReqString, err := getInvoice(s.prepayHash, s.prepayInvoiceAmt,
		prepayInvoiceDesc)
	if err != nil {
		return nil, err
	}

	var senderKeyArray [33]byte
	copy(senderKeyArray[:], senderKey.SerializeCompressed())

	return &newLoopOutResponse{
		senderKey:     senderKeyArray,
		swapInvoice:   swapPayReqString,
		prepayInvoice: prePayReqString,
	}, nil
}

func (s *serverMock) GetLoopOutTerms(ctx context.Context, initiator string) (
	*LoopOutTerms, error) {

	return &LoopOutTerms{
		MinSwapAmount: testMinSwapAmount,
		MaxSwapAmount: testMaxSwapAmount,
		MinCltvDelta:  testLoopOutMinOnChainCltvDelta,
		MaxCltvDelta:  testLoopOutMaxOnChainCltvDelta,
	}, nil
}

func (s *serverMock) GetLoopOutQuote(ctx context.Context, amt btcutil.Amount,
	expiry int32, _ time.Time, initiator string) (*LoopOutQuote, error) {

	dest := [33]byte{1, 2, 3}

	return &LoopOutQuote{
		SwapFee:         testSwapFee,
		SwapPaymentDest: dest,
		PrepayAmount:    testFixedPrepayAmount,
	}, nil
}

func getInvoice(hash lntypes.Hash, amt btcutil.Amount, memo string) (string, error) {
	// Set different payment addresses for swap invoices.
	payAddr := [32]byte{1, 2, 3}
	if memo == swapInvoiceDesc {
		payAddr = [32]byte{3, 2, 1}
	}

	req, err := zpay32.NewInvoice(
		&chaincfg.TestNet3Params, hash, testTime,
		zpay32.Description(memo),
		zpay32.Amount(lnwire.MilliSatoshi(1000*amt)),
		zpay32.PaymentAddr(payAddr),
	)
	if err != nil {
		return "", err
	}

	reqString, err := test.EncodePayReq(req)
	if err != nil {
		return "", err
	}

	return reqString, nil
}

func (s *serverMock) NewLoopInSwap(_ context.Context, swapHash lntypes.Hash,
	amount btcutil.Amount, _, _ [33]byte, swapInvoice, _ string,
	_ *route.Vertex, _ string) (*newLoopInResponse, error) {

	_, receiverKey := test.CreateKey(101)
	_, receiverInternalKey := test.CreateKey(102)

	if amount != s.expectedSwapAmt {
		return nil, errors.New("unexpected test swap amount")
	}

	var receiverKeyArray, receiverInternalKeyArray [33]byte
	copy(receiverKeyArray[:], receiverKey.SerializeCompressed())
	copy(
		receiverInternalKeyArray[:],
		receiverInternalKey.SerializeCompressed(),
	)

	s.swapInvoice = swapInvoice
	s.swapHash = swapHash

	// Simulate the server paying the probe invoice and expect the client to
	// cancel the probe payment.
	probeSub := <-s.lnd.SingleInvoiceSubcribeChannel
	probeSub.Update <- lndclient.InvoiceUpdate{
		Invoice: lndclient.Invoice{
			State: invpkg.ContractAccepted,
		},
	}
	<-s.lnd.FailInvoiceChannel

	resp := &newLoopInResponse{
		expiry:              s.height + testChargeOnChainCltvDelta,
		receiverKey:         receiverKeyArray,
		receiverInternalKey: receiverInternalKeyArray,
	}

	return resp, nil
}

func (s *serverMock) PushLoopOutPreimage(_ context.Context,
	preimage lntypes.Preimage) error {

	// Push the preimage into the mock's preimage channel.
	s.preimagePush <- preimage

	return nil
}

// CancelLoopOutSwap pushes a request to cancel a swap into our mock's channel.
func (s *serverMock) CancelLoopOutSwap(ctx context.Context,
	details *outCancelDetails) error {

	s.cancelSwap <- details
	return nil
}

func (s *serverMock) assertSwapCanceled(t *testing.T, details *outCancelDetails) {
	require.Equal(t, details, <-s.cancelSwap)
}

func (s *serverMock) GetLoopInTerms(ctx context.Context, initiator string) (
	*LoopInTerms, error) {

	return &LoopInTerms{
		MinSwapAmount: testMinSwapAmount,
		MaxSwapAmount: testMaxSwapAmount,
	}, nil
}

func (s *serverMock) GetLoopInQuote(context.Context, btcutil.Amount,
	route.Vertex, *route.Vertex, [][]zpay32.HopHint, string,
	uint32, bool) (*LoopInQuote, error) {

	return &LoopInQuote{
		SwapFee:   testSwapFee,
		CltvDelta: testChargeOnChainCltvDelta,
	}, nil
}

// SubscribeLoopOutUpdates provides a mocked implementation of state
// subscriptions.
func (s *serverMock) SubscribeLoopOutUpdates(_ context.Context,
	_ lntypes.Hash) (<-chan *ServerUpdate, <-chan error, error) {

	return nil, nil, nil
}

// SubscribeLoopInUpdates provides a mocked implementation of state subscriptions.
func (s *serverMock) SubscribeLoopInUpdates(_ context.Context,
	_ lntypes.Hash) (<-chan *ServerUpdate, <-chan error, error) {

	return nil, nil, nil
}

func (s *serverMock) Probe(ctx context.Context, amt btcutil.Amount,
	pubKey route.Vertex, lastHop *route.Vertex,
	routeHints [][]zpay32.HopHint) error {

	return nil
}

func (s *serverMock) RecommendRoutingPlugin(_ context.Context, _ lntypes.Hash,
	_ [32]byte) (RoutingPluginType, error) {

	return RoutingPluginNone, nil
}

func (s *serverMock) ReportRoutingResult(_ context.Context, _ lntypes.Hash,
	_ [32]byte, _ RoutingPluginType, _ bool, _ int32, _ int64) error {

	return nil
}

func (s *serverMock) MuSig2SignSweep(_ context.Context, _ loopdb.ProtocolVersion,
	_ lntypes.Hash, _ [32]byte, _ []byte, _ []byte) ([]byte,
	[]byte, error) {

	return nil, nil, nil
}

func (s *serverMock) MultiMuSig2SignSweep(ctx context.Context,
	protocolVersion loopdb.ProtocolVersion, swapHash lntypes.Hash,
	paymentAddr [32]byte, nonce []byte, sweepTxPsbt []byte,
	prevoutMap map[wire.OutPoint]*wire.TxOut) (
	[]byte, []byte, error) {

	return nil, nil, nil
}

func (s *serverMock) PushKey(_ context.Context, _ loopdb.ProtocolVersion,
	_ lntypes.Hash, _ [32]byte) error {

	return nil
}

func (s *serverMock) FetchL402(_ context.Context) error {
	return nil
}
