package paychmgr

import (
	"context"
	"testing"

	"github.com/ipfs/go-cid"
	ds "github.com/ipfs/go-datastore"
	ds_sync "github.com/ipfs/go-datastore/sync"
	"github.com/stretchr/testify/require"

	"github.com/filecoin-project/go-state-types/abi"
	"github.com/filecoin-project/go-state-types/big"
	paychtypes "github.com/filecoin-project/go-state-types/builtin/v8/paych"
	builtin2 "github.com/filecoin-project/specs-actors/v2/actors/builtin"
	tutils2 "github.com/filecoin-project/specs-actors/v2/support/testing"

	"github.com/filecoin-project/lotus/chain/actors/builtin/paych"
	paychmock "github.com/filecoin-project/lotus/chain/actors/builtin/paych/mock"
	"github.com/filecoin-project/lotus/chain/types"
)

// TestPaychAddVoucherAfterAddFunds tests adding a voucher to a channel with
// insufficient funds, then adding funds to the channel, then adding the
// voucher again
func TestPaychAddVoucherAfterAddFunds(t *testing.T) {
	//stm: @TOKEN_PAYCH_WAIT_READY_001
	ctx := context.Background()
	store := NewStore(ds_sync.MutexWrap(ds.NewMapDatastore()))

	fromKeyPrivate, fromKeyPublic := testGenerateKeyPair(t)
	ch := tutils2.NewIDAddr(t, 100)
	from := tutils2.NewSECP256K1Addr(t, string(fromKeyPublic))
	to := tutils2.NewSECP256K1Addr(t, "secpTo")
	fromAcct := tutils2.NewActorAddr(t, "fromAct")
	toAcct := tutils2.NewActorAddr(t, "toAct")

	mock := newMockManagerAPI()
	defer mock.close()

	// Add the from signing key to the wallet
	mock.setAccountAddress(fromAcct, from)
	mock.setAccountAddress(toAcct, to)
	mock.addSigningKey(fromKeyPrivate)

	mgr, err := newManager(store, mock)
	require.NoError(t, err)

	// Send create message for a channel with value 10
	createAmt := big.NewInt(10)
	_, createMsgCid, err := mgr.GetPaych(ctx, from, to, createAmt, onChainReserve)
	require.NoError(t, err)

	// Send create channel response
	response := testChannelResponse(t, ch)
	mock.receiveMsgResponse(createMsgCid, response)

	// Create an actor in state for the channel with the initial channel balance
	act := &types.Actor{
		Code:    builtin2.AccountActorCodeID,
		Head:    cid.Cid{},
		Nonce:   0,
		Balance: createAmt,
	}
	mock.setPaychState(ch, act, paychmock.NewMockPayChState(fromAcct, toAcct, abi.ChainEpoch(0), make(map[uint64]paych.LaneState)))

	// Wait for create response to be processed by manager
	_, err = mgr.GetPaychWaitReady(ctx, createMsgCid)
	require.NoError(t, err)

	// Create a voucher with a value equal to the channel balance
	voucher := paychtypes.SignedVoucher{Amount: createAmt, Lane: 1}
	res, err := mgr.CreateVoucher(ctx, ch, voucher)
	require.NoError(t, err)
	require.NotNil(t, res.Voucher)

	// Create a voucher in a different lane with an amount that exceeds the
	// channel balance
	excessAmt := types.NewInt(5)
	voucher = paychtypes.SignedVoucher{Amount: excessAmt, Lane: 2}
	res, err = mgr.CreateVoucher(ctx, ch, voucher)
	require.NoError(t, err)
	require.Nil(t, res.Voucher)
	require.Equal(t, res.Shortfall, excessAmt)

	// Add funds so as to cover the voucher shortfall
	_, addFundsMsgCid, err := mgr.GetPaych(ctx, from, to, excessAmt, onChainReserve)
	require.NoError(t, err)

	// Trigger add funds confirmation
	mock.receiveMsgResponse(addFundsMsgCid, types.MessageReceipt{ExitCode: 0})

	// Update actor test case balance to reflect added funds
	act.Balance = types.BigAdd(createAmt, excessAmt)

	// Wait for add funds confirmation to be processed by manager
	_, err = mgr.GetPaychWaitReady(ctx, addFundsMsgCid)
	require.NoError(t, err)

	// Adding same voucher that previously exceeded channel balance
	// should succeed now that the channel balance has been increased
	res, err = mgr.CreateVoucher(ctx, ch, voucher)
	require.NoError(t, err)
	require.NotNil(t, res.Voucher)
}