package loop

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/loopdb"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightninglabs/loop/sweep"
	"github.com/lightninglabs/loop/sweepbatcher"
	"github.com/lightninglabs/loop/test"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/lnrpc"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/stretchr/testify/require"
)

var (
	testPreimage = lntypes.Preimage([32]byte{
		1, 1, 1, 1, 2, 2, 2, 2,
		3, 3, 3, 3, 4, 4, 4, 4,
		1, 1, 1, 1, 2, 2, 2, 2,
		3, 3, 3, 3, 4, 4, 4, 4,
	})
)

// testContext contains functionality to support client unit tests.
type testContext struct {
	test.Context

	serverMock *serverMock
	swapClient *Client
	statusChan chan SwapInfo
	store      *loopdb.StoreMock
	expiryChan chan time.Time
	runErr     chan error
	stop       func()
}

// mockVerifySchnorrSigFail is used to simulate failed taproot keyspend
// signature verification. If passed to the executeConfig we'll test an
// uncooperative server and will fall back to scriptspend sweep.
func mockVerifySchnorrSigFail(pubKey *btcec.PublicKey, hash,
	sig []byte) error {

	return fmt.Errorf("invalid sig")
}

// mockVerifySchnorrSigSuccess is used to simulate successful taproot keyspend
// signature verification. If passed to the executeConfig we'll test an
// uncooperative server and will fall back to scriptspend sweep.
func mockVerifySchnorrSigSuccess(pubKey *btcec.PublicKey, hash,
	sig []byte) error {

	return fmt.Errorf("invalid sig")
}

func mockMuSig2SignSweep(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 newSwapClient(t *testing.T, config *clientConfig) *Client {
	sweeper := &sweep.Sweeper{
		Lnd: config.LndServices,
	}

	lndServices := config.LndServices

	batcherStore := sweepbatcher.NewStoreMock()

	sweepStore, err := sweepbatcher.NewSweepFetcherFromSwapStore(
		config.Store, config.LndServices.ChainParams,
	)
	require.NoError(t, err)

	batcher := sweepbatcher.NewBatcher(
		config.LndServices.WalletKit, config.LndServices.ChainNotifier,
		config.LndServices.Signer, mockMuSig2SignSweep,
		mockVerifySchnorrSigSuccess, config.LndServices.ChainParams,
		batcherStore, sweepStore,
	)

	executor := newExecutor(&executorConfig{
		lnd:               lndServices,
		store:             config.Store,
		sweeper:           sweeper,
		batcher:           batcher,
		createExpiryTimer: config.CreateExpiryTimer,
		cancelSwap:        config.Server.CancelLoopOutSwap,
		verifySchnorrSig:  mockVerifySchnorrSigFail,
	})

	return &Client{
		errChan:      make(chan error),
		clientConfig: *config,
		lndServices:  lndServices,
		sweeper:      sweeper,
		executor:     executor,
		resumeReady:  make(chan struct{}),
		abandonChans: make(map[lntypes.Hash]chan struct{}),
	}
}

func createClientTestContext(t *testing.T,
	pendingSwaps []*loopdb.LoopOut) *testContext {

	clientLnd := test.NewMockLnd()
	serverMock := newServerMock(clientLnd)

	store := loopdb.NewStoreMock(t)
	for _, s := range pendingSwaps {
		store.LoopOutSwaps[s.Hash] = s.Contract

		updates := []loopdb.SwapStateData{}
		for _, e := range s.Events {
			updates = append(updates, e.SwapStateData)
		}
		store.LoopOutUpdates[s.Hash] = updates
	}

	expiryChan := make(chan time.Time)
	timerFactory := func(expiry time.Duration) <-chan time.Time {
		return expiryChan
	}

	swapClient := newSwapClient(t, &clientConfig{
		LndServices:       &clientLnd.LndServices,
		Server:            serverMock,
		Store:             store,
		CreateExpiryTimer: timerFactory,
	})

	statusChan := make(chan SwapInfo)

	ctx := &testContext{
		Context: test.NewContext(
			t,
			clientLnd,
		),
		swapClient: swapClient,
		statusChan: statusChan,
		expiryChan: expiryChan,
		store:      store,
		serverMock: serverMock,
	}

	ctx.runErr = make(chan error)
	runCtx, stop := context.WithCancel(context.Background())
	ctx.stop = stop

	go func() {
		err := swapClient.Run(runCtx, statusChan)
		log.Errorf("client run: %v", err)
		ctx.runErr <- err
	}()

	return ctx
}

func (ctx *testContext) finish() {
	ctx.stop()
	select {
	case err := <-ctx.runErr:
		require.NoError(ctx.Context.T, err)

	case <-time.After(test.Timeout):
		ctx.Context.T.Fatal("client not stopping")
	}

	ctx.assertIsDone()
}
func (ctx *testContext) assertIsDone() {
	require.NoError(ctx.Context.T, ctx.Context.Lnd.IsDone())
	require.NoError(ctx.Context.T, ctx.store.IsDone())

	select {
	case <-ctx.statusChan:
		ctx.Context.T.Fatalf("not all status updates read")
	default:
	}
}

func (ctx *testContext) assertStored() {
	ctx.Context.T.Helper()

	ctx.store.AssertLoopOutStored()
}

func (ctx *testContext) assertStorePreimageReveal() {
	ctx.Context.T.Helper()

	ctx.store.AssertStorePreimageReveal()
}

func (ctx *testContext) assertStoreFinished(expectedResult loopdb.SwapState) {
	ctx.Context.T.Helper()

	ctx.store.AssertStoreFinished(expectedResult)
}

func (ctx *testContext) assertStatus(expectedState loopdb.SwapState) {
	ctx.Context.T.Helper()

	for {
		select {
		case update := <-ctx.statusChan:
			if update.SwapType != swap.TypeOut {
				continue
			}

			if update.State == expectedState {
				return
			}
		case <-time.After(test.Timeout):
			ctx.Context.T.Fatalf("expected status %v not "+
				"received in time", expectedState)
		}
	}
}

func (ctx *testContext) publishHtlc(script []byte,
	amt btcutil.Amount) wire.OutPoint {

	// Create the htlc tx.
	htlcTx := wire.MsgTx{}
	htlcTx.AddTxIn(&wire.TxIn{
		PreviousOutPoint: wire.OutPoint{},
	})
	htlcTx.AddTxOut(&wire.TxOut{
		PkScript: script,
		Value:    int64(amt),
	})

	htlcTxHash := htlcTx.TxHash()

	// Signal client that script has been published.
	select {
	case ctx.Lnd.ConfChannel <- &chainntnfs.TxConfirmation{
		Tx: &htlcTx,
	}:
	case <-time.After(test.Timeout):
		ctx.Context.T.Fatalf("htlc confirmed not consumed")
	}

	return wire.OutPoint{
		Hash:  htlcTxHash,
		Index: 0,
	}
}

// trackPayment asserts that a call to track payment was sent and sends the
// status provided into the updates channel.
func (ctx *testContext) trackPayment(status lnrpc.Payment_PaymentStatus) {
	trackPayment := ctx.Context.AssertTrackPayment()

	select {
	case trackPayment.Updates <- lndclient.PaymentStatus{
		State: status,
	}:

	case <-time.After(test.Timeout):
		ctx.Context.T.Fatalf("could not send payment update")
	}
}

// assertPreimagePush asserts that we made an attempt to push our preimage to
// the server.
func (ctx *testContext) assertPreimagePush(preimage lntypes.Preimage) {
	select {
	case pushedPreimage := <-ctx.serverMock.preimagePush:
		require.Equal(ctx.Context.T, preimage, pushedPreimage)

	case <-time.After(test.Timeout):
		ctx.Context.T.Fatalf("preimage not pushed")
	}
}

func (ctx *testContext) AssertEpochListeners(numListeners int32) {
	ctx.Context.T.Helper()

	require.Eventually(ctx.Context.T, func() bool {
		return ctx.Lnd.EpochSubscribers() == numListeners
	}, test.Timeout, time.Millisecond*250)
}
