package gc

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

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/test/resourcemanager/mockappbox"
	"github.com/tidbcloud/infra-provider/regional-server/test/resourcemanager/mockapproute"
	"go.uber.org/mock/gomock"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func Test_checkIdleAPPBoxOne(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockapprouter := mockapproute.NewMockAPPRouteReader(ctl)
	mockappboxrw := mockappbox.NewMockAPPBoxReaderWriter(ctl)
	idleappbox := &v1alpha1.APPBox{
		ObjectMeta: metav1.ObjectMeta{
			Name:        "test-name",
			Namespace:   "test-ns",
			Annotations: map[string]string{},
		},
	}
	gc := &appBoxGCWorker{
		conf:      fakeGCConfig,
		log:       log,
		approuter: mockapprouter,
		appboxrw:  mockappboxrw,
	}

	t.Run("a new idle appbox", func(t *testing.T) {
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.NotEmpty(t, appbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			},
		).Times(1)
		err := gc.checkIdleAPPBoxOne(context.Background(), idleappbox)
		assert.Nil(t, err)
		idleStartTime := idleappbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime]
		assert.NotEmpty(t, idleStartTime)
	})

	t.Run("target appbox not idle", func(t *testing.T) {
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPRoute{})
				return nil
			},
		).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Empty(t, appbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			},
		).Times(1)
		err := gc.checkIdleAPPBoxOne(context.Background(), idleappbox)
		assert.Nil(t, err)
		idleStartTime := idleappbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime]
		assert.Empty(t, idleStartTime)
	})
	t.Run("idleTIme > idleTimeThreshold", func(t *testing.T) {
		gc.conf.IdleTimeThreshold = metav1.Duration{Duration: 1 * time.Second}
		idleappbox.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = time.Now().Add(-10 * time.Second).Format(time.RFC3339)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.checkIdleAPPBoxOne(context.Background(), idleappbox)
		assert.Nil(t, err)
		assert.Equal(t, v1alpha1.APPBoxPhaseReadyForDelete, idleappbox.Status.Phase)
	})
	t.Run("idleTIme < idleTimeThreshold", func(t *testing.T) {
		gc.conf.IdleTimeThreshold = metav1.Duration{Duration: 10 * time.Second}
		startTIme := time.Now().Add(-2 * time.Second).Format(time.RFC3339)
		idleappbox.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = startTIme
		idleappbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.checkIdleAPPBoxOne(context.Background(), idleappbox)
		assert.Nil(t, err)
		assert.Equal(t, v1alpha1.APPBoxPhaseActive, idleappbox.Status.Phase)
		assert.Equal(t, startTIme, idleappbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
	})
	t.Run("update appbox failed", func(t *testing.T) {
		gc.conf.IdleTimeThreshold = metav1.Duration{Duration: 1 * time.Second}
		idleappbox.Status.Phase = v1alpha1.APPBoxPhaseActive
		idleappbox.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = time.Now().Add(-10 * time.Second).Format(time.RFC3339)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).Return(fmt.Errorf("update failed")).Times(1)
		err := gc.checkIdleAPPBoxOne(context.Background(), idleappbox)
		assert.NotNil(t, err)
		assert.Equal(t, v1alpha1.APPBoxPhaseReadyForDelete, idleappbox.Status.Phase)
	})
}

func Test_isIdleAPPBox(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockapprouter := mockapproute.NewMockAPPRouteReader(ctl)
	gc := &appBoxGCWorker{
		conf:      fakeGCConfig,
		log:       log,
		approuter: mockapprouter,
	}
	ctx := context.Background()
	t.Run("idle appbox", func(t *testing.T) {
		mockapprouter.EXPECT().List(ctx, &v1alpha1.APPRouteList{}, &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.APPBoxNameField, "test-name"),
			Namespace:     "test-ns",
		}).Return(nil).Times(1)
		idle := gc.isIdleAPPBox(ctx, &v1alpha1.APPBox{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
		})
		assert.True(t, idle)
	})
	t.Run("not idle appbox", func(t *testing.T) {
		mockapprouter.EXPECT().List(ctx, gomock.Any(), &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.APPBoxNameField, "test-name"),
			Namespace:     "test-ns",
		}).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPRoute{})
				return nil
			},
		).Times(1)
		idle := gc.isIdleAPPBox(ctx, &v1alpha1.APPBox{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
		})
		assert.False(t, idle)
	})
	t.Run("list approute failed", func(t *testing.T) {
		mockapprouter.EXPECT().List(ctx, gomock.Any(), &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.APPBoxNameField, "test-name"),
			Namespace:     "test-ns",
		}).Return(fmt.Errorf("unknown error")).Times(1)
		idle := gc.isIdleAPPBox(ctx, &v1alpha1.APPBox{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
		})
		assert.False(t, idle)
	})
}

func Test_scanIdleAPPbox(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockappboxrw := mockappbox.NewMockAPPBoxReaderWriter(ctl)
	mockapprouter := mockapproute.NewMockAPPRouteReader(ctl)
	gc := &appBoxGCWorker{
		conf:      fakeGCConfig,
		log:       log,
		appboxrw:  mockappboxrw,
		approuter: mockapprouter,
	}
	ctx := context.Background()
	t.Run("empty appbox list", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("list appbox failed", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("list failed")).Times(1)
		err := gc.scanIdle(ctx)
		assert.NotNil(t, err)
	})

	t.Run("find two idle appbox", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Equal(t, v1alpha1.APPBoxPhaseActive, appbox.Status.Phase)
				assert.NotEmpty(t, appbox.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			}).Times(2)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("one appbox is not active", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseCreating,
					},
				})
				return nil
			},
		).Times(1)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Equal(t, v1alpha1.APPBoxPhaseActive, appbox.Status.Phase)
				return nil
			}).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("appbox with skipGC annotation", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.SkipGCAnnotationKey: v1alpha1.SkipGCAnnotationVal,
						},
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Equal(t, v1alpha1.APPBoxPhaseActive, appbox.Status.Phase)
				return nil
			}).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
}

func Test_cleanIdleAPPBox(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockappboxrw := mockappbox.NewMockAPPBoxReaderWriter(ctl)
	mockapprouter := mockapproute.NewMockAPPRouteReader(ctl)
	gc := &appBoxGCWorker{
		conf:      fakeGCConfig,
		log:       log,
		appboxrw:  mockappboxrw,
		approuter: mockapprouter,
	}
	ctx := context.Background()
	t.Run("empty readyForDeleteAPPBox", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("appbox not in ReadyForDelete phase", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("add readyForDeleteStartTime annotation", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.NotEmpty(t, appbox.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime])
				return nil
			}).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("appbox is not idle", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.AnnotationKeyReadyForDeleteStartTime: time.Now().Add(-10 * time.Second).Format(time.RFC3339),
						},
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPRouteList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPRoute{})
				return nil
			},
		).Times(1)
		mockappboxrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Equal(t, v1alpha1.APPBoxPhaseActive, appbox.Status.Phase)
				return nil
			},
		).Times(1)
		mockappboxrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, appbox *v1alpha1.APPBox) error {
				assert.Empty(t, appbox.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime])
				return nil
			},
		).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("appbox readyForDeleteTime > IdleTimeRetention", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.AnnotationKeyReadyForDeleteStartTime: time.Now().Add(-10 * time.Second).Format(time.RFC3339),
						},
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		gc.conf.IdleTimeRetention = metav1.Duration{Duration: 5 * time.Second}
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockappboxrw.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("appbox readyForDeleteTime < IdleTimeRetention", func(t *testing.T) {
		mockappboxrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.AnnotationKeyReadyForDeleteStartTime: time.Now().Add(-10 * time.Second).Format(time.RFC3339),
						},
					},
					Status: v1alpha1.APPBoxStatus{
						Phase: v1alpha1.APPBoxPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		gc.conf.IdleTimeRetention = metav1.Duration{Duration: 20 * time.Second}
		mockapprouter.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
}
