package action

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

	userv1 "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/api/core-server/user/v1"
	coreuser "github.com/tidbcloud/aisupport/packages/sdk-go/contracts/entity/core-server/user/v1"
	appent "github.com/tidbcloud/devops/entity/application"
	apprent "github.com/tidbcloud/devops/entity/approval"
	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/pkg/infra/clients/eecli"
	"github.com/tidbcloud/devops/pkg/infra/clients/fadcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/githubcli"
	"github.com/tidbcloud/devops/pkg/infra/clients/larkcli"
	larknotify "github.com/tidbcloud/devops/pkg/infra/clients/larkcli/notify"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	"github.com/tidbcloud/devops/pkg/service/application"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	"github.com/tidbcloud/devops/pkg/service/github"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/shurcooL/githubv4"
	"go.uber.org/mock/gomock"
)

func getUserRespForActionTest(user *coreuser.User) *userv1.GetUserResponse {
	return &userv1.GetUserResponse{User: user}
}

func getUserLeaderRespForActionTest(user *coreuser.User) *userv1.GetUserLeaderResponse {
	return &userv1.GetUserLeaderResponse{User: user}
}

func TestChangeService(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Change Action Suite", test.ReporterConfig())
}

var _ = Describe("[ChangeAction]", func() {
	var fw = test.Framework()
	var repoDriver *repo.MockChangeDriver
	var ctx context.Context
	var comp Components
	var change *relent.ChangeInRepo
	var orgUser *coreuser.User
	var orgApplication *appent.Application
	var orgRelease *relent.Release

	BeforeEach(func() {
		repoDriver = repo.NewMockChangeDriver()
		ctx = context.Background()
		comp = Components{
			Logger:             GinkgoLogr,
			ReleaseService:     relsvcapi.NewMockServiceAPI(fw.Ctrl),
			UserService:        opscore.NewMockUserServiceClient(fw.Ctrl),
			ApplicationService: application.NewMockApplicationService(fw.Ctrl),
			ApprovalService:    apprsvcapi.NewMockApprovalServiceAPI(fw.Ctrl),
			ChangeRepo:         repo.NewRepoWithDriver(repoDriver, nil, nil),
			GithubService:      github.NewMockGithubServiceAPI(fw.Ctrl),
			LarkClient:         larkcli.NewMock(fw.Ctrl),
			FADClient:          fadcli.NewMockClient(fw.Ctrl),
		}
		comp.GithubService.(*github.MockGithubServiceAPI).EXPECT().
			RawAPI().AnyTimes().Return(githubcli.NewMockGithubClient(fw.Ctrl))
		change = &relent.ChangeInRepo{
			ID:              1,
			Name:            "change_action_test_change",
			WindowStartAt:   time.Now().Add(-1 * time.Hour).Unix(),
			WindowEndAt:     time.Now().Add(1 * time.Hour).Unix(),
			Tier:            []relent.ChangeTier{relent.ChangeTierDedicated},
			RiskLevel:       relent.RiskLevelHigh,
			ChangeItemNames: []string{"DM-1234"},
			Status:          relent.ChangeStatusCreated,
			Type:            relent.ChangeTypeSprint,
			Detail: &relent.ChangeDetail{
				StagingTestReport: "test report",
			},
			Deployments: []*relent.ChangeDeployment{
				{
					ID:      1,
					Order:   0,
					Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
					Method:  relent.ChangeDeployMethodArgoCD,
					Status:  relent.ChangeDeployStatusCreated,
					Info: relent.ChangeDeployInfo{
						ArgoCD: &relent.ChangeDeployArgoCD{
							DeployPRs: []relent.GithubPR{
								{
									RepoName:  "change_action_test_repo",
									RepoOwner: "tidbcloud",
									PRNumber:  10080,
								},
							},
						},
					},
				},
			},
			Notification: &relent.ChangeNotification{},

			ReleaseID:       100,
			ApplicantEmail:  "change_action_test_applicant",
			ApplicationName: "test-app",
		}
		repoDriver.Save(context.Background(), change)

		orgUser = &coreuser.User{
			Email:      change.ApplicantEmail,
			LarkUserId: "change_action_test_lark_id",
			LarkType:   coreuser.LarkType_LARK_TYPE_GLOBAL,
		}
		orgApplication = &appent.Application{Name: "test-app"}
		orgRelease = &relent.Release{
			ReleaseInRepo: relent.ReleaseInRepo{
				ID: 100,
			},
			Tickets: []*relent.ReleaseTicket{
				{
					ReleaseTicketInRepo: relent.ReleaseTicketInRepo{
						Name: "DM-1234", Status: relent.ReleaseTicketStatusReady,
					},
				},
				{
					ReleaseTicketInRepo: relent.ReleaseTicketInRepo{
						Name: "DM-1235", Status: relent.ReleaseTicketStatusDone,
					},
				},
				{
					ReleaseTicketInRepo: relent.ReleaseTicketInRepo{
						Name: "DM-1234-updated", Status: relent.ReleaseTicketStatusReady},
				},
			},
		}
	})

	Context("[Submit]", func() {
		BeforeEach(func() {
		})

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusDraft {
					err := action.Submit()
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("should transition status to 'created'", func() {
			change.Status = relent.ChangeStatusDraft
			actor := NewChangeActor(ctx, comp, change)

			err := actor.Submit()
			Expect(err).To(BeNil())

			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(got.Status).To(Equal(relent.ChangeStatusCreated))
		})
	})

	Context("[SendApprovalIfReady]", func() {
		var orgApproval *apprent.Approval

		BeforeEach(func() {

			orgApproval = &apprent.Approval{ID: 1000}

			comp.ApplicationService.(*application.MockApplicationService).EXPECT().
				GetApplication(gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgApplication, nil)
			comp.UserService.(*opscore.MockUserServiceClient).EXPECT().
				GetUser(gomock.Any(), gomock.Any()).AnyTimes().
				Return(getUserRespForActionTest(orgUser), nil)
			comp.UserService.(*opscore.MockUserServiceClient).EXPECT().
				GetUserLeader(gomock.Any(), gomock.Any()).AnyTimes().
				Return(getUserLeaderRespForActionTest(orgUser), nil)
		})

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusCreated {
					err := action.SendApprovalIfReady()
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("should return err if any tickets are not found", func() {
			change.Status = relent.ChangeStatusCreated
			action := NewChangeActor(ctx, comp, change)

			// make a ticket not found
			orgRelease.Tickets = orgRelease.Tickets[:0]
			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)

			err := action.SendApprovalIfReady()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("not found in release"))
		})

		It("should return err if any tickets are unready", func() {
			change.Status = relent.ChangeStatusCreated
			action := NewChangeActor(ctx, comp, change)

			// make a ticket unready
			orgRelease.Tickets[0].Status = "To Do"
			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)

			err := action.SendApprovalIfReady()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("tickets [DM-1234] are unready"))
		})

		It("should return err if send approval failed", func() {
			change.Status = relent.ChangeStatusCreated
			action := NewChangeActor(ctx, comp, change)

			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				CreateApproval(gomock.Any(), gomock.Any()).
				Return(nil, fmt.Errorf("test error"))

			err := action.SendApprovalIfReady()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("create approval failed: test error"))
		})

		It("update approval id and status after sending approval", func() {
			change.Status = relent.ChangeStatusCreated
			action := NewChangeActor(ctx, comp, change)

			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				CreateApproval(gomock.Any(), gomock.Any()).
				Return(&orgApproval.ID, nil)

			err := action.SendApprovalIfReady()
			Expect(err).To(BeNil())

			// check change status are updated
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(got.Status).To(Equal(relent.ChangeStatusApproving))
			Expect(*got.ApprovalID).To(Equal(orgApproval.ID))
		})

		It("cancel prev approval if approval id is setted", func() {
			orgApproval.Status = apprent.ApprovalStatusPending
			newApprovalID := uint64(2000)
			newApproval := test.DeepCopyOrDie(orgApproval)
			newApproval.ID = newApprovalID
			canceledApprovalID := orgApproval.ID
			change.ApprovalID = pointer.Pointer(canceledApprovalID) // prev approval id
			change.Status = relent.ChangeStatusCreated
			repoDriver.Save(context.Background(), change)

			action := NewChangeActor(ctx, comp, change)

			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				GetApproval(gomock.Any(), canceledApprovalID).
				Return(orgApproval, nil)
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				CreateApproval(gomock.Any(), gomock.Any()).
				Return(&newApproval.ID, nil) // approve new approval
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				CancelApproval(gomock.Any(), canceledApprovalID).Times(1).
				Return(nil)

			err := action.SendApprovalIfReady()
			Expect(err).To(BeNil())

			// check change status are updated
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(got.Status).To(Equal(relent.ChangeStatusApproving))
			Expect(*got.ApprovalID).To(Equal(newApproval.ID))
			Expect(got.PrevApprovalIDs).To(ContainElement(canceledApprovalID))
		})
	})

	Context("[UpdateChangeBeforeDeploy]", func() {
		var param relsvcapi.UpdateChangeParam
		var updatedChange *relent.ChangeInRepo

		BeforeEach(func() {
			// param should be different from the default change
			param = relsvcapi.UpdateChangeParam{
				Name:              pointer.Pointer("change_action_test_change_updated"),
				WindowStartAt:     pointer.Pointer(test.ParseTimeOrDie(time.DateTime, "2023-12-13 12:00:00").Unix()),
				WindowEndAt:       pointer.Pointer(test.ParseTimeOrDie(time.DateTime, "2023-12-13 14:00:00").Unix()),
				Tier:              pointer.Pointer([]relent.ChangeTier{relent.ChangeTierServerless}),
				RiskLevel:         pointer.Pointer(relent.RiskLevelLow),
				NeedOutage:        pointer.Pointer(true),
				Note:              pointer.Pointer("note-updated"),
				RollbackPlan:      pointer.Pointer("rollback-updated"),
				StagingTestReport: pointer.Pointer("staging-test-report-updated"),
				Type:              pointer.Pointer(relent.ChangeTypeHotfix),
				ChangeItemNames:   pointer.Pointer([]string{"DM-1234-updated"}),
				Deployments: &[]*relent.ChangeDeployment{
					{
						ID:      1,
						Order:   0,
						Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
						Status:  relent.ChangeDeployStatusDeployed,
						Info: relent.ChangeDeployInfo{
							Description: "deploy-description",
							ArgoCD: &relent.ChangeDeployArgoCD{
								DeployPRs: []relent.GithubPR{
									{
										RepoName:  "change_action_test_repo",
										RepoOwner: "tidbcloud",
										PRNumber:  100000,
										Link:      "http://pr-link-updated",
									},
								},
							},
						},
						Method: relent.ChangeDeployMethodArgoCD,
						Note:   "deployment-note-updated",
					},
					{
						ID:      2,
						Order:   1,
						Status:  relent.ChangeDeployStatusDeployed,
						Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
						Info: relent.ChangeDeployInfo{
							Description: "manual-deploy-description-updated",
							Manual: &relent.ChangeDeployManual{
								Description: "manual-deploy-description",
							},
						},
						Method: relent.ChangeDeployMethodManual,
						Note:   "deployment-note-updated",
					},
					{
						ID:      3,
						Order:   2,
						Status:  relent.ChangeDeployStatusDeployed,
						Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
						Info: relent.ChangeDeployInfo{
							Description: "ee_api-deploy-description-updated",
							EEAPI: &relent.ChangeDeployEEAPI{
								EEAPIBuildNgImage: &eecli.BumpNgTagRequest{
									Author: "devops-test",
									PRLink: "http://ee-api-pr-link-updated",
									Branch: "main",
									Commit: "commit-sha-updated",
									Repo:   eecli.TiDBRepoTiDB,
									Meta: eecli.BumpNgTagReqMeta{
										OpsReq: eecli.BumpNgTagOpsReq{
											Applicant: "devops-test",
											ReleaseID: "100",
											ChangeID:  "1",
										},
									},
								},
							},
						},
						Method: relent.ChangeDeployMethodEEAPI,
						Note:   "deployment-note-updated",
					},
				},
			}
			ignoreFields := []string{
				".Deployments[0].DeployedAt",
				".Deployments[0].Order",
				".Deployments[0].Info.Manual",
				".Deployments[0].Info.EEAPI",
				".Deployments[1].DeployedAt",
				".Deployments[1].Info.ArgoCD",
				".Deployments[1].Info.EEAPI",
				".Deployments[2].DeployedAt",
				".Deployments[2].Info.ArgoCD",
				".Deployments[2].Info.Manual",
			}
			Expect(test.CheckNonZeroValue(param, ignoreFields)).To(BeNil(), "every supported fields should by update, maybe some fields are missed")

			// expect change should be updated as same as param
			updatedChange = test.DeepCopyOrDie(change)
			updatedChange.Name = *param.Name
			updatedChange.WindowStartAt = *param.WindowStartAt
			updatedChange.WindowEndAt = *param.WindowEndAt
			updatedChange.Tier = *param.Tier
			updatedChange.RiskLevel = *param.RiskLevel
			updatedChange.Type = *param.Type
			updatedChange.Detail.NeedOutage = *param.NeedOutage
			updatedChange.Detail.Note = *param.Note
			updatedChange.Detail.RollbackPlan = *param.RollbackPlan
			updatedChange.Detail.StagingTestReport = *param.StagingTestReport
			updatedChange.ChangeItemNames = *param.ChangeItemNames
			updatedChange.Deployments = *param.Deployments
			updatedChange.Deployments[0].ID = change.Deployments[0].ID             // should not be updated
			updatedChange.Deployments[0].Status = change.Deployments[0].Status     // should not be updated
			updatedChange.Deployments[0].Note = change.Deployments[0].Note         // should not be updated
			updatedChange.Deployments[1].ID = 0                                    // should not be updated
			updatedChange.Deployments[1].Status = relent.ChangeDeployStatusCreated // should not be updated
			updatedChange.Deployments[1].Note = ""                                 // should not be updated
			updatedChange.Deployments[2].ID = 0
			updatedChange.Deployments[2].Status = relent.ChangeDeployStatusCreated
			updatedChange.Deployments[2].Note = ""

			comp.ApplicationService.(*application.MockApplicationService).EXPECT().
				GetApplication(gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgApplication, nil)
			comp.GithubService.RawAPI().PullRequest.(*githubcli.MockPullRequestsService).EXPECT().
				Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(&githubcli.PullRequest{State: githubv4.PullRequestStateOpen}, nil)
			comp.UserService.(*opscore.MockUserServiceClient).EXPECT().
				GetUser(gomock.Any(), gomock.Any()).AnyTimes().
				Return(getUserRespForActionTest(orgUser), nil)
			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				ListTickets(gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease.Tickets, int64(len(orgRelease.Tickets)), nil)
		})

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusCreated &&
					status != relent.ChangeStatusDraft &&
					status != relent.ChangeStatusApproving &&
					status != relent.ChangeStatusWithdrawn &&
					status != relent.ChangeStatusWaitForDeploy {
					err := action.UpdateChangeBeforeDeploy(param)
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("update change when status is created", func() {
			change.Status = relent.ChangeStatusCreated
			expChange := test.DeepCopyOrDie(updatedChange)

			action := NewChangeActor(ctx, comp, change)

			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)

			err := action.UpdateChangeBeforeDeploy(param)
			Expect(err).To(BeNil())

			// check change are updated
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})

		It("update change when status is approving", func() {
			change.Status = relent.ChangeStatusApproving
			expChange := test.DeepCopyOrDie(updatedChange)

			action := NewChangeActor(ctx, comp, change)

			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				GetRelease(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease, nil)

			err := action.UpdateChangeBeforeDeploy(param)
			Expect(err).To(BeNil())

			// check change are updated
			expChange.Status = relent.ChangeStatusCreated
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})
	})

	Context("[ContinueIfApproved]", func() {
		notificationMsgID := "notification_id"

		BeforeEach(func() {
			comp.UserService.(*opscore.MockUserServiceClient).EXPECT().
				GetUser(gomock.Any(), gomock.Any()).AnyTimes().
				Return(getUserRespForActionTest(orgUser), nil)
			comp.LarkClient.Notify.(*larknotify.MockNotification).EXPECT().
				ReleaseTopic(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(&notificationMsgID, nil)
			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				ListTickets(gomock.Any(), gomock.Any()).AnyTimes().
				Return(orgRelease.Tickets, int64(len(orgRelease.Tickets)), nil)
		})

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusApproving {
					err := action.ContinueIfApproved()
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("update change status if approval is approved", func() {
			change.Status = relent.ChangeStatusApproving
			change.ApprovalID = pointer.Pointer(uint64(1000))
			expChange := test.DeepCopyOrDie(change)

			comp.FADClient.(*fadcli.MockClient).EXPECT().
				MarkPR(gomock.Any()).
				Return(nil).AnyTimes()

			action := NewChangeActor(ctx, comp, change)

			// returned approval is approved
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				GetApproval(gomock.Any(), *change.ApprovalID).
				Return(&apprent.Approval{Status: apprent.ApprovalStatusApproved}, nil)

			err := action.ContinueIfApproved()
			Expect(err).To(BeNil())

			// check change are updated
			expChange.Status = relent.ChangeStatusWaitForDeploy
			expChange.Notification.LarkTopicID = &notificationMsgID
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			expChange.Notification.LarkTopicCreatedAt = got.Notification.LarkTopicCreatedAt
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})

		It("rollback change status to created if approval is rejected", func() {
			change.Status = relent.ChangeStatusApproving
			change.ApprovalID = pointer.Pointer(uint64(1000))
			expChange := test.DeepCopyOrDie(change)

			action := NewChangeActor(ctx, comp, change)

			// returned approval is rejected
			comp.ApprovalService.(*apprsvcapi.MockApprovalServiceAPI).EXPECT().
				GetApproval(gomock.Any(), *change.ApprovalID).
				Return(&apprent.Approval{Status: apprent.ApprovalStatusRejected}, nil)

			err := action.ContinueIfApproved()
			Expect(err).To(BeNil())

			// check change are updated
			expChange.Status = relent.ChangeStatusWithdrawn
			expChange.StatusReason = "The approval is rejected."
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})
	})

	Context("[ExecuteDeployment]", func() {
		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusWaitForDeploy &&
					status != relent.ChangeStatusInProgress {
					err := action.ExecuteDeployment(0, false)
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		Context("[Status:WaitForDeploy]", func() {
			It("return err if window is not reach", func() {
				change.Status = relent.ChangeStatusWaitForDeploy
				change.WindowStartAt = time.Now().Add(time.Hour).Unix()
				change.WindowEndAt = time.Now().Add(2 * time.Hour).Unix()
				action := NewChangeActor(ctx, comp, change)

				err := action.ExecuteDeployment(0, false)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("code:FAILED_PRECONDITION msg:window start time"))
			})

			It("return err if window is not passed", func() {
				change.Status = relent.ChangeStatusWaitForDeploy
				change.WindowStartAt = time.Now().Add(-2 * time.Hour).Unix()
				change.WindowEndAt = time.Now().Add(-1 * time.Hour).Unix()
				action := NewChangeActor(ctx, comp, change)

				err := action.ExecuteDeployment(0, false)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(ContainSubstring("code:FAILED_PRECONDITION msg:window end time"))
			})

			It("return err if deployment method is unknown", func() {
				change.Status = relent.ChangeStatusWaitForDeploy
				change.Deployments[0].Method = "unknown"
				action := NewChangeActor(ctx, comp, change)

				err := action.ExecuteDeployment(0, false)
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("unknown deploy method: unknown"))
			})

			It("update change status if deployment method is argocd", func() {
				change.Status = relent.ChangeStatusWaitForDeploy
				expChange := test.DeepCopyOrDie(change)
				repoDriver.Save(context.Background(), change)

				action := NewChangeActor(ctx, comp, change)

				comp.GithubService.RawAPI().PullRequest.(*githubcli.MockPullRequestsService).EXPECT().
					Approve(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
					Return(nil, nil)
				comp.GithubService.RawAPI().PullRequest.(*githubcli.MockPullRequestsService).EXPECT().
					Merge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
					Return(nil, nil)

				err := action.ExecuteDeployment(0, false)
				Expect(err).To(BeNil())

				expChange.Status = relent.ChangeStatusObserving
				expChange.Deployments[0].Status = relent.ChangeDeployStatusDeployed
				got, err := repoDriver.Load(context.Background(), change.ID, false)
				expChange.Deployments[0].DeployedAt = got.Deployments[0].DeployedAt // ignored
				Expect(err).To(BeNil())
				Expect(got.Deployments[0].DeployedAt).NotTo(BeNil())
				Expect(test.IsEqual(expChange, got)).To(BeNil())
			})
		})

		It("update change status if deployment method is manual", func() {
			change.Status = relent.ChangeStatusWaitForDeploy
			change.Deployments[0] = &relent.ChangeDeployment{
				ID:      1,
				Order:   0,
				Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
				Method:  relent.ChangeDeployMethodManual,
				Status:  relent.ChangeDeployStatusCreated,
				Info: relent.ChangeDeployInfo{
					Description: "manual-deploy-description",
					Manual: &relent.ChangeDeployManual{
						Description: "manual-deploy-description",
					},
				},
			}
			expChange := test.DeepCopyOrDie(change)
			repoDriver.Save(context.Background(), change)

			action := NewChangeActor(ctx, comp, change)

			err := action.ExecuteDeployment(0, false)
			Expect(err).To(BeNil())

			expChange.Status = relent.ChangeStatusObserving
			expChange.Deployments[0].Status = relent.ChangeDeployStatusDeployed
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			expChange.Deployments[0].DeployedAt = got.Deployments[0].DeployedAt // ignored
			Expect(got.Deployments[0].DeployedAt).NotTo(BeNil())
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})
	})

	Context("[VerifyDeployment]", func() {
		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusObserving &&
					status != relent.ChangeStatusInProgress {
					err := action.VerifyDeployment(0, "")
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("verify deployment when inprogress", func() {
			change.Status = relent.ChangeStatusInProgress
			change.Deployments[0].Status = relent.ChangeDeployStatusDeployed
			change.Deployments[0].DeployedAt = pointer.Pointer(time.Now())
			expChange := test.DeepCopyOrDie(change)
			repoDriver.Save(context.Background(), change)

			action := NewChangeActor(ctx, comp, change)

			err := action.VerifyDeployment(0, "verified")
			Expect(err).To(BeNil())

			expChange.Status = relent.ChangeStatusInProgress
			expChange.Deployments[0].Status = relent.ChangeDeployStatusVerified
			expChange.Deployments[0].Note = "verified"
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			expChange.Deployments[0].DeployedAt = change.Deployments[0].DeployedAt // ignored
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})

		It("verify deployment when observing", func() {
			change.Status = relent.ChangeStatusObserving
			change.Deployments[0].Status = relent.ChangeDeployStatusDeployed
			change.Deployments[0].DeployedAt = pointer.Pointer(time.Now())
			expChange := test.DeepCopyOrDie(change)
			repoDriver.Save(context.Background(), change)

			action := NewChangeActor(ctx, comp, change)

			err := action.VerifyDeployment(0, "verified")
			Expect(err).To(BeNil())

			expChange.Status = relent.ChangeStatusObserving
			expChange.Deployments[0].Status = relent.ChangeDeployStatusVerified
			expChange.Deployments[0].Note = "verified"
			got, err := repoDriver.Load(context.Background(), change.ID, false)
			expChange.Deployments[0].DeployedAt = change.Deployments[0].DeployedAt // ignored
			Expect(err).To(BeNil())
			Expect(test.IsEqual(expChange, got)).To(BeNil())
		})
	})

	Context("[Cancel]", func() {
		supportStatuses := []relent.ChangeStatus{
			relent.ChangeStatusCreated,
			relent.ChangeStatusDraft,
			relent.ChangeStatusApproving,
			relent.ChangeStatusWaitForDeploy,
			relent.ChangeStatusWithdrawn,
		}

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if !slices.Contains(supportStatuses, status) {
					err := action.Cancel()
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("Cancel change", func() {
			for _, status := range supportStatuses {
				change.Status = status
				expChange := test.DeepCopyOrDie(change)
				repoDriver.Save(context.Background(), change)

				action := NewChangeActor(ctx, comp, change)

				err := action.Cancel()
				Expect(err).To(BeNil())

				expChange.Status = relent.ChangeStatusClosed
				expChange.Result = pointer.Pointer(relent.ChangeResultCancel)
				expChange.Detail.Summary = pointer.Pointer(fmt.Sprintf("cancel when status is %s", status))
				got, err := repoDriver.Load(context.Background(), change.ID, false)
				expChange.ClosedAt = got.ClosedAt // ignored
				Expect(err).To(BeNil())
				Expect(got.ClosedAt).NotTo(BeNil())
				Expect(test.IsEqual(expChange, got)).To(BeNil())
			}
		})
	})

	Context("[Close]", func() {
		BeforeEach(func() {
			change.Notification.LarkTopicID = pointer.Pointer("")
			comp.ReleaseService.(*relsvcapi.MockServiceAPI).EXPECT().
				CloseTickets(gomock.Any(), gomock.Any()).AnyTimes().
				Return(nil)
			comp.LarkClient.Notify.(*larknotify.MockNotification).EXPECT().
				ReleaseClose(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().
				Return(pointer.Pointer(""), nil)
		})

		It("unsupported status", func() {
			for _, status := range relent.ChangeStatusValues {
				change.Status = status
				action := NewChangeActor(ctx, comp, change)

				if status != relent.ChangeStatusObserving && status != relent.ChangeStatusInProgress {
					err := action.Close(relent.ChangeResultSuccessful, "none")
					Expect(err).To(HaveOccurred())
					Expect(err.Error()).To(ContainSubstring("unsupported action when status is"))
				}
			}
		})

		It("return err if result is unsupported", func() {
			change.Status = relent.ChangeStatusObserving
			action := NewChangeActor(ctx, comp, change)

			err := action.Close(relent.ChangeResultCancel, "none")

			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("invalid result `cancel` to close"))
		})

		It("close change", func() {
			for _, result := range resultsToClose {
				change.Status = relent.ChangeStatusObserving
				expChange := test.DeepCopyOrDie(change)
				repoDriver.Save(context.Background(), change)

				action := NewChangeActor(ctx, comp, change)

				err := action.Close(result, "closed by ut")
				Expect(err).To(BeNil())

				expChange.Status = relent.ChangeStatusClosed
				expChange.Result = pointer.Pointer(result)
				expChange.Detail.Summary = pointer.Pointer("closed by ut")
				got, err := repoDriver.Load(context.Background(), change.ID, false)
				expChange.ClosedAt = got.ClosedAt // ignored
				Expect(err).To(BeNil())
				Expect(got.ClosedAt).NotTo(BeNil())
				Expect(test.IsEqual(expChange, got)).To(BeNil())
			}
		})
	})
})
