package cli

import (
	"context"
	"fmt"
	"testing"

	"github.com/filecoin-project/go-address"
	"github.com/filecoin-project/go-state-types/big"
	"github.com/filecoin-project/go-state-types/crypto"
	"github.com/filecoin-project/lotus/api"
	mocks "github.com/filecoin-project/lotus/api/v0api/v0mocks"
	types "github.com/filecoin-project/lotus/chain/types"
	gomock "github.com/golang/mock/gomock"
	cid "github.com/ipfs/go-cid"
	"github.com/stretchr/testify/assert"
)

type markerKeyType struct{}

var markerKey = markerKeyType{}

type contextMatcher struct {
	marker *int
}

// Matches returns whether x is a match.
func (cm contextMatcher) Matches(x interface{}) bool {
	ctx, ok := x.(context.Context)
	if !ok {
		return false
	}
	maybeMarker, ok := ctx.Value(markerKey).(*int)
	if !ok {
		return false
	}

	return cm.marker == maybeMarker
}

func (cm contextMatcher) String() string {
	return fmt.Sprintf("Context with Value(%v/%T, %p)", markerKey, markerKey, cm.marker)
}

func ContextWithMarker(ctx context.Context) (context.Context, gomock.Matcher) {
	marker := new(int)
	outCtx := context.WithValue(ctx, markerKey, marker)
	return outCtx, contextMatcher{marker: marker}

}

func setupMockSrvcs(t *testing.T) (*ServicesImpl, *mocks.MockFullNode) {
	mockCtrl := gomock.NewController(t)

	mockApi := mocks.NewMockFullNode(mockCtrl)

	srvcs := &ServicesImpl{
		api:    mockApi,
		closer: mockCtrl.Finish,
	}
	return srvcs, mockApi
}

func fakeSign(msg *types.Message) *types.SignedMessage {
	return &types.SignedMessage{
		Message:   *msg,
		Signature: crypto.Signature{Type: crypto.SigTypeSecp256k1, Data: make([]byte, 32)},
	}
}

func makeMessageSigner() (*cid.Cid, interface{}) {
	smCid := cid.Undef
	return &smCid,
		func(_ context.Context, msg *types.Message, _ *api.MessageSendSpec) (*types.SignedMessage, error) {
			sm := fakeSign(msg)
			smCid = sm.Cid()
			return sm, nil
		}
}

type MessageMatcher SendParams

var _ gomock.Matcher = MessageMatcher{}

// Matches returns whether x is a match.
func (mm MessageMatcher) Matches(x interface{}) bool {
	m, ok := x.(*types.Message)
	if !ok {
		return false
	}

	if mm.From != address.Undef && mm.From != m.From {
		return false
	}
	if mm.To != address.Undef && mm.To != m.To {
		return false
	}

	if types.BigCmp(mm.Val, m.Value) != 0 {
		return false
	}

	if mm.Nonce != nil && *mm.Nonce != m.Nonce {
		return false
	}

	if mm.GasPremium != nil && big.Cmp(*mm.GasPremium, m.GasPremium) != 0 {
		return false
	}
	if mm.GasPremium == nil && m.GasPremium.Sign() != 0 {
		return false
	}

	if mm.GasFeeCap != nil && big.Cmp(*mm.GasFeeCap, m.GasFeeCap) != 0 {
		return false
	}
	if mm.GasFeeCap == nil && m.GasFeeCap.Sign() != 0 {
		return false
	}

	if mm.GasLimit != nil && *mm.GasLimit != m.GasLimit {
		return false
	}

	if mm.GasLimit == nil && m.GasLimit != 0 {
		return false
	}
	// handle rest of options
	return true
}

// String describes what the matcher matches.
func (mm MessageMatcher) String() string {
	return fmt.Sprintf("%#v", SendParams(mm))
}

func TestSendService(t *testing.T) {
	addrGen := address.NewForTestGetter()
	a1 := addrGen()
	a2 := addrGen()

	const balance = 10000

	params := SendParams{
		From: a1,
		To:   a2,
		Val:  types.NewInt(balance - 100),
	}

	ctx, ctxM := ContextWithMarker(context.Background())

	t.Run("happy", func(t *testing.T) {
		params := params
		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		msgCid, sign := makeMessageSigner()
		gomock.InOrder(
			mockApi.EXPECT().WalletBalance(ctxM, params.From).Return(types.NewInt(balance), nil),
			mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
		)

		c, err := srvcs.Send(ctx, params)
		assert.NoError(t, err)
		assert.Equal(t, *msgCid, c)
	})

	t.Run("balance-too-low", func(t *testing.T) {
		params := params
		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		gomock.InOrder(
			mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance-200), nil),
			// no MpoolPushMessage
		)

		c, err := srvcs.Send(ctx, params)
		assert.Equal(t, c, cid.Undef)
		assert.ErrorIs(t, err, ErrSendBalanceTooLow)
	})

	t.Run("force", func(t *testing.T) {
		params := params
		params.Force = true
		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		msgCid, sign := makeMessageSigner()
		gomock.InOrder(
			mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance-200), nil).AnyTimes(),
			mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
		)

		c, err := srvcs.Send(ctx, params)
		assert.NoError(t, err)
		assert.Equal(t, *msgCid, c)
	})

	t.Run("default-from", func(t *testing.T) {
		params := params
		params.From = address.Undef
		mm := MessageMatcher(params)
		mm.From = a1

		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		msgCid, sign := makeMessageSigner()
		gomock.InOrder(
			mockApi.EXPECT().WalletDefaultAddress(ctxM).Return(a1, nil),
			mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance), nil),
			mockApi.EXPECT().MpoolPushMessage(ctxM, mm, nil).DoAndReturn(sign),
		)

		c, err := srvcs.Send(ctx, params)
		assert.NoError(t, err)
		assert.Equal(t, *msgCid, c)
	})

	t.Run("set-nonce", func(t *testing.T) {
		params := params
		n := uint64(5)
		params.Nonce = &n
		mm := MessageMatcher(params)

		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		_, _ = mm, mockApi

		var sm *types.SignedMessage
		gomock.InOrder(
			mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance), nil),
			mockApi.EXPECT().WalletSignMessage(ctxM, a1, mm).DoAndReturn(
				func(_ context.Context, _ address.Address, msg *types.Message) (*types.SignedMessage, error) {
					sm = fakeSign(msg)

					// now we expect MpoolPush with that SignedMessage
					mockApi.EXPECT().MpoolPush(ctxM, sm).Return(sm.Cid(), nil)
					return sm, nil
				}),
		)

		c, err := srvcs.Send(ctx, params)
		assert.NoError(t, err)
		assert.Equal(t, sm.Cid(), c)
	})

	t.Run("gas-params", func(t *testing.T) {
		params := params
		limit := int64(1)
		params.GasLimit = &limit
		gfc := big.NewInt(100)
		params.GasFeeCap = &gfc
		gp := big.NewInt(10)
		params.GasPremium = &gp

		srvcs, mockApi := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck
		msgCid, sign := makeMessageSigner()
		gomock.InOrder(
			mockApi.EXPECT().WalletBalance(ctxM, params.From).Return(types.NewInt(balance), nil),
			mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
		)

		c, err := srvcs.Send(ctx, params)
		assert.NoError(t, err)
		assert.Equal(t, *msgCid, c)
	})
}