package opworker

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

	"github.com/golang/mock/gomock"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/mock_db"
	"github.com/pingcap/dbaas/central/event"
	"github.com/pingcap/dbaas/central/infra-api/mock_infraapi"
	"github.com/pingcap/dbaas/central/model/util"
	"github.com/pingcap/dbaas/central/model/util/mock_util"
	"github.com/pingcap/dbaas/central/repository/mock_repo"
	"github.com/pingcap/log"
	. "github.com/smartystreets/goconvey/convey"
	v1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/pkgv2/mail/mock_mail"
	"github.com/tidbcloud/pkgv2/pubsub/mock_pubsub"
	"gorm.io/gorm"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestDevTierClusterWorker(t *testing.T) {
	Convey("TestDevTierClusterWorker", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		logger := log.L()
		bgWorker := GlobalWorker{
			logger:  logger,
			workers: nil,
			manager: nil,
		}
		mockMail := mock_mail.NewMockFacade(ctrl)
		mockNotificationMgr := mock_util.NewMockNotificationManager(ctrl)
		mockInfraCli := mock_infraapi.NewMockInfraFacade(ctrl)
		mockTenantRepo := mock_repo.NewMockTenantRepository(ctrl)
		mockUserRepo := mock_repo.NewMockUserRepository(ctrl)
		mockClusterRepo := mock_repo.NewMockClusterRepository(ctrl)
		mockClusterTrafficUsageRepo := mock_repo.NewMockClusterTrafficUsageRepository(ctrl)
		mockPublisher := mock_pubsub.NewMockPublisher(ctrl)
		db := &gorm.DB{}
		mockDBProvider := mock_db.NewFakeTransProvider(db)
		ctx := context.Background()
		worker := NewDevTierClusterWorker(
			bgWorker, mockMail, mockInfraCli,
			mockNotificationMgr, mockDBProvider,
			mockTenantRepo, mockUserRepo, mockClusterRepo, mockClusterTrafficUsageRepo, mockPublisher)
		projectID := entity.ID(111)
		tenantID := entity.ID(123)
		tenantUserID := entity.ID(456)
		tenant := entity.Tenant{
			Updatable: entity.Updatable{Base: entity.Base{ID: tenantID}},
			Email:     "tenant_email@test.com",
		}
		tenantUser := entity.User{
			Updatable: entity.Updatable{Base: entity.Base{ID: tenantUserID}},
			Email:     tenant.Email,
			FirstName: "first name of tenantUser",
		}
		cluster1 := entity.Cluster{
			Updatable: entity.Updatable{Base: entity.Base{ID: 1}},
			Status:    entity.ClusterStatusNormal, Free: true,
			ProjectID: projectID,
			TenantID:  tenantID,
		}

		infraClusterObj := &v1.Cluster{
			Status: v1.ClusterStatus{
				LastActiveTimestamp: &metav1.Time{
					Time: time.Time{},
				},
			},
		}

		Convey("scan idle cluster", func() {
			Convey("should skip when lastActiveTime is Zero", func() {
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)

				err := worker.scanIdleCluster(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should clean cluster idle warning notification identifier when cluster is activating", func() {
				infraClusterObj.Status.LastActiveTimestamp.Time = time.Now()
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockTenantRepo.EXPECT().Load(ctx, cluster1.TenantID).Return(tenant, nil)
				mockUserRepo.EXPECT().LoadByEmail(ctx, tenant.Email).Return(tenantUser, nil)
				mockNotificationMgr.EXPECT().ClearNotificationIdentifier(ctx, gomock.Any(), tenant.ID,
					entity.NotificationSceneDevTierIdle, worker.makeClusterIdleIdentifierByStage(ctx, &cluster1, entity.DevTierIdleStageWarning))

				err := worker.scanIdleCluster(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should send encourage email to the user who their cluster have no activity 2.5 days", func() {
				infraClusterObj.Status.LastActiveTimestamp.Time = time.Now().Add(-2*time.Hour*24 - time.Hour*12)
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockTenantRepo.EXPECT().Load(ctx, cluster1.TenantID).Return(tenant, nil)
				mockUserRepo.EXPECT().LoadByEmail(ctx, tenant.Email).Return(tenantUser, nil)
				mockNotificationMgr.EXPECT().SendNotificationOnce(ctx, gomock.Any(), tenant.ID, entity.NotificationSceneDevTierIdle,
					worker.makeClusterIdleIdentifierByStage(ctx, &cluster1, entity.DevTierIdleStageEncourage), gomock.Any()).DoAndReturn(util.FakeSendNotificationOnce)
				vars := map[string]string{
					"userName": tenantUser.FirstName,
				}
				mockMail.EXPECT().SendMailWithTemplate(ctx, []string{tenantUser.Email}, config.EmailSubjectDevTierClusterIdleEncourage, config.TNDevTierClusterIdleEncourage, vars, nil)

				err := worker.scanIdleCluster(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should send warning email to the user who their cluster have no activity 6 days", func() {
				infraClusterObj.Status.LastActiveTimestamp.Time = time.Now().Add(-time.Hour * 24 * 6)
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockTenantRepo.EXPECT().Load(ctx, cluster1.TenantID).Return(tenant, nil)
				mockUserRepo.EXPECT().LoadByEmail(ctx, tenant.Email).Return(tenantUser, nil)
				mockNotificationMgr.EXPECT().SendNotificationOnce(ctx, gomock.Any(), tenant.ID, entity.NotificationSceneDevTierIdle,
					worker.makeClusterIdleIdentifierByStage(ctx, &cluster1, entity.DevTierIdleStageWarning), gomock.Any()).DoAndReturn(util.FakeSendNotificationOnce)
				vars := map[string]string{
					"userName": tenantUser.FirstName,
				}
				mockMail.EXPECT().SendMailWithTemplate(ctx, []string{tenantUser.Email}, config.EmailSubjectDevTierClusterIdleWarning, config.TNDevTierClusterIdleWarning, vars, nil)

				err := worker.scanIdleCluster(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should send shutdown-email to user and shutdown the cluster which has no activity 7 days", func() {
				const idleDays = 7
				infraClusterObj.Status.LastActiveTimestamp.Time = time.Now().Add(-time.Hour * 24 * idleDays)
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockTenantRepo.EXPECT().Load(ctx, cluster1.TenantID).Return(tenant, nil)
				mockUserRepo.EXPECT().LoadByEmail(ctx, tenant.Email).Return(tenantUser, nil)
				mockNotificationMgr.EXPECT().SendNotificationOnce(ctx, gomock.Any(), tenant.ID, entity.NotificationSceneDevTierIdle,
					worker.makeClusterIdleIdentifierByStage(ctx, &cluster1, entity.DevTierIdleStageShutdown), gomock.Any()).DoAndReturn(util.FakeSendNotificationOnce)
				vars := map[string]string{
					"userName": tenantUser.FirstName,
				}
				mockMail.EXPECT().SendMailWithTemplate(ctx, []string{tenantUser.Email}, config.EmailSubjectDevTierClusterIdleShutdown, config.TNDevTierClusterIdleShutdown, vars, nil)
				mockClusterRepo.EXPECT().DeleteClusterAndMakeFinalBackup(ctx, cluster1.ID)

				err := worker.scanIdleCluster(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
		})
		Convey("sync trafficUsage", func() {
			trafficUsageEn := &entity.ClusterTrafficUsage{
				TenantID:     tenantID,
				ClusterID:    cluster1.ID,
				Period:       "2021:36",
				IngressUsage: 0,
				EgressUsage:  0,
			}
			Convey("should skip when get clusterTrafficUsage failed", func() {
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					nil, fmt.Errorf("some error"),
				)

				err := worker.syncTrafficUsage(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should be success when all is well", func() {
				period := "2021:36"
				infraClusterObj.Annotations = map[string]string{
					trafficUsageAnnKey: `{"ingressUsage":100,"egressUsage":200,"lastAccumulateTime":"2021-09-07T02:57:24.836411357Z"}`,
				}
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockClusterTrafficUsageRepo.EXPECT().LoadByPeriod(ctx, cluster1.ID, period).Return(trafficUsageEn, nil)
				newTrafficUsageEn := *trafficUsageEn
				newTrafficUsageEn.IngressUsage = 100
				newTrafficUsageEn.EgressUsage = 200
				mockClusterTrafficUsageRepo.EXPECT().Save(ctx, &newTrafficUsageEn)

				err := worker.syncTrafficUsage(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
			Convey("should create a new trafficUsage record when record is not found", func() {
				period := "2021:36"
				infraClusterObj.Annotations = map[string]string{
					trafficUsageAnnKey: `{"ingressUsage":100,"egressUsage":200,"lastAccumulateTime":"2021-09-07T02:57:24.836411357Z"}`,
				}
				mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(
					infraClusterObj, nil,
				)
				mockClusterTrafficUsageRepo.EXPECT().LoadByPeriod(ctx, cluster1.ID, period).Return(nil, gorm.ErrRecordNotFound)
				newTrafficUsageEn := *trafficUsageEn
				newTrafficUsageEn.IngressUsage = 100
				newTrafficUsageEn.EgressUsage = 200
				mockClusterTrafficUsageRepo.EXPECT().Save(ctx, &newTrafficUsageEn)

				err := worker.syncTrafficUsage(ctx, []entity.Cluster{cluster1})
				So(err, ShouldBeNil)
			})
		})
		Convey("scan thresholdExceed cluster", func() {
			now := time.Date(2021, 01, 02, 2, 1, 3, 0, time.UTC)
			period := "2020:53"
			ev := &event.ClusterThresholdStateChanged{
				TenantID:         cluster1.TenantID.Uint64(),
				ProjectID:        cluster1.ProjectID.Uint64(),
				ClusterID:        cluster1.ID.Uint64(),
				TriggerTime:      now,
				IsDevTier:        true,
				IngressThrottled: true,
				EgressThrottled:  true,
			}
			Convey("should be success when all is well", func() {
				mockClusterTrafficUsageRepo.EXPECT().SumUsageByTenant(ctx, cluster1.TenantID, period).Return(
					int64(config.MaxTrafficUsagePerWeek4DevTier+10), int64(config.MaxTrafficUsagePerWeek4DevTier+10), nil)
				mockPublisher.EXPECT().Publish(ctx, ev)

				err := worker.scanNetworkThrottledCluster(ctx, []entity.Cluster{cluster1}, now)
				So(err, ShouldBeNil)
			})
			Convey("throttled should be false when usage is LTE than MaxTrafficUsagePerWeek4DevTier", func() {
				mockClusterTrafficUsageRepo.EXPECT().SumUsageByTenant(ctx, cluster1.TenantID, period).Return(
					int64(config.MaxTrafficUsagePerWeek4DevTier), int64(10), nil)
				ev.IngressThrottled = false
				ev.EgressThrottled = false
				mockPublisher.EXPECT().Publish(ctx, ev)

				err := worker.scanNetworkThrottledCluster(ctx, []entity.Cluster{cluster1}, now)
				So(err, ShouldBeNil)
			})
		})
		Convey("makeClusterIdleIdentifierByStage", func() {
			r1 := worker.makeClusterIdleIdentifierByStage(ctx, &cluster1, entity.DevTierIdleStageShutdown)
			So(r1, ShouldEqual, fmt.Sprintf("%d:%s", cluster1.ID, entity.DevTierIdleStageShutdown))
		})
	})
}

func Test_calcDevTierIdleStage(t *testing.T) {
	Convey("Test_calcDevTierIdleStage", t, func() {
		Convey("should return DevTierIdleStageShutdown when idleDuration is 7days", func() {
			idleDuration := 7 * time.Hour * 24
			r1 := calcDevTierIdleStage(idleDuration)
			So(r1, ShouldEqual, entity.DevTierIdleStageShutdown)
		})
		Convey("should return DevTierIdleStageWarning when idleDuration is six and a half days", func() {
			idleDuration := 6*time.Hour*24 + time.Hour*12
			r1 := calcDevTierIdleStage(idleDuration)
			So(r1, ShouldEqual, entity.DevTierIdleStageWarning)
		})
		Convey("should return DevTierIdleStageWarning when idleDuration is 3 days", func() {
			idleDuration := 3 * time.Hour * 24
			r1 := calcDevTierIdleStage(idleDuration)
			So(r1, ShouldEqual, entity.DevTierIdleStageEncourage)
		})
		Convey("should return DevTierIdleStageActive when idleDuration is 1:23:59:59", func() {
			idleDuration := time.Hour*24 + time.Hour*23 + 59*time.Minute + 59*time.Second
			r1 := calcDevTierIdleStage(idleDuration)
			So(r1, ShouldEqual, entity.DevTierIdleStageActive)
		})
	})
}

func TestGetLastActiveTimeOfCluster(t *testing.T) {
	Convey("TestGetLastActiveTimeOfCluster", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockInfraCli := mock_infraapi.NewMockInfraFacade(ctrl)
		ctx := context.Background()
		projectID := entity.ID(111)
		cluster1 := &entity.Cluster{
			Updatable: entity.Updatable{Base: entity.Base{ID: 1}},
			Status:    entity.ClusterStatusNormal, Free: true,
			ProjectID: projectID,
		}
		infraClusterObj := &v1.Cluster{
			Status: v1.ClusterStatus{
				LastActiveTimestamp: &metav1.Time{
					Time: time.Now(),
				},
			}}

		Convey("should be success when all is well", func() {
			mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(infraClusterObj, nil)

			r1, err := GetLastActiveTimeOfCluster(ctx, cluster1, mockInfraCli)
			So(err, ShouldBeNil)
			So(r1, ShouldEqual, infraClusterObj.Status.LastActiveTimestamp.Time)
		})
		Convey("should return error when cluster is not dev tier", func() {
			cluster1.Free = false

			r1, err := GetLastActiveTimeOfCluster(ctx, cluster1, mockInfraCli)
			So(err, ShouldNotBeNil)
			So(r1, ShouldEqual, time.Time{})
		})
		Convey("should return ZERO time when lastActiveTimestamp is nil", func() {
			infraClusterObj.Status.LastActiveTimestamp = nil
			mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster1.ID.Uint64()).Return(infraClusterObj, nil)

			r1, err := GetLastActiveTimeOfCluster(ctx, cluster1, mockInfraCli)
			So(err, ShouldBeNil)
			So(r1, ShouldEqual, time.Time{})
		})
	})
}

func TestGetClusterTrafficUsage(t *testing.T) {
	Convey("TestGetClusterTrafficUsage", t, func() {
		ctrl := gomock.NewController(t)
		defer ctrl.Finish()

		mockInfraCli := mock_infraapi.NewMockInfraFacade(ctrl)
		ctx := context.Background()
		projectID := entity.ID(111)
		cluster := &entity.Cluster{
			Updatable: entity.Updatable{Base: entity.Base{ID: 1}},
			Status:    entity.ClusterStatusNormal, Free: true,
			ProjectID: projectID,
		}
		infraClusterObj := &v1.Cluster{
			Status: v1.ClusterStatus{
				LastActiveTimestamp: &metav1.Time{
					Time: time.Time{},
				},
			},
		}

		Convey("should be success when all is well", func() {
			infraClusterObj.Annotations = map[string]string{
				trafficUsageAnnKey: `{"ingressUsage":10.1,"egressUsage":20,"lastAccumulateTime":"2021-09-07T02:57:24.836411357Z"}`,
			}
			mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster.ID.Uint64()).Return(
				infraClusterObj, nil,
			)

			r1, err := GetClusterTrafficUsage(ctx, cluster, mockInfraCli)
			So(err, ShouldBeNil)
			So(r1, ShouldResemble, &trafficUsageInfo{
				IngressUsage:       10.1,
				EgressUsage:        20,
				LastAccumulateTime: time.Date(2021, 9, 7, 2, 57, 24, 836411357, time.UTC),
			})
		})
		Convey("should return empty trafficUsage when trafficUsageStr is empty", func() {
			infraClusterObj.Annotations = map[string]string{}
			mockInfraCli.EXPECT().GetCluster(ctx, projectID.Uint64(), cluster.ID.Uint64()).Return(
				infraClusterObj, nil,
			)

			r1, err := GetClusterTrafficUsage(ctx, cluster, mockInfraCli)
			So(err, ShouldBeNil)
			So(r1.IngressUsage, ShouldBeZeroValue)
			So(r1.EgressUsage, ShouldBeZeroValue)
		})
		Convey("should return error when cluster is NOT dev tier", func() {
			cluster.Free = false

			r1, err := GetClusterTrafficUsage(ctx, cluster, mockInfraCli)
			So(err, ShouldNotBeNil)
			So(r1, ShouldBeNil)
		})
	})
}
