package release

import (
	"context"
	"fmt"
	"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"
	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/pkg/infra/clients/opscore"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	"github.com/tidbcloud/devops/pkg/service/application"
	apprsvcapi "github.com/tidbcloud/devops/pkg/service/approval/api"
	relsvcapi "github.com/tidbcloud/devops/pkg/service/release/api"
	"github.com/tidbcloud/devops/pkg/service/release/repo"
	"github.com/tidbcloud/pkgv2/db/mock_db"

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

func getUserRespForReleaseTest(email string) *userv1.GetUserResponse {
	return &userv1.GetUserResponse{User: &coreuser.User{Email: email}}
}

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

var _ = Describe("[ChangeService]", func() {
	var fw = test.Framework()
	var userSvc *opscore.MockUserServiceClient
	var applSvc *application.MockApplicationService
	var apprSvc *apprsvcapi.MockApprovalServiceAPI
	var chgRepo *repo.MockRepo
	var s *svc

	BeforeEach(func() {
		userSvc = opscore.NewMockUserServiceClient(fw.Ctrl)
		applSvc = application.NewMockApplicationService(fw.Ctrl)
		apprSvc = apprsvcapi.NewMockApprovalServiceAPI(fw.Ctrl)
		chgRepo = repo.NewMockRepo(fw.Ctrl)
		s = &svc{
			userSvc:  userSvc,
			applSvc:  applSvc,
			apprSvc:  apprSvc,
			dbp:      mock_db.NewFakeTransProvider(),
			eventbus: eventbus.NewMockEventBus(),
			repo:     chgRepo,
		}
	})

	Describe("[CreateChange]", func() {
		var param relsvcapi.CreateChangeParam
		var orgRelease *relent.Release
		var orgApplicant *coreuser.User
		var orgApplication *appent.Application
		var expChange *relent.ChangeInRepo

		BeforeEach(func() {
			param = relsvcapi.CreateChangeParam{
				Spec: relent.ChangeInRepo{
					Name:      "change_service_test_change",
					Tier:      []relent.ChangeTier{relent.ChangeTierDedicated},
					RiskLevel: relent.RiskLevelLow,
					Status:    relent.ChangeStatusCreated,

					ChangeItemNames: []string{"DM-1234"},
					ReleaseID:       1,
					ApplicantEmail:  "change_service_test_user@pingcap.com",
					ApplicationName: "test-app",
					Detail:          &relent.ChangeDetail{},
				},
			}
			orgRelease = &relent.Release{
				ReleaseInRepo: relent.ReleaseInRepo{
					ID:            1,
					Name:          "change_service_test_release",
					Type:          relent.ReleaseTypePeriodic,
					WindowStartAt: *test.ParseTimeOrDie(time.DateTime, "2023-12-13 6:00:00"),
					WindowEndAt:   *test.ParseTimeOrDie(time.DateTime, "2023-12-13 18:00:00"),
				},
				Tickets: []*relent.ReleaseTicket{
					{
						ReleaseTicketInRepo: relent.ReleaseTicketInRepo{
							Name: "DM-1234",
						},
					},
				},
			}
			orgApplicant = &coreuser.User{
				LarkUserName: "change_service_test_user",
				Email:        "change_service_test_user@pingcap.com",
			}
			orgApplication = &appent.Application{
				Name: "test-app",
			}
			expChange = &relent.ChangeInRepo{
				Name:      "change_service_test_change",
				Tier:      []relent.ChangeTier{relent.ChangeTierDedicated},
				RiskLevel: relent.RiskLevelLow,
				Status:    relent.ChangeStatusCreated,

				ChangeItemNames: []string{"DM-1234"},
				ReleaseID:       1,
				ApplicantEmail:  "change_service_test_user@pingcap.com",
				ApplicationName: "test-app",
				Detail:          &relent.ChangeDetail{},
			}
		})

		PIt("create a periodic change without window", func() {
			orgRelease.Type = relent.ReleaseTypePeriodic

			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(getUserRespForReleaseTest(orgApplicant.Email), nil)
			applSvc.EXPECT().GetApplication(gomock.Any(), gomock.Any()).Return(orgApplication, nil)
			chgRepo.EXPECT().CreateChange(gomock.Any(), gomock.Any()).DoAndReturn(
				func(arg0 context.Context, arg1 *relent.ChangeInRepo) (*relent.ChangeInRepo, error) {
					// return test.IsEqual(expChange, arg1)
					// TODO: complete this
					if expChange == nil {
						return nil, fmt.Errorf("not implement")
					}
					return nil, nil
				},
			)

			changeID, err := s.CreateChange(context.Background(), param)
			Expect(err).To(BeNil())
			Expect(changeID).NotTo(BeNil())
		})
	})

	Describe("[ListChanges]", func() {
		var param relsvcapi.ListChangeParam

		BeforeEach(func() {
			param = relsvcapi.ListChangeParam{}
		})

		It("list changes meta only", func() {
			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Times(0)        // should not be called
			applSvc.EXPECT().GetApplication(gomock.Any(), gomock.Any()).Times(0) // should not be called
			chgRepo.EXPECT().ListChanges(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error) {
					Expect(detail).To(BeFalse())
					Expect(options).To(HaveLen(0))
					return []*relent.ChangeInRepo{{ID: 1022}}, nil
				},
			)

			changes, err := s.ListChanges(context.Background(), param)
			Expect(err).To(BeNil())
			Expect(changes).To(HaveLen(1))
			for _, change := range changes {
				Expect(change.ID).To(Equal(uint64(1022)))
				Expect(change.Detail).To(BeNil())
				Expect(change.Release).To(BeNil())
				Expect(change.Applicant).To(BeNil())
				Expect(change.Application).To(BeNil())
			}

		})

		It("list changes with filter", func() {
			param.Releases = []uint64{1, 2}
			param.Status = []relent.ChangeStatus{relent.ChangeStatusCreated, relent.ChangeStatusApproving}

			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Times(0)        // should not be called
			applSvc.EXPECT().GetApplication(gomock.Any(), gomock.Any()).Times(0) // should not be called
			chgRepo.EXPECT().ListChanges(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error) {
					Expect(detail).To(BeFalse())
					Expect(options).To(HaveLen(2))
					Expect(options[0]).To(Equal(repo.ChangeOpts.ReleaseIn([]uint64{1, 2})))
					Expect(options[1]).To(Equal(repo.ChangeOpts.StatusIn(param.Status)))
					return []*relent.ChangeInRepo{{ID: 1023}}, nil
				},
			)

			changes, err := s.ListChanges(context.Background(), param)
			Expect(err).To(BeNil())
			Expect(changes).To(HaveLen(1))
			for _, change := range changes {
				Expect(change.ID).To(Equal(uint64(1023)))
				Expect(change.Detail).To(BeNil())
				Expect(change.Release).To(BeNil())
				Expect(change.Applicant).To(BeNil())
				Expect(change.Application).To(BeNil())
			}
		})

		It("list changes with detail", func() {
			param.WithChangeDetail = true

			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Times(0)        // should not be called
			applSvc.EXPECT().GetApplication(gomock.Any(), gomock.Any()).Times(0) // should not be called
			chgRepo.EXPECT().ListChanges(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error) {
					Expect(detail).To(BeTrue())
					Expect(options).To(HaveLen(0))
					return []*relent.ChangeInRepo{{ID: 1024, Detail: &relent.ChangeDetail{ID: 2024}}}, nil
				},
			)

			changes, err := s.ListChanges(context.Background(), param)
			Expect(err).To(BeNil())
			Expect(changes).To(HaveLen(1))
			for _, change := range changes {
				Expect(change.ID).To(Equal(uint64(1024)))
				Expect(change.Detail).NotTo(BeNil())
				Expect(change.Detail.ID).To(Equal(uint64(2024)))
				Expect(change.Release).To(BeNil())
				Expect(change.Applicant).To(BeNil())
				Expect(change.Application).To(BeNil())
			}
		})

		It("list changes with other deps", func() {
			param.WithRelease = true
			param.WithApplicant = true
			param.WithApplication = true

			releaseID := uint64(300)
			applicantEmail := "change_service_test"
			applicationName := "test-app"
			approvalID := uint64(600)

			userSvc.EXPECT().GetUser(gomock.Any(), gomock.Any()).Return(getUserRespForReleaseTest(applicantEmail), nil)
			applSvc.EXPECT().GetApplication(gomock.Any(), applicationName).Return(&appent.Application{Name: applicationName}, nil)
			chgRepo.EXPECT().ListChanges(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
				func(_ context.Context, detail bool, options ...repoopt.Option) ([]*relent.ChangeInRepo, error) {
					return []*relent.ChangeInRepo{{
						ID:              1025,
						ReleaseID:       releaseID,
						ApplicantEmail:  applicantEmail,
						ApplicationName: applicationName,
						ApprovalID:      pointer.Pointer(uint64(approvalID)),
					}}, nil
				},
			)
			chgRepo.EXPECT().GetRelease(gomock.Any(), gomock.Any()).Return(&relent.ReleaseInRepo{ID: releaseID}, nil)

			changes, err := s.ListChanges(context.Background(), param)
			Expect(err).To(BeNil())
			Expect(changes).To(HaveLen(1))
			for _, change := range changes {
				Expect(change.ID).To(Equal(uint64(1025)))
				Expect(change.Detail).To(BeNil())

				Expect(change.Release).NotTo(BeNil())
				Expect(change.Release.ID).To(Equal(releaseID))

				Expect(change.Applicant).NotTo(BeNil())
				Expect(change.Applicant.Email).To(Equal(applicantEmail))

				Expect(change.Application).NotTo(BeNil())
				Expect(change.Application.Name).To(Equal(applicationName))
			}
		})
	})
})
