package action

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/avast/retry-go/v4"
	"github.com/shiningrush/fastflow/pkg/entity"
	"github.com/shiningrush/fastflow/pkg/entity/run"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
)

const maxRunAfterNum = 20

var runErr = errors.New("error")

type runActionTest struct {
	runNum int
}

func (a *runActionTest) Params() interface{} {
	return nil
}

func (a *runActionTest) Run(ctx context.Context, logger *zap.Logger, params interface{}) error {
	a.runNum++
	return runErr
}

type runAfterActionTest struct {
	runActionTest
	runAfterNum int
}

func (a *runAfterActionTest) RunAfter(ctx context.Context, logger *zap.Logger, params interface{}) error {
	a.runAfterNum++
	if a.runAfterNum < maxRunAfterNum {
		return runErr
	} else {
		return StopRetry
	}
}

func TestNewActionWrapper(t *testing.T) {
	testcases := []struct {
		name   string
		action interface{}
	}{
		{
			name:   "run action",
			action: &runActionTest{},
		},
		{
			name:   "run after action",
			action: &runAfterActionTest{},
		},
		{
			name:   "not run action",
			action: 1,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			_, ok := tc.action.(RunAction)
			if !ok {
				assert.Panics(t, func() {
					MustNewActionWrapper(tc.name, tc.action)
				})
			} else {
				wrapper := MustNewActionWrapper(tc.name, tc.action)

				assert.Equal(t, tc.action, wrapper.run)
				require.NotNil(t, wrapper.config)
				assert.NotEmpty(t, wrapper.config.runRetryFun)
				assert.NotEmpty(t, wrapper.config.runAfterRetryFun)
				_, ok := tc.action.(RunAfterAction)
				if ok {
					assert.Equal(t, tc.action, wrapper.runAfter)
				} else {
					assert.Nil(t, wrapper.runAfter)
				}
			}
		})
	}
}

func TestSetRunRetryFunAndRun(t *testing.T) {
	retryNum := 10
	retryFun := func(ctx context.Context, logger *zap.Logger, run func() error) error {
		return retry.Do(func() error {
			return run()
		},
			retry.Context(ctx),
			retry.Attempts(uint(retryNum)),
			retry.DelayType(retry.FixedDelay),
			retry.Delay(time.Millisecond),
			retry.LastErrorOnly(true),
		)
	}
	runAct := &runActionTest{}

	wrapper := MustNewActionWrapper("run action", runAct, SetRunRetryFun(retryFun))

	ctx := context.WithValue(context.Background(), entity.CtxKeyRunningTaskIns, &entity.TaskInstance{})
	dagCtx := run.NewDefExecuteContext(ctx, nil, nil, nil, nil)
	err := wrapper.Run(dagCtx, nil)
	assert.Equal(t, runErr, err)
	assert.Equal(t, retryNum, runAct.runNum)
}

func TestSetRunAfterRetryFunAndRunAfter(t *testing.T) {
	retryNum := 10
	retryFun := func(ctx context.Context, logger *zap.Logger, run func() error) error {
		return retry.Do(func() error {
			return run()
		},
			retry.Context(ctx),
			retry.Attempts(uint(retryNum)),
			retry.DelayType(retry.FixedDelay),
			retry.Delay(time.Millisecond),
			retry.LastErrorOnly(true),
		)
	}
	afterRetryRun := func(ctx context.Context, logger *zap.Logger, run func() error) error {
		return retry.Do(func() error {
			return run()
		},
			retry.Context(ctx),
			retry.Attempts(0),
			retry.DelayType(retry.FixedDelay),
			retry.Delay(time.Millisecond),
			retry.RetryIf(func(err error) bool {
				return err != StopRetry
			}),
			retry.LastErrorOnly(true),
		)
	}
	runAct := &runAfterActionTest{}

	wrapper := MustNewActionWrapper(
		"run action", runAct, SetRunRetryFun(retryFun), SetRunAfterRetryFun(afterRetryRun))

	ctx := context.WithValue(context.Background(), entity.CtxKeyRunningTaskIns, &entity.TaskInstance{})
	dagCtx := run.NewDefExecuteContext(ctx, nil, nil, nil, nil)
	err := wrapper.Run(dagCtx, nil)
	assert.Equal(t, runErr, err)
	assert.Equal(t, retryNum, runAct.runNum)

	err = wrapper.RunAfter(dagCtx, nil)
	assert.Equal(t, StopRetry, err)
	assert.Equal(t, maxRunAfterNum, runAct.runAfterNum)
}
