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/mocks"
	types "github.com/filecoin-project/lotus/chain/types"
	gomock "github.com/golang/mock/gomock"
	"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
}

// linter doesn't like dead code, so these are commented out.
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 {
	proto, ok := x.(*api.MessagePrototype)
	if !ok {
		return false
	}

	m := &proto.Message

	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, _ := setupMockSrvcs(t)
		defer srvcs.Close() //nolint:errcheck

		proto, err := srvcs.MessageForSend(ctx, params)
		assert.NoError(t, err)
		assert.True(t, MessageMatcher(params).Matches(proto))
	})

	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

		gomock.InOrder(
			mockApi.EXPECT().WalletDefaultAddress(ctxM).Return(a1, nil),
		)

		proto, err := srvcs.MessageForSend(ctx, params)
		assert.NoError(t, err)
		assert.True(t, mm.Matches(proto))
	})

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

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

		proto, err := srvcs.MessageForSend(ctx, params)
		assert.NoError(t, err)
		assert.True(t, mm.Matches(proto))
	})

	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

		mm := MessageMatcher(params)

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

		proto, err := srvcs.MessageForSend(ctx, params)
		assert.NoError(t, err)
		assert.True(t, mm.Matches(proto))

	})
}