package repo

import (
	"context"
	"testing"
	"time"

	relent "github.com/tidbcloud/devops/entity/release"
	"github.com/tidbcloud/devops/models/base"
	relmod "github.com/tidbcloud/devops/models/release"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	"github.com/tidbcloud/devops/pkg/service/release/repo/cvt"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

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

var _ = Describe("[ReleaseRepo]", func() {
	var fw = test.Framework()

	Describe("[ChangeDriver]", func() {
		var (
			d ChangeDriver
		)

		BeforeEach(func() {
			fw.InitUTDB()

			d = &changeDriver{
				cvter:      cvt.NewConverter(),
				dbProvider: fw.UTDBProvider,
			}
		})

		Context("[Save]", func() {
			var org *relent.ChangeInRepo
			var exp *relmod.Change
			ctx := context.Background()

			BeforeEach(func() {
				org = &relent.ChangeInRepo{
					ID:              0,
					Name:            "release devops server",
					WindowStartAt:   test.ParseTimeOrDie(time.DateTime, "2023-12-13 6:00:00").Unix(),
					WindowEndAt:     test.ParseTimeOrDie(time.DateTime, "2023-12-13 18:00:00").Unix(),
					Tier:            []relent.ChangeTier{relent.ChangeTierDedicated},
					RiskLevel:       relent.RiskLevelHigh,
					ChangeItemNames: []string{"DM-50", "DM-60"},
					Status:          relent.ChangeStatusCreated,
					Type:            relent.ChangeTypeSprint,
					Result:          nil,

					Detail: &relent.ChangeDetail{
						NeedOutage: true,
						Note:       "none",
					},
					Deployments: []*relent.ChangeDeployment{
						{
							Order:   0,
							Regions: []relent.RegionName{relent.RegionNameAWSUsWest2},
							Method:  relent.ChangeDeployMethodArgoCD,
							Status:  relent.ChangeDeployStatusCreated,
							Info: relent.ChangeDeployInfo{
								ArgoCD: &relent.ChangeDeployArgoCD{
									DeployPRs: []relent.GithubPR{
										{
											RepoOwner: "tidbcloud",
											RepoName:  "devops_ut",
											PRNumber:  10080,
											Link:      "http://pr.link",
										},
									},
								},
							},
						},
						{
							Order:   1,
							Regions: []relent.RegionName{relent.RegionNameGCPUsWest1},
							Method:  relent.ChangeDeployMethodManual,
							Status:  relent.ChangeDeployStatusCreated,
							Info:    relent.ChangeDeployInfo{},
						},
					},

					ReleaseID:       1,
					ApplicantEmail:  "test@pingcap.com",
					ApplicationName: "test-app",
				}
				exp = &relmod.Change{
					Name:            "release devops server",
					WindowStartAt:   *test.ParseTimeOrDie(time.DateTime, "2023-12-13 6:00:00"),
					WindowEndAt:     *test.ParseTimeOrDie(time.DateTime, "2023-12-13 18:00:00"),
					Tier:            string(relent.ChangeTierDedicated),
					RiskLevel:       relmod.RiskLevelHigh,
					ChangeItemNames: "DM-50,DM-60",
					Status:          relmod.ChangeStatusCreated,
					Type:            relmod.ChangeTypeSprint,
					Result:          nil,

					Detail: &relmod.ChangeDetail{
						NeedOutage: true,
						Note:       "none",
					},
					Deployments: []*relmod.ChangeDeployment{
						{
							Order:   0,
							Regions: "us-west-2",
							Method:  relmod.ChangeDeployMethodArgoCD,
							Status:  relmod.ChangeDeployStatusCreated,
							Info:    []byte(`{"argocd": {"deploy_prs": [{"link": "http://pr.link", "pr_number": 10080, "repo_name": "devops_ut", "repo_owner": "tidbcloud"}]}}`),
						},
						{
							Order:   1,
							Regions: "us-west1",
							Method:  relmod.ChangeDeployMethodManual,
							Status:  relmod.ChangeDeployStatusCreated,
							Info:    []byte(`{}`),
						},
					},

					ReleaseID:       base.ID(1),
					ApplicantEmail:  "test@pingcap.com",
					ApplicationName: "test-app",
				}
			})

			AfterEach(func() {
				if org.ID != 0 {
					fw.Logger.Info("Cleaning up changes in db", "id", org.ID)
					if err := fw.UTDB.Delete(&relmod.Change{}, org.ID).Error; err != nil {
						fw.Logger.Error(err, "failed to delete change", "id", org.ID)
					}
					if org.Detail != nil {
						if err := fw.UTDB.Delete(&relmod.ChangeDetail{}, org.Detail.ID).Error; err != nil {
							fw.Logger.Error(err, "failed to delete change detail", "id", org.Detail.ID)
						}
					}
					for _, d := range org.Deployments {
						if err := fw.UTDB.Delete(&relmod.ChangeDeployment{}, d.ID).Error; err != nil {
							fw.Logger.Error(err, "failed to delete deployment", "id", d.ID)
						}
					}
				}
			})

			It("create a new change if id is 0", func() {
				var err error
				org.ID = 0

				// add a new change
				org, err = d.Save(ctx, org)
				Expect(err).To(BeNil())
				Expect(org.ID).NotTo(BeZero())

				// read from db
				cur := &relmod.Change{}
				err = buildModel(fw.UTDB, true).Where("id = ?", org.ID).First(cur).Error
				Expect(err).To(BeNil())
				Expect(cur.CreatedAt.IsZero()).NotTo(BeTrue())

				// compare (ignore some generated fields)
				exp.Updatable = cur.Updatable
				exp.Detail.Updatable = cur.Detail.Updatable
				exp.Detail.ChangeID = cur.Detail.ChangeID
				for i := range cur.Deployments {
					exp.Deployments[i].ChangeID = cur.Deployments[i].ChangeID
					exp.Deployments[i].Updatable = cur.Deployments[i].Updatable
				}
				Expect(test.IsEqual(exp, cur)).To(BeNil())
			})

			It("update a change if id is not 0", func() {
				var err error
				org.ID = 0

				// add a new release
				org, err = d.Save(ctx, org)
				Expect(err).To(BeNil())
				Expect(org.ID).NotTo(BeZero())

				// update the release
				org.WindowStartAt = test.ParseTimeOrDie(time.DateTime, "2023-12-13 6:00:00").Unix()
				org.WindowEndAt = test.ParseTimeOrDie(time.DateTime, "2023-12-13 18:00:00").Unix()
				org.ClosedAt = test.ParseTimeOrDie(time.DateTime, "2023-12-13 14:00:00")
				org.Detail.Summary = pointer.Pointer("summary")
				org.Deployments = []*relent.ChangeDeployment{
					{
						ID:         0,
						Regions:    []relent.RegionName{relent.RegionNameAWSUsWest2},
						DeployedAt: test.ParseTimeOrDie(time.DateTime, "2023-12-13 12:00:00"),
						Method:     relent.ChangeDeployMethodArgoCD,
						Status:     relent.ChangeDeployStatusDeployed,
					},
				}
				org.ApprovalID = pointer.Pointer(uint64(1000))
				org, err = d.Save(ctx, org)
				Expect(err).To(BeNil())

				// read the record from db
				cur := &relmod.Change{}
				err = buildModel(fw.UTDB, true).Where("id = ?", org.ID).First(cur).Error
				Expect(err).To(BeNil())
				Expect(cur.CreatedAt.IsZero()).NotTo(BeTrue())
				Expect(cur.UpdatedAt.IsZero()).NotTo(BeNil())

				// compare
				// updated fields
				exp.WindowStartAt = *test.ParseTimeOrDie(time.DateTime, "2023-12-13 6:00:00")
				exp.WindowEndAt = *test.ParseTimeOrDie(time.DateTime, "2023-12-13 18:00:00")
				exp.ClosedAt = test.ParseTimeOrDie(time.DateTime, "2023-12-13 14:00:00")
				exp.Detail.Summary = pointer.Pointer("summary")
				exp.Deployments = []*relmod.ChangeDeployment{
					{
						Regions:    "us-west-2",
						DeployedAt: test.ParseTimeOrDie(time.DateTime, "2023-12-13 12:00:00"),
						Method:     relmod.ChangeDeployMethodArgoCD,
						Status:     relmod.ChangeDeployStatusDeployed,
						Info:       []byte("{}"),
					},
				}
				exp.ApprovalID = pointer.Pointer(base.ID(1000))
				// ignored fields
				exp.Updatable = cur.Updatable
				exp.Detail.Updatable = cur.Detail.Updatable
				exp.Detail.ChangeID = cur.Detail.ChangeID
				for i := range cur.Deployments {
					exp.Deployments[i].ChangeID = cur.Deployments[i].ChangeID
					exp.Deployments[i].Updatable = cur.Deployments[i].Updatable
				}
				Expect(test.IsEqual(exp, cur)).To(BeNil())
			})
		})

	})

	Describe("[ReleaseDriver]", func() {
		var repo ReleaseDriver

		BeforeEach(func() {
			fw.InitUTDB()

			repo = &releaseDriver{
				cvter:      cvt.NewConverter(),
				dbProvider: fw.UTDBProvider,
			}
		})

		Context("[Save]", func() {
			var org *relent.ReleaseInRepo
			var exp *relmod.Release
			ctx := context.Background()

			BeforeEach(func() {
				org = &relent.ReleaseInRepo{
					ID:            0,
					Name:          "release-20231212",
					Type:          relent.ReleaseTypePeriodic,
					WindowStartAt: time.Date(2023, 12, 12, 6, 0, 0, 0, time.UTC),
					WindowEndAt:   time.Date(2023, 12, 12, 18, 0, 0, 0, time.UTC),
					ClosedAt:      nil,
					Status:        relent.ReleaseStatusBeforeRelease,
				}
				exp = &relmod.Release{
					Name:          "release-20231212",
					Type:          relmod.ReleaseTypePeriodic,
					WindowStartAt: time.Date(2023, 12, 12, 6, 0, 0, 0, time.UTC),
					WindowEndAt:   time.Date(2023, 12, 12, 18, 0, 0, 0, time.UTC),
					ClosedAt:      nil,
					Status:        relmod.ReleaseStatusBeforeRelease,
				}
			})

			AfterEach(func() {
				if org.ID != 0 {
					fw.Logger.Info("cleaning up release", "id", org.ID)
					err := fw.UTDB.Delete(&relmod.Release{}, org.ID).Error
					if err != nil {
						fw.Logger.Error(err, "failed to delete release", "id", org.ID)
					}
				}
			})

			It("create a new release if id is 0", func() {
				org.ID = 0

				// add a new release
				got, err := repo.Save(ctx, org)
				Expect(err).To(BeNil())
				Expect(got.ID).NotTo(BeZero())

				// read from db
				cur := &relmod.Release{}
				err = fw.UTDB.Model(&relmod.Release{}).Where("id = ?", got.ID).First(cur).Error
				Expect(err).To(BeNil())
				Expect(cur.CreatedAt.IsZero()).NotTo(BeTrue())

				// compare
				exp.ID = base.ID(got.ID)
				exp.CreatedAt = cur.CreatedAt // ignore created_at
				exp.UpdatedAt = cur.UpdatedAt // ignore updated_at
				Expect(cur).To(Equal(exp))
			})

			It("update a release if id is not 0", func() {
				org.ID = 0

				// add a new release
				got, err := repo.Save(ctx, org)
				Expect(err).To(BeNil())
				Expect(got.ID).NotTo(BeZero())

				// update the release
				org.WindowStartAt = time.Date(2023, 12, 13, 6, 0, 0, 0, time.UTC)
				org.WindowEndAt = time.Date(2023, 12, 13, 18, 0, 0, 0, time.UTC)
				org.ClosedAt = pointer.Pointer(time.Date(2023, 12, 14, 0, 0, 0, 0, time.UTC))
				got, err = repo.Save(ctx, org)
				Expect(err).To(BeNil())

				// read the record from db
				cur := &relmod.Release{}
				err = fw.UTDB.Model(&relmod.Release{}).Where("id = ?", got.ID).First(cur).Error
				Expect(err).To(BeNil())
				Expect(cur.CreatedAt.IsZero()).NotTo(BeTrue())
				Expect(cur.UpdatedAt.IsZero()).NotTo(BeNil())

				// check
				exp.ID = base.ID(got.ID)
				exp.WindowStartAt = time.Date(2023, 12, 13, 6, 0, 0, 0, time.UTC)
				exp.WindowEndAt = time.Date(2023, 12, 13, 18, 0, 0, 0, time.UTC)
				exp.ClosedAt = pointer.Pointer(time.Date(2023, 12, 14, 0, 0, 0, 0, time.UTC))
				exp.CreatedAt = cur.CreatedAt // ignore created_at
				exp.UpdatedAt = cur.UpdatedAt // ignore updated_at
				Expect(cur).To(Equal(exp))
			})
		})

		Context("[Load]", func() {
			var org *relmod.Release
			var exp *relent.ReleaseInRepo

			BeforeEach(func() {
				org = &relmod.Release{
					Name:          "release-20231212",
					Type:          relmod.ReleaseTypePeriodic,
					WindowStartAt: time.Date(2023, 12, 12, 6, 0, 0, 0, time.UTC),
					WindowEndAt:   time.Date(2023, 12, 12, 18, 0, 0, 0, time.UTC),
					ClosedAt:      nil,
					Status:        relmod.ReleaseStatusBeforeRelease,
				}
				exp = &relent.ReleaseInRepo{
					Name:          "release-20231212",
					Type:          relent.ReleaseTypePeriodic,
					WindowStartAt: time.Date(2023, 12, 12, 6, 0, 0, 0, time.UTC),
					WindowEndAt:   time.Date(2023, 12, 12, 18, 0, 0, 0, time.UTC),
					ClosedAt:      nil,
					Status:        relent.ReleaseStatusBeforeRelease,
				}
			})

			AfterEach(func() {
				if org.ID != 0 {
					fw.Logger.Info("cleaning up release", "release_id", org.ID)
					err := fw.UTDB.Delete(&relmod.Release{}, org.ID).Error
					fw.Logger.Error(err, "failed to delete release", "release_id", org.ID)
				}
			})

			It("load a existing release", func() {
				// add a new release
				err := fw.UTDB.Model(&relmod.Release{}).Create(org).Error
				Expect(err).To(BeNil())
				Expect(org.ID).NotTo(BeZero())

				// load the release
				cur, err := repo.Load(context.Background(), org.ID.Uint64())
				Expect(err).To(BeNil())

				// check
				exp.ID = org.ID.Uint64()
				Expect(cur).To(Equal(exp))
			})

			It("return error if not found", func() {
			})
		})
	})

})
