package test

import (
	"context"
	"errors"
	"sync"

	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightningnetwork/lnd/chainntnfs"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/lightningnetwork/lnd/zpay32"
)

var (
	testStartingHeight = int32(600)
	testNodePubkey     = "03f5374b16f0b1f1b49101de1b9d89e0b460bc57ce9c2f9" +
		"132b73dfc76d3704daa"
	testSignature    = []byte{55, 66, 77, 88, 99}
	testSignatureMsg = "test"
)

// NewMockLnd returns a new instance of LndMockServices that can be used in unit
// tests.
func NewMockLnd() *LndMockServices {
	lightningClient := &mockLightningClient{}
	walletKit := &mockWalletKit{
		feeEstimates: make(map[int32]chainfee.SatPerKWeight),
		minRelayFee:  chainfee.FeePerKwFloor,
	}
	chainNotifier := &mockChainNotifier{}
	signer := &mockSigner{}
	invoices := &mockInvoices{}
	router := &mockRouter{}
	versioner := newMockVersioner()

	lnd := LndMockServices{
		LndServices: lndclient.LndServices{
			WalletKit:     walletKit,
			Client:        lightningClient,
			ChainNotifier: chainNotifier,
			Signer:        signer,
			Invoices:      invoices,
			Router:        router,
			ChainParams:   &chaincfg.TestNet3Params,
			Versioner:     versioner,
		},
		SendPaymentChannel:           make(chan PaymentChannelMessage),
		ConfChannel:                  make(chan *chainntnfs.TxConfirmation),
		RegisterConfChannel:          make(chan *ConfRegistration),
		RegisterSpendChannel:         make(chan *SpendRegistration),
		SpendChannel:                 make(chan *chainntnfs.SpendDetail),
		TxPublishChannel:             make(chan *wire.MsgTx),
		SendOutputsChannel:           make(chan wire.MsgTx),
		SettleInvoiceChannel:         make(chan lntypes.Preimage),
		SingleInvoiceSubcribeChannel: make(chan *SingleInvoiceSubscription, 1),

		RouterSendPaymentChannel: make(chan RouterPaymentChannelMessage),
		TrackPaymentChannel:      make(chan TrackPaymentMessage),

		SignOutputRawChannel: make(chan SignOutputRawRequest),

		FailInvoiceChannel:   make(chan lntypes.Hash, 2),
		blockHeightListeners: make([]chan int32, 0),
		Height:               testStartingHeight,
		NodePubkey:           testNodePubkey,
		Signature:            testSignature,
		SignatureMsg:         testSignatureMsg,
		Invoices:             make(map[lntypes.Hash]*lndclient.Invoice),
	}

	lightningClient.lnd = &lnd
	chainNotifier.lnd = &lnd
	walletKit.lnd = &lnd
	invoices.lnd = &lnd
	router.lnd = &lnd
	signer.lnd = &lnd

	// Also simulate the cached info that is loaded on startup.
	info, _ := lightningClient.GetInfo(context.Background())
	version, _ := versioner.GetVersion(context.Background())
	lnd.LndServices.NodeAlias = info.Alias
	lnd.LndServices.NodePubkey = info.IdentityPubkey
	lnd.LndServices.Version = version

	lnd.WaitForFinished = func() {
		chainNotifier.WaitForFinished()
		lightningClient.WaitForFinished()
		invoices.WaitForFinished()
	}

	return &lnd
}

// PaymentChannelMessage is the data that passed through SendPaymentChannel.
type PaymentChannelMessage struct {
	PaymentRequest string
	Done           chan lndclient.PaymentResult
}

// TrackPaymentMessage is the data that passed through TrackPaymentChannel.
type TrackPaymentMessage struct {
	Hash lntypes.Hash

	Updates chan lndclient.PaymentStatus
	Errors  chan error
}

// RouterPaymentChannelMessage is the data that passed through RouterSendPaymentChannel.
type RouterPaymentChannelMessage struct {
	lndclient.SendPaymentRequest

	TrackPaymentMessage
}

// SingleInvoiceSubscription contains the single invoice subscribers.
type SingleInvoiceSubscription struct {
	Hash   lntypes.Hash
	Update chan lndclient.InvoiceUpdate
	Err    chan error
}

// SignOutputRawRequest contains input data for a tx signing request.
type SignOutputRawRequest struct {
	Tx              *wire.MsgTx
	SignDescriptors []*lndclient.SignDescriptor
}

// PublishHandler is optional transaction handler function called upon calling
// the method PublishTransaction.
type PublishHandler func(ctx context.Context, tx *wire.MsgTx,
	label string) error

// LndMockServices provides a full set of mocked lnd services.
type LndMockServices struct {
	lndclient.LndServices

	SendPaymentChannel   chan PaymentChannelMessage
	SpendChannel         chan *chainntnfs.SpendDetail
	TxPublishChannel     chan *wire.MsgTx
	SendOutputsChannel   chan wire.MsgTx
	SettleInvoiceChannel chan lntypes.Preimage
	FailInvoiceChannel   chan lntypes.Hash
	blockHeightListeners []chan int32

	ConfChannel          chan *chainntnfs.TxConfirmation
	RegisterConfChannel  chan *ConfRegistration
	RegisterSpendChannel chan *SpendRegistration

	SingleInvoiceSubcribeChannel chan *SingleInvoiceSubscription

	RouterSendPaymentChannel chan RouterPaymentChannelMessage
	TrackPaymentChannel      chan TrackPaymentMessage

	SignOutputRawChannel chan SignOutputRawRequest

	Height       int32
	NodePubkey   string
	Signature    []byte
	SignatureMsg string

	Transactions  []lndclient.Transaction
	Sweeps        []string
	SweepsVerbose []lnwallet.TransactionDetail

	// Invoices is a set of invoices that have been created by the mock,
	// keyed by hash string.
	Invoices map[lntypes.Hash]*lndclient.Invoice

	Channels            []lndclient.ChannelInfo
	ChannelEdges        map[uint64]*lndclient.ChannelEdge
	ClosedChannels      []lndclient.ClosedChannel
	ForwardingEvents    []lndclient.ForwardingEvent
	Payments            []lndclient.Payment
	MissionControlState []lndclient.MissionControlEntry

	WaitForFinished func()

	PublishHandler PublishHandler

	lock sync.Mutex
}

// EpochSubscribers returns the number of subscribers to block epoch
// notifications.
func (s *LndMockServices) EpochSubscribers() int32 {
	s.lock.Lock()
	defer s.lock.Unlock()

	return int32(len(s.blockHeightListeners))
}

// NotifyHeight notifies a new block height.
func (s *LndMockServices) NotifyHeight(height int32) error {
	s.lock.Lock()
	defer s.lock.Unlock()
	s.Height = height

	for _, listener := range s.blockHeightListeners {
		lis := listener
		go func() {
			lis <- height
		}()
	}

	return nil
}

// AddTx marks the given transaction as relevant.
func (s *LndMockServices) AddTx(tx *wire.MsgTx) {
	s.lock.Lock()
	s.Transactions = append(s.Transactions, lndclient.Transaction{
		Tx: tx.Copy(),
	})
	s.lock.Unlock()
}

// IsDone checks whether all channels have been fully emptied. If not this may
// indicate unexpected behaviour of the code under test.
func (s *LndMockServices) IsDone() error {
	select {
	case <-s.SendPaymentChannel:
		return errors.New("SendPaymentChannel not empty")
	default:
	}

	select {
	case <-s.SpendChannel:
		return errors.New("SpendChannel not empty")
	default:
	}

	select {
	case <-s.TxPublishChannel:
		return errors.New("TxPublishChannel not empty")
	default:
	}

	select {
	case <-s.SendOutputsChannel:
		return errors.New("SendOutputsChannel not empty")
	default:
	}

	select {
	case <-s.SettleInvoiceChannel:
		return errors.New("SettleInvoiceChannel not empty")
	default:
	}

	select {
	case <-s.ConfChannel:
		return errors.New("ConfChannel not empty")
	default:
	}

	select {
	case <-s.RegisterConfChannel:
		return errors.New("RegisterConfChannel not empty")
	default:
	}

	select {
	case <-s.RegisterSpendChannel:
		return errors.New("RegisterSpendChannel not empty")
	default:
	}

	return nil
}

// DecodeInvoice decodes a payment request string.
func (s *LndMockServices) DecodeInvoice(request string) (*zpay32.Invoice,
	error) {

	return zpay32.Decode(request, s.ChainParams)
}

func (s *LndMockServices) SetFeeEstimate(confTarget int32,
	feeEstimate chainfee.SatPerKWeight) {

	s.LndServices.WalletKit.(*mockWalletKit).setFeeEstimate(
		confTarget, feeEstimate,
	)
}

func (s *LndMockServices) SetMinRelayFee(feeEstimate chainfee.SatPerKWeight) {
	s.LndServices.WalletKit.(*mockWalletKit).setMinRelayFee(feeEstimate)
}

// SetListUnspent sets the list of UTXOs returned by the mock's WalletKit
// ListUnspent call.
func (s *LndMockServices) SetListUnspent(utxos []*lnwallet.Utxo) {
	s.LndServices.WalletKit.(*mockWalletKit).setListUnspent(utxos)
}
