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/mockinternet"
	"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_checkIdleInternetOne(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockinternetrw := mockinternet.NewMockInternetReaderWriter(ctl)
	mockappboxr := mockappbox.NewMockAPPBoxReader(ctl)
	idleInternet := &v1alpha1.Internet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-name",
			Namespace: "test-ns",
		},
	}
	idleInternet.Spec.VPCRef = &v1alpha1.Reference{
		Name: "notexisted",
	}
	gc := &internetGCWorker{
		conf:       fakeGCConfig,
		log:        log,
		internetrw: mockinternetrw,
		appboxr:    mockappboxr,
	}

	t.Run("a new idle internet", func(t *testing.T) {
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.NotEmpty(t, internet.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			},
		).Times(1)
		err := gc.checkIdleInternetOne(context.Background(), idleInternet)
		assert.Nil(t, err)
	})

	t.Run("target internet not idle", func(t *testing.T) {
		mockappboxr.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{})
				return nil
			},
		).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Empty(t, internet.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			},
		).Times(1)
		err := gc.checkIdleInternetOne(context.Background(), idleInternet)
		assert.Nil(t, err)
		idleStartTime := idleInternet.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime]
		assert.Empty(t, idleStartTime)
	})
	t.Run("idleTIme > idleTimeThreshold", func(t *testing.T) {
		gc.conf.IdleTimeThreshold = metav1.Duration{Duration: 1 * time.Second}
		idleInternet.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = time.Now().Add(-10 * time.Second).Format(time.RFC3339)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.checkIdleInternetOne(context.Background(), idleInternet)
		assert.Nil(t, err)
		assert.Equal(t, v1alpha1.InternetPhaseReadyForDelete, idleInternet.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)
		idleInternet.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = startTIme
		idleInternet.Status.Phase = v1alpha1.InternetPhaseActive
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.checkIdleInternetOne(context.Background(), idleInternet)
		assert.Nil(t, err)
		assert.Equal(t, v1alpha1.InternetPhaseActive, idleInternet.Status.Phase)
		assert.Equal(t, startTIme, idleInternet.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
	})
	t.Run("update internet failed", func(t *testing.T) {
		gc.conf.IdleTimeThreshold = metav1.Duration{Duration: 1 * time.Second}
		idleInternet.Status.Phase = v1alpha1.InternetPhaseActive
		idleInternet.Annotations[v1alpha1.AnnotationKeyIdelStartTime] = time.Now().Add(-10 * time.Second).Format(time.RFC3339)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).Return(fmt.Errorf("update failed")).Times(1)
		err := gc.checkIdleInternetOne(context.Background(), idleInternet)
		assert.NotNil(t, err)
		assert.Equal(t, v1alpha1.InternetPhaseReadyForDelete, idleInternet.Status.Phase)
	})
}

func Test_isIdleInternet(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockappboxr := mockappbox.NewMockAPPBoxReader(ctl)
	gc := &internetGCWorker{
		conf:    fakeGCConfig,
		log:     log,
		appboxr: mockappboxr,
	}
	ctx := context.Background()
	t.Run("idle appbox", func(t *testing.T) {
		mockappboxr.EXPECT().List(ctx, &v1alpha1.APPBoxList{}, &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.VPCNameField, "test-name-vpc"),
			Namespace:     "test-ns",
		}).Return(nil).Times(1)
		idle := gc.isIdleInternet(ctx, &v1alpha1.Internet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
			Spec: v1alpha1.InternetSpec{
				VPCRef: &v1alpha1.Reference{
					Name: "test-name-vpc",
				},
			},
		})
		assert.True(t, idle)
	})
	t.Run("not idle internet", func(t *testing.T) {
		mockappboxr.EXPECT().List(ctx, &v1alpha1.APPBoxList{}, &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.VPCNameField, "test-name-vpc"),
			Namespace:     "test-ns",
		}).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{})
				return nil
			},
		).Times(1)
		idle := gc.isIdleInternet(ctx, &v1alpha1.Internet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
			Spec: v1alpha1.InternetSpec{
				VPCRef: &v1alpha1.Reference{
					Name: "test-name-vpc",
				},
			},
		})
		assert.False(t, idle)
	})
	t.Run("list appbox failed", func(t *testing.T) {
		mockappboxr.EXPECT().List(ctx, &v1alpha1.APPBoxList{}, &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.VPCNameField, "test-name-vpc"),
			Namespace:     "test-ns",
		}).Return(fmt.Errorf("unknown error")).Times(1)
		idle := gc.isIdleInternet(ctx, &v1alpha1.Internet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "test-ns",
			},
			Spec: v1alpha1.InternetSpec{
				VPCRef: &v1alpha1.Reference{
					Name: "test-name-vpc",
				},
			},
		})
		assert.False(t, idle)
	})
}

func Test_scanIdleInternet(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockappboxr := mockappbox.NewMockAPPBoxReader(ctl)
	mockinternetrw := mockinternet.NewMockInternetReaderWriter(ctl)
	gc := &internetGCWorker{
		conf:       fakeGCConfig,
		log:        log,
		appboxr:    mockappboxr,
		internetrw: mockinternetrw,
	}
	ctx := context.Background()
	t.Run("empty internet list", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("list internet failed", func(t *testing.T) {
		mockinternetrw.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 internet", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-1",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-2",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Equal(t, v1alpha1.InternetPhaseActive, internet.Status.Phase)
				assert.NotEmpty(t, internet.GetAnnotations()[v1alpha1.AnnotationKeyIdelStartTime])
				return nil
			}).Times(2)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("one internet is not active", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-1",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-2",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseCreating,
					},
				})
				return nil
			},
		).Times(1)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Equal(t, v1alpha1.InternetPhaseActive, internet.Status.Phase)
				return nil
			}).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("internet with skipGC annotation", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-1",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.SkipGCAnnotationKey: v1alpha1.SkipGCAnnotationVal,
						},
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-1",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name-2",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc-2",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Equal(t, v1alpha1.InternetPhaseActive, internet.Status.Phase)
				return nil
			}).Times(1)
		err := gc.scanIdle(ctx)
		assert.Nil(t, err)
	})
}

func TestGC_cleanIdleInternet(t *testing.T) {
	log := logr.Discard()
	ctl := gomock.NewController(t)
	defer ctl.Finish()
	mockappboxr := mockappbox.NewMockAPPBoxReader(ctl)
	mockinternetrw := mockinternet.NewMockInternetReaderWriter(ctl)
	gc := &internetGCWorker{
		conf:       fakeGCConfig,
		log:        log,
		appboxr:    mockappboxr,
		internetrw: mockinternetrw,
	}
	ctx := context.Background()
	t.Run("empty readyForDeleteInternet", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("internet not in ReadyForDelete phase", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name",
						Namespace: "test-ns",
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseActive,
					},
				})
				return nil
			},
		).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("add readyForDeleteStartTime annotation", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name",
						Namespace: "test-ns",
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.NotEmpty(t, internet.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime])
				return nil
			}).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("internet is not idle", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-name",
						Namespace: "test-ns",
						Annotations: map[string]string{
							v1alpha1.AnnotationKeyReadyForDeleteStartTime: time.Now().Format(time.RFC3339),
						},
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		opts := &client.ListOptions{
			FieldSelector: fields.OneTermEqualSelector(controllers.VPCNameField, "test-name-vpc"),
			Namespace:     "test-ns",
		}
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), opts).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.APPBoxList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.APPBox{})
				return nil
			},
		).Times(1)
		mockinternetrw.EXPECT().UpdateStatus(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Equal(t, v1alpha1.InternetPhaseActive, internet.Status.Phase)
				return nil
			},
		).Times(1)
		mockinternetrw.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, internet *v1alpha1.Internet) error {
				assert.Empty(t, internet.GetAnnotations()[v1alpha1.AnnotationKeyReadyForDeleteStartTime])
				return nil
			},
		).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("internet readyForDeleteTime > IdleTimeRetention", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					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),
						},
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		gc.conf.IdleTimeRetention = metav1.Duration{Duration: 5 * time.Second}
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockinternetrw.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
	t.Run("internet readyForDeleteTime < IdleTimeRetention", func(t *testing.T) {
		mockinternetrw.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
			func(ctx context.Context, list *v1alpha1.InternetList, opts ...client.ListOption) error {
				list.Items = append(list.Items, v1alpha1.Internet{
					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),
						},
					},
					Spec: v1alpha1.InternetSpec{
						VPCRef: &v1alpha1.Reference{
							Name: "test-name-vpc",
						},
					},
					Status: v1alpha1.InternetStatus{
						Phase: v1alpha1.InternetPhaseReadyForDelete,
					},
				})
				return nil
			},
		).Times(1)
		gc.conf.IdleTimeRetention = metav1.Duration{Duration: 20 * time.Second}
		mockappboxr.EXPECT().List(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		err := gc.cleanIdle(ctx)
		assert.Nil(t, err)
	})
}
