package service import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // ========== Test Setup ========== func setupSlaPolicyServiceTest(t *testing.T) (*SlaPolicyService, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "failed to open SQLite test db") require.NoError(t, db.AutoMigrate( &model.Account{}, &model.SlaPolicy{}, &model.SlaPolicyInbox{}, &model.AppliedSLA{}, &model.SlaEvent{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.User{}, &model.Team{}, ), "failed to auto-migrate") t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) slaPolicyRepo := repository.NewSlaPolicyRepo(db) appliedSlaRepo := repository.NewAppliedSlaRepo(db) slaEventRepo := repository.NewSlaEventRepo(db) slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db) svc := NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo) return svc, db } func createSlaSvcTestAccount(t *testing.T, db *gorm.DB) *model.Account { t.Helper() account := &model.Account{Name: "SlaSvcOrg", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) return account } // ========== Create ========== func TestSlaPolicyService_Create(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, err := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Priority SLA", Description: "High priority response times", FirstResponseTimeThreshold: 30, NextResponseTimeThreshold: 60, ResolutionTimeThreshold: 480, }) require.NoError(t, err) assert.NotZero(t, policy.ID) assert.Equal(t, account.ID, policy.AccountID) assert.Equal(t, "Priority SLA", policy.Name) assert.Equal(t, 30, policy.FirstResponseTimeThreshold) assert.Equal(t, 60, policy.NextResponseTimeThreshold) assert.Equal(t, 480, policy.ResolutionTimeThreshold) } func TestSlaPolicyService_Create_ValidationError(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) _, err := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "", // required }) require.Error(t, err) assert.Contains(t, err.Error(), "validation error") } // ========== Get ========== func TestSlaPolicyService_Get(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) found, err := svc.Get(context.Background(), account.ID, policy.ID) require.NoError(t, err) assert.Equal(t, policy.ID, found.ID) assert.Equal(t, "Test SLA", found.Name) } func TestSlaPolicyService_Get_NotFound(t *testing.T) { svc, _ := setupSlaPolicyServiceTest(t) _, err := svc.Get(context.Background(), 1, 9999) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } func TestSlaPolicyService_Get_WrongAccount(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) _, err := svc.Get(context.Background(), 9999, policy.ID) require.Error(t, err) assert.Contains(t, err.Error(), "does not belong to account") } // ========== List ========== func TestSlaPolicyService_List(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{Name: "SLA-A", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100}) svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{Name: "SLA-B", FirstResponseTimeThreshold: 20, NextResponseTimeThreshold: 40, ResolutionTimeThreshold: 200}) policies, err := svc.List(context.Background(), account.ID) require.NoError(t, err) assert.Len(t, policies, 2) } func TestSlaPolicyService_List_Empty(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policies, err := svc.List(context.Background(), account.ID) require.NoError(t, err) assert.Len(t, policies, 0) } // ========== Update ========== func TestSlaPolicyService_Update(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Original", Description: "Original desc", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) updated, err := svc.Update(context.Background(), account.ID, policy.ID, &UpdateSlaPolicyRequest{ Name: "Updated SLA", Description: "Updated desc", FirstResponseTimeThreshold: intPtr(45), NextResponseTimeThreshold: intPtr(90), ResolutionTimeThreshold: intPtr(360), }) require.NoError(t, err) assert.Equal(t, "Updated SLA", updated.Name) assert.Equal(t, "Updated desc", updated.Description) assert.Equal(t, 45, updated.FirstResponseTimeThreshold) assert.Equal(t, 90, updated.NextResponseTimeThreshold) assert.Equal(t, 360, updated.ResolutionTimeThreshold) } func TestSlaPolicyService_Update_PartialFields(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Original", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) updated, err := svc.Update(context.Background(), account.ID, policy.ID, &UpdateSlaPolicyRequest{ Name: "Updated Only Name", }) require.NoError(t, err) assert.Equal(t, "Updated Only Name", updated.Name) assert.Equal(t, 10, updated.FirstResponseTimeThreshold) // unchanged } func TestSlaPolicyService_Update_WrongAccount(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) _, err := svc.Update(context.Background(), 9999, policy.ID, &UpdateSlaPolicyRequest{Name: "Nope"}) require.Error(t, err) } // ========== Delete ========== func TestSlaPolicyService_Delete(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "ToDelete", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) err := svc.Delete(context.Background(), account.ID, policy.ID) require.NoError(t, err) _, err = svc.Get(context.Background(), account.ID, policy.ID) require.Error(t, err) // soft-deleted } func TestSlaPolicyService_Delete_WrongAccount(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) err := svc.Delete(context.Background(), 9999, policy.ID) require.Error(t, err) } func TestSlaPolicyService_Delete_CascadesInboxes(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "ToDelete", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: account.ID} require.NoError(t, db.Create(inbox).Error) svc.AddInbox(context.Background(), account.ID, policy.ID, inbox.ID) err := svc.Delete(context.Background(), account.ID, policy.ID) require.NoError(t, err) remaining, _ := svc.ListInboxes(context.Background(), account.ID, policy.ID) assert.Empty(t, remaining) // inbox associations removed on delete } // ========== AddInbox ========== func TestSlaPolicyService_AddInbox(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: account.ID} require.NoError(t, db.Create(inbox).Error) spi, err := svc.AddInbox(context.Background(), account.ID, policy.ID, inbox.ID) require.NoError(t, err) assert.NotZero(t, spi.ID) assert.Equal(t, policy.ID, spi.SlaPolicyID) assert.Equal(t, inbox.ID, spi.InboxID) } func TestSlaPolicyService_AddInbox_WrongAccount(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) _, err := svc.AddInbox(context.Background(), 9999, policy.ID, 1) require.Error(t, err) } // ========== ListInboxes ========== func TestSlaPolicyService_ListInboxes(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox1 := &model.Inbox{Name: "Inbox1", AccountID: account.ID} inbox2 := &model.Inbox{Name: "Inbox2", AccountID: account.ID} require.NoError(t, db.Create(inbox1).Error) require.NoError(t, db.Create(inbox2).Error) svc.AddInbox(context.Background(), account.ID, policy.ID, inbox1.ID) svc.AddInbox(context.Background(), account.ID, policy.ID, inbox2.ID) inboxes, err := svc.ListInboxes(context.Background(), account.ID, policy.ID) require.NoError(t, err) assert.Len(t, inboxes, 2) } // ========== RemoveInbox ========== func TestSlaPolicyService_RemoveInbox(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: account.ID} require.NoError(t, db.Create(inbox).Error) svc.AddInbox(context.Background(), account.ID, policy.ID, inbox.ID) err := svc.RemoveInbox(context.Background(), account.ID, policy.ID, inbox.ID) require.NoError(t, err) remaining, _ := svc.ListInboxes(context.Background(), account.ID, policy.ID) assert.Len(t, remaining, 0) } func TestSlaPolicyService_RemoveInbox_NotAssociated(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) err := svc.RemoveInbox(context.Background(), account.ID, policy.ID, 9999) require.Error(t, err) assert.Contains(t, err.Error(), "not associated") } // ========== GetAppliedSlaMetrics ========== func TestSlaPolicyService_GetAppliedSlaMetrics(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) applied := &model.AppliedSLA{ AccountID: account.ID, ConversationID: 1, SlaPolicyID: policy.ID, SLAStatus: model.SLAStatusActive, } require.NoError(t, db.Create(applied).Error) event := &model.SlaEvent{AppliedSlaID: applied.ID, EventType: model.SLAEventFRT} require.NoError(t, db.Create(event).Error) foundApplied, events, err := svc.GetAppliedSlaMetrics(context.Background(), account.ID, 1) require.NoError(t, err) assert.Equal(t, applied.ID, foundApplied.ID) assert.Len(t, events, 1) assert.Equal(t, model.SLAEventFRT, events[0].EventType) } func TestSlaPolicyService_GetAppliedSlaMetrics_NotFound(t *testing.T) { svc, _ := setupSlaPolicyServiceTest(t) _, _, err := svc.GetAppliedSlaMetrics(context.Background(), 1, 9999) require.Error(t, err) assert.Contains(t, err.Error(), "not found") } // ========== GetAppliedSlaDownload ========== func TestSlaPolicyService_GetAppliedSlaDownload(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) policy, _ := svc.Create(context.Background(), account.ID, &CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) applied := &model.AppliedSLA{ AccountID: account.ID, ConversationID: 1, SlaPolicyID: policy.ID, SLAStatus: model.SLAStatusActive, } require.NoError(t, db.Create(applied).Error) appliedSLAs, err := svc.GetAppliedSlaDownload(context.Background(), account.ID) require.NoError(t, err) assert.Len(t, appliedSLAs, 1) assert.Equal(t, applied.ID, appliedSLAs[0].ID) } func TestSlaPolicyService_ListAppliedSlaReportsFiltersMisses(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) now := time.Now().UTC() _, missedConv := seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusActiveWithMisses, "vip, urgent", now.Add(-time.Hour)) seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusHit, "vip, urgent", now.Add(-time.Hour)) seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusMissed, "other", now.Add(-time.Hour)) result, err := svc.ListAppliedSlaReports(context.Background(), account.ID, AppliedSlaReportFilter{ Since: timePtr(now.Add(-2 * time.Hour)), Until: timePtr(now.Add(time.Hour)), InboxID: &missedConv.InboxID, TeamID: missedConv.TeamID, LabelList: "urgent", }, 1) require.NoError(t, err) assert.Equal(t, int64(1), result.Count) require.Len(t, result.AppliedSLAs, 1) assert.Equal(t, model.SLAStatusActiveWithMisses, result.AppliedSLAs[0].SLAStatus) assert.Equal(t, "Gold SLA", result.AppliedSLAs[0].SlaPolicy.Name) require.Len(t, result.AppliedSLAs[0].SlaEvents, 1) } func TestSlaPolicyService_GetAppliedSlaReportMetrics(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) now := time.Now().UTC() seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusHit, "vip", now.Add(-time.Hour)) seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusMissed, "vip", now.Add(-time.Hour)) seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusActiveWithMisses, "other", now.Add(-time.Hour)) metrics, err := svc.GetAppliedSlaReportMetrics(context.Background(), account.ID, AppliedSlaReportFilter{LabelList: "vip"}) require.NoError(t, err) assert.Equal(t, int64(2), metrics.TotalAppliedSlas) assert.Equal(t, int64(1), metrics.NumberOfSlaMisses) assert.Equal(t, "50.0%", metrics.HitRate) } func TestSlaPolicyService_ListAppliedSlaReportDownload(t *testing.T) { svc, db := setupSlaPolicyServiceTest(t) account := createSlaSvcTestAccount(t, db) now := time.Now().UTC() seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusMissed, "vip", now.Add(-time.Hour)) seedSlaSvcReportRecord(t, db, account.ID, model.SLAStatusHit, "vip", now.Add(-time.Hour)) applied, err := svc.ListAppliedSlaReportDownload(context.Background(), account.ID, AppliedSlaReportFilter{LabelList: "vip"}) require.NoError(t, err) require.Len(t, applied, 1) assert.Equal(t, model.SLAStatusMissed, applied[0].SLAStatus) } func seedSlaSvcReportRecord(t *testing.T, db *gorm.DB, accountID uint, status model.SLAStatus, labels string, createdAt time.Time) (*model.AppliedSLA, *model.Conversation) { t.Helper() policy := &model.SlaPolicy{AccountID: accountID, Name: "Gold SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 30} require.NoError(t, db.Create(policy).Error) inbox := &model.Inbox{AccountID: accountID, Name: "Priority", ChannelType: "web_widget", ChannelID: 1} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "SLA Contact"} require.NoError(t, db.Create(contact).Error) team := &model.Team{AccountID: accountID, Name: "Escalation"} require.NoError(t, db.Create(team).Error) conversation := &model.Conversation{AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID, TeamID: &team.ID, Status: "open", ChannelType: "Channel::WebWidget", Channel: "web_widget", Labels: labels} require.NoError(t, db.Create(conversation).Error) applied := &model.AppliedSLA{AccountID: accountID, ConversationID: conversation.ID, SlaPolicyID: policy.ID, SLAStatus: status} require.NoError(t, db.Create(applied).Error) require.NoError(t, db.Model(applied).Updates(map[string]any{"created_at": createdAt, "updated_at": createdAt}).Error) if status == model.SLAStatusMissed || status == model.SLAStatusActiveWithMisses { require.NoError(t, db.Create(&model.SlaEvent{AppliedSlaID: applied.ID, AccountID: accountID, ConversationID: conversation.ID, InboxID: inbox.ID, SlaPolicyID: policy.ID, EventType: model.SLAEventFRT}).Error) } return applied, conversation } func timePtr(t time.Time) *time.Time { return &t }