// stm: #unit
package cli

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

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"

	"github.com/filecoin-project/go-state-types/abi"

	"github.com/filecoin-project/lotus/api"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/lotus/chain/types/mock"
)

func TestSyncStatus(t *testing.T) {
	app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncStatusCmd))
	defer done()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	ts1 := mock.TipSet(mock.MkBlock(nil, 0, 0))
	ts2 := mock.TipSet(mock.MkBlock(ts1, 0, 0))

	start := time.Now()
	end := start.Add(time.Minute)

	state := &api.SyncState{
		ActiveSyncs: []api.ActiveSync{{
			WorkerID: 1,
			Base:     ts1,
			Target:   ts2,
			Stage:    api.StageMessages,
			Height:   abi.ChainEpoch(0),
			Start:    start,
			End:      end,
			Message:  "whatever",
		}},
		VMApplied: 0,
	}

	mockApi.EXPECT().SyncState(ctx).Return(state, nil)

	//stm: @CLI_SYNC_STATUS_001
	err := app.Run([]string{"sync", "status"})
	assert.NoError(t, err)

	out := buf.String()

	// output is plaintext, had to do string matching
	assert.Contains(t, out, fmt.Sprintf("Base:\t[%s]", ts1.Blocks()[0].Cid().String()))
	assert.Contains(t, out, fmt.Sprintf("Target:\t[%s]", ts2.Blocks()[0].Cid().String()))
	assert.Contains(t, out, "Height diff:\t1")
	assert.Contains(t, out, "Stage: message sync")
	assert.Contains(t, out, "Height: 0")
	assert.Contains(t, out, "Elapsed: 1m0s")
}

func TestSyncMarkBad(t *testing.T) {
	app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncMarkBadCmd))
	defer done()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	blk := mock.MkBlock(nil, 0, 0)

	mockApi.EXPECT().SyncMarkBad(ctx, blk.Cid()).Return(nil)

	//stm: @CLI_SYNC_MARK_BAD_001
	err := app.Run([]string{"sync", "mark-bad", blk.Cid().String()})
	assert.NoError(t, err)
}

func TestSyncUnmarkBad(t *testing.T) {
	t.Run("one-block", func(t *testing.T) {
		app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncUnmarkBadCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		blk := mock.MkBlock(nil, 0, 0)

		mockApi.EXPECT().SyncUnmarkBad(ctx, blk.Cid()).Return(nil)

		//stm: @CLI_SYNC_UNMARK_BAD_001
		err := app.Run([]string{"sync", "unmark-bad", blk.Cid().String()})
		assert.NoError(t, err)
	})

	t.Run("all", func(t *testing.T) {
		app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncUnmarkBadCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		mockApi.EXPECT().SyncUnmarkAllBad(ctx).Return(nil)

		//stm: @CLI_SYNC_UNMARK_BAD_002
		err := app.Run([]string{"sync", "unmark-bad", "-all"})
		assert.NoError(t, err)
	})
}

func TestSyncCheckBad(t *testing.T) {
	t.Run("not-bad", func(t *testing.T) {
		app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncCheckBadCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		blk := mock.MkBlock(nil, 0, 0)

		mockApi.EXPECT().SyncCheckBad(ctx, blk.Cid()).Return("", nil)

		//stm: @CLI_SYNC_CHECK_BAD_002
		err := app.Run([]string{"sync", "check-bad", blk.Cid().String()})
		assert.NoError(t, err)

		assert.Contains(t, buf.String(), "block was not marked as bad")
	})

	t.Run("bad", func(t *testing.T) {
		app, mockApi, buf, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncCheckBadCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		blk := mock.MkBlock(nil, 0, 0)
		reason := "whatever"

		mockApi.EXPECT().SyncCheckBad(ctx, blk.Cid()).Return(reason, nil)

		//stm: @CLI_SYNC_CHECK_BAD_001
		err := app.Run([]string{"sync", "check-bad", blk.Cid().String()})
		assert.NoError(t, err)

		assert.Contains(t, buf.String(), reason)
	})
}

func TestSyncCheckpoint(t *testing.T) {
	t.Run("tipset", func(t *testing.T) {
		app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncCheckpointCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		blk := mock.MkBlock(nil, 0, 0)
		ts := mock.TipSet(blk)

		gomock.InOrder(
			mockApi.EXPECT().ChainGetBlock(ctx, blk.Cid()).Return(blk, nil),
			mockApi.EXPECT().SyncCheckpoint(ctx, ts.Key()).Return(nil),
		)

		//stm: @CLI_SYNC_CHECKPOINT_001
		err := app.Run([]string{"sync", "checkpoint", blk.Cid().String()})
		assert.NoError(t, err)
	})

	t.Run("epoch", func(t *testing.T) {
		app, mockApi, _, done := NewMockAppWithFullAPI(t, WithCategory("sync", SyncCheckpointCmd))
		defer done()

		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()

		epoch := abi.ChainEpoch(0)
		blk := mock.MkBlock(nil, 0, 0)
		ts := mock.TipSet(blk)

		gomock.InOrder(
			mockApi.EXPECT().ChainGetTipSetByHeight(ctx, epoch, types.EmptyTSK).Return(ts, nil),
			mockApi.EXPECT().SyncCheckpoint(ctx, ts.Key()).Return(nil),
		)

		//stm: @CLI_SYNC_CHECKPOINT_002
		err := app.Run([]string{"sync", "checkpoint", fmt.Sprintf("-epoch=%d", epoch)})
		assert.NoError(t, err)
	})
}