package task

import (
	"context"
	"testing"
	"time"

	maintent "github.com/tidbcloud/devops/entity/maintenance"

	"github.com/onsi/gomega"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"go.uber.org/mock/gomock"

	k8sent "github.com/tidbcloud/devops/entity/k8supgrade"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	cmxapi "github.com/tidbcloud/devops/pkg/service/clustermaintenance/api"
	"github.com/tidbcloud/devops/pkg/service/k8supgrade/generator"
	"github.com/tidbcloud/devops/pkg/service/k8supgrade/infrainfo"
)

func testLegacyUO(phase cloudv1.UpgradeOperationPhase) *k8sent.UpgradeOperation {
	return &k8sent.UpgradeOperation{
		MaintenancePolicyMode: MaintenancePolicyModeLegacy,
		Status:                &k8sent.UpgradeOperationStatus{Phase: string(phase)},
	}
}

func Test_svc_syncMaintenanceWindow(t *testing.T) {
	tests := []struct {
		name         string
		task         *k8sent.Task
		cluster      *k8sent.K8sCluster
		currentUO    *k8sent.UpgradeOperation
		mockCalls    func(*cmxapi.MockServiceAPI)
		wantSchedule time.Time
		wantClosed   bool
		wantErr      bool
	}{
		{
			name: "UO CR does not exist yet",
			task: &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID: pointer.Pointer(uint64(8)),
				Info: &k8sent.K8sInfo{
					ProjectID:            "11111",
					NumberOfTiDBClusters: 1,
					TiDBClusters:         []*k8sent.TiDBClusterInfraInfo{{ID: "123"}},
				},
			},
			currentUO: nil,
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           28,
						ProjectID:    11111,
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusRunning,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:        0,
								TaskID:    28,
								ClusterID: 123,
								Status:    maintent.ClusterMaintenanceExecutionStatusPending,
							},
						},
					},
				}, nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			name:         "a running task without tidb clusters",
			task:         &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster:      &k8sent.K8sCluster{Info: &k8sent.K8sInfo{NumberOfTiDBClusters: 0}},
			currentUO:    testLegacyUO(cloudv1.UpgradeOperationPhasePending),
			wantSchedule: deadline,
			wantClosed:   true,
		},
		{
			name:      "a running task without tidb clusters, with a maintenance window",
			task:      &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster:   &k8sent.K8sCluster{MaintenanceWindowID: pointer.Pointer(uint64(8)), Info: &k8sent.K8sInfo{NumberOfTiDBClusters: 0}},
			currentUO: testLegacyUO(cloudv1.UpgradeOperationPhasePending),
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           2,
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusPending,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:           0,
								TaskID:       2,
								ClusterID:    12345,
								Status:       maintent.ClusterMaintenanceExecutionStatusCanceled,
								CancelReason: "REASON_CLUSTER_DELETED",
							},
						},
					},
				}, nil)
				mockAPI.EXPECT().CloseMaintenance(gomock.Any(), uint64(8)).Return(nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
			wantClosed:   true,
		},
		{
			name:      "a running task without tidb clusters, with a closed maintenance window",
			task:      &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster:   &k8sent.K8sCluster{MaintenanceWindowID: pointer.Pointer(uint64(8)), Info: &k8sent.K8sInfo{NumberOfTiDBClusters: 0}},
			currentUO: testLegacyUO(cloudv1.UpgradeOperationPhasePending),
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           9,
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusClosed,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:           0,
								TaskID:       9,
								ClusterID:    12345,
								Status:       maintent.ClusterMaintenanceExecutionStatusCanceled,
								CancelReason: "REASON_CLUSTER_DELETED",
							},
						},
					},
				}, nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
			wantClosed:   true,
		},
		{
			name: "a running task with tidb clusters, without maintenance windows",
			task: &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster: &k8sent.K8sCluster{
				Info: &k8sent.K8sInfo{
					ProjectID:            "11111",
					NumberOfTiDBClusters: 1,
					TiDBClusters:         []*k8sent.TiDBClusterInfraInfo{{ID: "123"}},
				},
			},
			currentUO: testLegacyUO(cloudv1.UpgradeOperationPhasePending),
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().CreateMaintenance(gomock.Any(), gomock.Any()).Return(pointer.Pointer(uint64(8)), nil)
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           19,
						ProjectID:    0,
						CreateAt:     time.Time{},
						Description:  "",
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusPending,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:        1,
								TaskID:    19,
								ClusterID: 123,
								Status:    maintent.ClusterMaintenanceExecutionStatusPending,
							},
						},
					},
				}, nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			name: "a running task with tidb clusters and a maintenance windows",
			task: &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhaseRunning),
				Info: &k8sent.K8sInfo{
					ProjectID:            "11111",
					NumberOfTiDBClusters: 1,
					TiDBClusters:         []*k8sent.TiDBClusterInfraInfo{{ID: "123"}},
				}},
			currentUO: testLegacyUO(cloudv1.UpgradeOperationPhaseRunning),
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           28,
						ProjectID:    11111,
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusRunning,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:        0,
								TaskID:    28,
								ClusterID: 123,
								Status:    maintent.ClusterMaintenanceExecutionStatusPending,
							},
						},
					},
				}, nil)
				mockAPI.EXPECT().StartMaintenanceExecution(gomock.Any(), uint64(8), uint64(0)).Return(nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
		},
		{
			name: "a running task with a completed UpgradeOperation",
			task: &k8sent.Task{Deadline: deadline.Unix(), Status: k8sent.TaskStatusRunning},
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhaseCompleted),
				Info: &k8sent.K8sInfo{
					ProjectID:            "11111",
					NumberOfTiDBClusters: 1,
					TiDBClusters:         []*k8sent.TiDBClusterInfraInfo{{ID: "123"}},
				},
			},
			currentUO: testLegacyUO(cloudv1.UpgradeOperationPhaseCompleted),
			mockCalls: func(mockAPI *cmxapi.MockServiceAPI) {
				mockAPI.EXPECT().GetMaintenance(gomock.Any(), uint64(8), gomock.Any()).Return(&maintent.ClusterMaintenance{
					ClusterMaintenanceInRepo: maintent.ClusterMaintenanceInRepo{ID: 8},
					Task: maintent.ClusterMaintenanceTask{
						ID:           28,
						ProjectID:    11111,
						ScheduleTime: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
						Deadline:     &time.Time{},
						Status:       maintent.ClusterMaintenanceTaskStatusRunning,
						Executions: []maintent.ClusterMaintenanceExecution{
							{
								ID:        0,
								TaskID:    28,
								ClusterID: 123,
								Status:    maintent.ClusterMaintenanceExecutionStatusRunning,
							},
						},
					},
				}, nil)
				mockAPI.EXPECT().FinishMaintenanceExecution(gomock.Any(), uint64(8), uint64(0)).Return(nil)
				mockAPI.EXPECT().CloseMaintenance(gomock.Any(), uint64(8)).Return(nil)
			},
			wantSchedule: time.Date(2024, 11, 1, 0, 0, 0, 0, time.UTC),
			wantClosed:   true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockMaintenanceWindowSvc := cmxapi.NewMockServiceAPI(ctrl)
			if tt.mockCalls != nil {
				tt.mockCalls(mockMaintenanceWindowSvc)
			}

			s := &svc{
				maintenanceWindowSvc: mockMaintenanceWindowSvc,
			}
			gotSchedule, gotClosed, err := s.syncMaintenanceWindow(context.TODO(), k8sent.NewReadOnlyTask(tt.task), tt.cluster, tt.currentUO)
			if (err != nil) != tt.wantErr {
				t.Errorf("syncMaintenanceWindow() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !gotSchedule.Equal(tt.wantSchedule) {
				t.Errorf("syncMaintenanceWindow() gotSchedule = %v, want %v", gotSchedule, tt.wantSchedule)
			}
			if gotClosed != tt.wantClosed {
				t.Errorf("syncMaintenanceWindow() gotClosed = %v, want %v", gotClosed, tt.wantClosed)
			}
		})
	}
}

func Test_shouldReportMaintenanceWindowNotCreated(t *testing.T) {
	tests := []struct {
		name    string
		cluster *k8sent.K8sCluster
		want    bool
	}{
		{
			name: "cluster needs maintenance window but it is not created",
			cluster: &k8sent.K8sCluster{
				Info: &k8sent.K8sInfo{
					NumberOfTiDBClusters: 1,
				},
			},
			want: true,
		},
		{
			name: "maintenance window is already created",
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID: pointer.Pointer(uint64(8)),
				Info: &k8sent.K8sInfo{
					NumberOfTiDBClusters: 1,
				},
			},
		},
		{
			name: "cluster does not need maintenance window",
			cluster: &k8sent.K8sCluster{
				Info: &k8sent.K8sInfo{
					NumberOfTiDBClusters: 0,
				},
			},
		},
		{
			name:    "k8s info is missing",
			cluster: &k8sent.K8sCluster{},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := shouldReportMaintenanceWindowNotCreated(tt.cluster); got != tt.want {
				t.Errorf("shouldReportMaintenanceWindowNotCreated() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_shouldReportK8sClusterUONotStart(t *testing.T) {
	now := time.Now()
	tests := []struct {
		name     string
		schedule time.Time
		closed   bool
		cluster  *k8sent.K8sCluster
		want     bool
	}{
		{
			name:     "maintenance window started but upgrade operation is still pending",
			schedule: now.Add(-15 * time.Minute),
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhasePending),
			},
			want: true,
		},
		{
			name:     "maintenance window has not started",
			schedule: now.Add(time.Minute),
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhasePending),
			},
		},
		{
			name:     "upgrade operation is running",
			schedule: now.Add(-15 * time.Minute),
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhaseRunning),
			},
		},
		{
			name:     "maintenance window is closed",
			schedule: now.Add(-15 * time.Minute),
			closed:   true,
			cluster: &k8sent.K8sCluster{
				MaintenanceWindowID:    pointer.Pointer(uint64(8)),
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhasePending),
			},
		},
		{
			name:     "maintenance window is missing",
			schedule: now.Add(-15 * time.Minute),
			cluster: &k8sent.K8sCluster{
				UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhasePending),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := shouldReportK8sClusterUONotStart(tt.schedule, tt.closed, tt.cluster); got != tt.want {
				t.Errorf("shouldReportK8sClusterUONotStart() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_svc_syncUpgradeOperation(t *testing.T) {
	tests := []struct {
		name      string
		task      *k8sent.Task
		cluster   *k8sent.K8sCluster
		schedule  time.Time
		currentUO *k8sent.UpgradeOperation
		mockCalls func(*infrainfo.MockServiceAPI, *generator.MockAPI)
		expect    func(*gomega.WithT, *k8sent.K8sCluster)
		wantErr   bool
	}{
		{
			name:      "CR is not created yet",
			task:      &k8sent.Task{},
			cluster:   &k8sent.K8sCluster{Info: &k8sent.K8sInfo{}},
			schedule:  deadline,
			currentUO: nil,
			mockCalls: func(mockAPI *infrainfo.MockServiceAPI, mockGen *generator.MockAPI) {
				mockGen.EXPECT().GenerateUpgradeOperationKey(gomock.Any(), gomock.Any()).Return(testUOName, testUONamespace)
				uo := buildTestUO(testUOName, testUONamespace, deadlineStr, nil)
				mockGen.EXPECT().GenerateUpgradeOperation(context.TODO(), gomock.Any(), testUOName, testUONamespace).Return(uo, "1.30")
				mockAPI.EXPECT().CreateOrUpdateUpgradeOperation(gomock.Any(), &infrainfo.CommonUpgradeOperationReq{
					Namespace:        testUONamespace,
					Name:             testUOName,
					UpgradeOperation: uo,
				}).Return(nil)
			},
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhasePending)))
			},
		},
		{
			name:      "CR was created, in none phase",
			task:      &k8sent.Task{},
			cluster:   &k8sent.K8sCluster{DisableAutoGeneration: true, UpgradeOperationCR: testCRString, Info: &k8sent.K8sInfo{}},
			schedule:  time.Date(2024, 12, 1, 13, 0, 0, 0, time.UTC),
			currentUO: buildTestUO(testUOName, testUONamespace, deadlineStr, &k8sent.UpgradeOperationStatus{Phase: string(cloudv1.UpgradeOperationPhaseNone)}),
			mockCalls: func(mockAPI *infrainfo.MockServiceAPI, mockGen *generator.MockAPI) {
				mockAPI.EXPECT().CreateOrUpdateUpgradeOperation(gomock.Any(), gomock.Any()).Return(nil)
			},
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhasePending)))
			},
		},
		{
			name:     "CR was created, in pending phase",
			task:     &k8sent.Task{},
			cluster:  &k8sent.K8sCluster{Info: &k8sent.K8sInfo{}},
			schedule: time.Date(2024, 12, 18, 10, 0, 0, 0, time.UTC),
			currentUO: func() *k8sent.UpgradeOperation {
				uo := buildTestUO(testUOName, testUONamespace, deadlineStr, &k8sent.UpgradeOperationStatus{Phase: string(cloudv1.UpgradeOperationPhasePending)})
				uo.Steps = []*k8sent.UpgradeStep{
					{Target: string(cloudv1.ControlPlane), Version: "1.30"},
					{Target: string(cloudv1.AdminNodes), Version: "1.30"},
				}
				return uo
			}(),
			mockCalls: func(mockAPI *infrainfo.MockServiceAPI, mockGen *generator.MockAPI) {
				mockGen.EXPECT().GenerateUpgradeOperationKey(gomock.Any(), gomock.Any()).Return(testUOName, testUONamespace)
				uo := buildTestUO(testUOName, testUONamespace, deadlineStr, &k8sent.UpgradeOperationStatus{Phase: string(cloudv1.UpgradeOperationPhasePending)})
				uo.Steps = []*k8sent.UpgradeStep{
					{Target: string(cloudv1.ControlPlane), Version: "1.30"},
					{Target: string(cloudv1.AdminNodes), Version: "1.30"},
				}
				mockGen.EXPECT().GenerateUpgradeOperation(context.TODO(), gomock.Any(), testUOName, testUONamespace).Return(uo, "1.30")
				mockAPI.EXPECT().CreateOrUpdateUpgradeOperation(gomock.Any(), gomock.Any()).Return(nil)
			},
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhasePending)))
			},
		},
		{
			name:      "CR in running phase",
			task:      &k8sent.Task{},
			cluster:   &k8sent.K8sCluster{Info: &k8sent.K8sInfo{}},
			schedule:  time.Date(2024, 12, 18, 10, 0, 0, 0, time.UTC),
			currentUO: buildTestUO(testUOName, testUONamespace, deadlineStr, &k8sent.UpgradeOperationStatus{Phase: string(cloudv1.UpgradeOperationPhaseRunning)}),
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhaseRunning)))
			},
		},
		{
			name:      "CR in completed phase",
			task:      &k8sent.Task{},
			cluster:   &k8sent.K8sCluster{Info: &k8sent.K8sInfo{}},
			schedule:  time.Date(2024, 12, 18, 10, 0, 0, 0, time.UTC),
			currentUO: buildTestUO(testUOName, testUONamespace, deadlineStr, &k8sent.UpgradeOperationStatus{Phase: string(cloudv1.UpgradeOperationPhaseCompleted)}),
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhaseCompleted)))
			},
		},
		{
			name:      "CR is completed and deleted",
			task:      &k8sent.Task{},
			cluster:   &k8sent.K8sCluster{Info: &k8sent.K8sInfo{}, UpgradeOperationStatus: string(cloudv1.UpgradeOperationPhaseCompleted)},
			schedule:  time.Date(2024, 12, 18, 10, 0, 0, 0, time.UTC),
			currentUO: nil,
			mockCalls: func(mockAPI *infrainfo.MockServiceAPI, mockGen *generator.MockAPI) {},
			expect: func(g *gomega.WithT, cluster *k8sent.K8sCluster) {
				g.Expect(cluster.UpgradeOperationStatus).To(gomega.Equal(string(cloudv1.UpgradeOperationPhaseCompleted)))
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockInfraInfoSvc := infrainfo.NewMockServiceAPI(ctrl)
			mockGenerator := generator.NewMockAPI(ctrl)

			if tt.mockCalls != nil {
				tt.mockCalls(mockInfraInfoSvc, mockGenerator)
			}

			s := &svc{infraInfoSvc: mockInfraInfoSvc, generator: mockGenerator}
			if err := s.syncUpgradeOperation(context.TODO(), k8sent.NewReadOnlyTask(tt.task), tt.cluster, tt.schedule, testUOName, testUONamespace, tt.currentUO); (err != nil) != tt.wantErr {
				t.Errorf("syncUpgradeOperation() error = %v, wantErr %v", err, tt.wantErr)
			}
			if tt.expect != nil {
				g := gomega.NewGomegaWithT(t)
				tt.expect(g, tt.cluster)
			}
		})
	}
}
