package jira

import (
	"context"
	"testing"
	"time"

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

	jiraent "github.com/tidbcloud/devops/entity/jira"
	"github.com/tidbcloud/devops/pkg/infra/clients/jiracli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/infra/utils/repoopt"
	"github.com/tidbcloud/devops/pkg/infra/utils/test"
	jirasvcapi "github.com/tidbcloud/devops/pkg/service/jira/api"
	"github.com/tidbcloud/devops/pkg/service/jira/issueloader"
	"github.com/tidbcloud/devops/pkg/service/jira/repo"
)

func TestJiraService(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "JiraService Suite", test.ReporterConfig())
}

var _ = Describe("[JiraService]", func() {
	var fw = test.Framework()
	var ctx context.Context
	var s *svc
	var now = time.Now()

	BeforeEach(func() {
		ctx = log.NewContext(context.Background(), fw.Logger)
		s = &svc{
			db:          fw.MockDBProvider,
			repo:        repo.NewMockRepo(fw.Ctrl),
			jiraClient:  jiracli.NewMockJiraClient(fw.Ctrl),
			issueLoader: issueloader.NewMockIssueLoader(fw.Ctrl),
		}
		fw.MockDBProvider.EXPECT().Transaction(ctx, gomock.Any()).DoAndReturn(
			func(_ context.Context, fn func(ctx context.Context) error) error {
				return fn(ctx)
			}).AnyTimes()
	})

	Describe("[IssueService]", func() {
		var issueInRepo jiraent.IssueInRepo
		var issueTriage jiraent.IssueTriage
		var issueTimelineSummary jiraent.IssueTimelineSummary

		BeforeEach(func() {
			issueInRepo = jiraent.IssueInRepo{
				Key:                 "TEST-1",
				CreatedAt:           now.Unix(),
				StoredAt:            pointer.Pointer(now.Unix()),
				HaveTriage:          true,
				HaveTimelineSummary: true,

				Snapshot: &jiraent.IssueSnapshot{
					Key:           "TEST-1",
					ID:            "10001",
					Summary:       "Test issue 1",
					Status:        "WIP",
					Priority:      "p1",
					CreatedAt:     now.Unix(),
					ResolvedAt:    pointer.Pointer(now.Unix()),
					ProjectKey:    "TEST",
					AssigneeEmail: pointer.Pointer("test@pingcap.com"),
					ReporterEmail: pointer.Pointer("test@pingcap.com"),
					Components:    []string{"TiDB"},
					CustomerInfo: &jiraent.IssueCustomerInfo{
						IssueKey:    "TEST-1",
						Environment: jiraent.IssueCustomerEnvironmentCloud,
						ClusterID:   pointer.Pointer("123456"),
						TenantID:    pointer.Pointer("654321"),
					},
				},
			}
			issueTriage = jiraent.IssueTriage{
				Key:          "TEST-1",
				CreatedAt:    now.Unix(),
				UpdatedAt:    pointer.Pointer(now.Unix()),
				BriefSummary: pointer.Pointer("This is a test issue"),
				Tags: jiraent.IssueTriageTags{
					Scene:          pointer.Pointer("production"),
					RootCause:      pointer.Pointer("memory leak"),
					Preventability: pointer.Pointer("improve code review"),
					Solution:       pointer.Pointer("optimize memory usage"),
				},
			}
			issueTimelineSummary = jiraent.IssueTimelineSummary{
				Timelines: []jiraent.IssueTimelineSummaryItem{
					{
						BeginTime:   now.Unix(),
						Description: "Issue created",
					},
					{
						BeginTime:   now.Add(1 * time.Hour).Unix(),
						Description: "Issue triaged",
					},
				},
			}
		})

		Describe("[ListUnifiedIssues]", func() {
			It("should list unified issues without filter", func() {
				param := jirasvcapi.ListUnifiedIssuesParam{}
				exp := &jiraent.UnifiedIssue{
					Key:         issueInRepo.Key,
					IssueInRepo: &issueInRepo,
				}

				s.repo.(*repo.MockRepo).EXPECT().CountIssues(ctx, []repoopt.Option{}).
					Return(int64(1), nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					ListIssues(ctx, []repoopt.Option{}).
					Return([]*jiraent.IssueInRepo{&issueInRepo}, nil).Times(1)

				got, total, err := s.ListUnifiedIssues(ctx, param)
				Expect(err).To(BeNil())
				Expect(total).To(Equal(int64(1)))
				Expect(got).To(HaveLen(1))
				Expect(test.IsEqual(exp, got[0])).To(BeNil())
			})

			It("should list unified issues with filter and pagination", func() {
				param := jirasvcapi.ListUnifiedIssuesParam{
					IssueInRepoFilter: jirasvcapi.IssueInRepoFilter{
						IssueSnapshotFilter: jirasvcapi.IssueSnapshotFilter{
							StatusIn:      pointer.Pointer([]string{"WIP", "OPEN"}),
							CreatedAfter:  pointer.Pointer(now.Add(-24 * time.Hour)),
							CreatedBefore: pointer.Pointer(now.Add(24 * time.Hour)),
							PriorityIn:    pointer.Pointer([]string{"p1", "p2"}),
							ProjectKeyIn:  pointer.Pointer([]string{"TEST", "DEMO"}),
							CustomerEnvIs: pointer.Pointer(jiraent.IssueCustomerEnvironmentCloud),
							ClusterIDIn:   pointer.Pointer([]string{"123456", "654321"}),
							TenantIDIn:    pointer.Pointer([]string{"654321", "987654"}),
						},

						HaveTriage:          pointer.Pointer(true),
						HaveTimelineSummary: pointer.Pointer(true),
					},
					Page:    2,
					PerPage: 10,
				}
				exp := &jiraent.UnifiedIssue{
					Key:         issueInRepo.Key,
					IssueInRepo: &issueInRepo,
				}

				s.repo.(*repo.MockRepo).EXPECT().CountIssues(ctx, gomock.Any()).
					Return(int64(1), nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					ListIssues(ctx, gomock.Any()).
					Return([]*jiraent.IssueInRepo{pointer.Pointer(issueInRepo)}, nil).Times(1)

				got, total, err := s.ListUnifiedIssues(ctx, param)
				Expect(err).To(BeNil())
				Expect(total).To(Equal(int64(1)))
				Expect(got).To(HaveLen(1))
				Expect(test.IsEqual(exp, got[0])).To(BeNil())
			})

			It("should list unified issues with triage and summary", func() {
				param := jirasvcapi.ListUnifiedIssuesParam{
					WithTriage:          true,
					WithTimelineSummary: true,
				}
				exp := &jiraent.UnifiedIssue{
					Key:                  issueInRepo.Key,
					IssueInRepo:          &issueInRepo,
					IssueTriage:          &issueTriage,
					IssueTimelineSummary: &issueTimelineSummary,
				}

				s.repo.(*repo.MockRepo).EXPECT().CountIssues(ctx, []repoopt.Option{}).
					Return(int64(1), nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					ListIssues(ctx, []repoopt.Option{}).
					Return([]*jiraent.IssueInRepo{pointer.Pointer(issueInRepo)}, nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					ListIssueTriages(ctx, []repoopt.Option{repo.IssueTriageOption.KeyIn([]string{issueInRepo.Key})}).
					Return([]*jiraent.IssueTriage{pointer.Pointer(issueTriage)}, nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					GetIssueTimelineSummary(ctx, issueInRepo.Key).
					Return(&issueTimelineSummary, nil).Times(1)

				got, total, err := s.ListUnifiedIssues(ctx, param)
				Expect(err).To(BeNil())
				Expect(total).To(Equal(int64(1)))
				Expect(got).To(HaveLen(1))
				Expect(test.IsEqual(exp, got[0])).To(BeNil())
			})
		})

		Describe("[SyncIssueSnapshotByKey]", func() {
			It("should create new issue snapshot when not found", func() {
				exp := &jiraent.IssueInRepo{
					Key: issueInRepo.Key,

					Snapshot: test.DeepCopyOrDie(issueInRepo.Snapshot),
				}

				s.issueLoader.(*issueloader.MockIssueLoader).EXPECT().
					LoadIssueInRepoByKey(ctx, issueInRepo.Key).
					Return(pointer.Pointer(*issueInRepo.Snapshot), nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					GetIssue(ctx, issueInRepo.Key).
					Return(nil, gorm.ErrRecordNotFound)
				s.repo.(*repo.MockRepo).EXPECT().
					CreateIssue(ctx, gomock.Any()).
					DoAndReturn(func(_ context.Context, issue *jiraent.IssueInRepo) (*jiraent.IssueInRepo, error) {
						return pointer.Pointer(*issue), nil
					}).Times(1)

				got, err := s.SyncIssueSnapshotByKey(ctx, issueInRepo.Key)
				Expect(err).To(BeNil())
				Expect(test.IsEqual(exp, got)).To(BeNil())
			})

			It("should update issue snapshot when found", func() {
				newSnapshot := test.DeepCopyOrDie(issueInRepo.Snapshot)
				newSnapshot.Status = "OPEN"
				exp := test.DeepCopyOrDie(&issueInRepo)
				exp.Snapshot = newSnapshot

				s.issueLoader.(*issueloader.MockIssueLoader).EXPECT().
					LoadIssueInRepoByKey(ctx, issueInRepo.Key).
					Return(newSnapshot, nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					GetIssue(ctx, issueInRepo.Key).
					Return(pointer.Pointer(issueInRepo), nil)
				s.repo.(*repo.MockRepo).EXPECT().
					UpdateIssue(ctx, gomock.Any()).
					DoAndReturn(func(_ context.Context, issue *jiraent.IssueInRepo) (*jiraent.IssueInRepo, error) {
						return issue, nil
					}).Times(1)

				got, err := s.SyncIssueSnapshotByKey(ctx, issueInRepo.Key)
				Expect(err).To(BeNil())
				Expect(test.IsEqual(exp, got)).To(BeNil())
			})
		})

		Describe("[UpsertIssueTriage]", func() {
			It("should upsert issue and issue triage", func() {
				issueInRepo.HaveTriage = false // need to be updated to true
				exp := test.DeepCopyOrDie(&issueTriage)
				var gotIssue *jiraent.IssueInRepo
				expIssue := test.DeepCopyOrDie(&issueInRepo)
				expIssue.HaveTriage = true

				s.issueLoader.(*issueloader.MockIssueLoader).EXPECT().
					LoadIssueInRepoByKey(ctx, issueInRepo.Key).
					Return(issueInRepo.Snapshot, nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					GetIssue(ctx, issueInRepo.Key).
					Return(pointer.Pointer(issueInRepo), nil)
				s.repo.(*repo.MockRepo).EXPECT().
					UpdateIssue(ctx, gomock.Any()).
					DoAndReturn(func(_ context.Context, issue *jiraent.IssueInRepo) (*jiraent.IssueInRepo, error) {
						gotIssue = issue
						return issue, nil
					}).AnyTimes()
				s.repo.(*repo.MockRepo).EXPECT().
					UpsertIssueTriage(ctx, gomock.Any()).
					DoAndReturn(func(_ context.Context, issue *jiraent.IssueTriage) (*jiraent.IssueTriage, error) {
						return issue, nil
					}).Times(1)

				got, err := s.UpsertIssueTriage(ctx, &issueTriage)
				Expect(err).To(BeNil())
				Expect(test.IsEqual(exp, got)).To(BeNil())
				Expect(test.IsEqual(expIssue, gotIssue)).To(BeNil())
			})
		})

		Describe("[SetIssueTimelineSummary]", func() {
			It("should update issue and timeline summary", func() {
				issueInRepo.HaveTimelineSummary = false // need to be updated to true
				var gotIssue *jiraent.IssueInRepo
				expIssue := test.DeepCopyOrDie(&issueInRepo)
				expIssue.HaveTimelineSummary = true

				s.issueLoader.(*issueloader.MockIssueLoader).EXPECT().
					LoadIssueInRepoByKey(ctx, issueInRepo.Key).
					Return(issueInRepo.Snapshot, nil).Times(1)
				s.repo.(*repo.MockRepo).EXPECT().
					GetIssue(ctx, issueInRepo.Key).
					Return(pointer.Pointer(issueInRepo), nil)
				s.repo.(*repo.MockRepo).EXPECT().
					UpdateIssue(ctx, gomock.Any()).
					DoAndReturn(func(_ context.Context, issue *jiraent.IssueInRepo) (*jiraent.IssueInRepo, error) {
						gotIssue = issue
						return issue, nil
					}).AnyTimes()
				s.repo.(*repo.MockRepo).EXPECT().
					SetIssueTimelineSummary(ctx, issueInRepo.Key, &issueTimelineSummary).
					DoAndReturn(func(_ context.Context, _ string, summary *jiraent.IssueTimelineSummary) error {
						return nil
					}).Times(1)

				err := s.SetIssueTimelineSummary(ctx, issueInRepo.Key, &issueTimelineSummary)
				Expect(err).To(BeNil())
				Expect(test.IsEqual(expIssue, gotIssue)).To(BeNil())
			})
		})
	})
})
