package service import ( "context" "testing" "time" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/worker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func setupAnalyticsP513Test(t *testing.T) (*gorm.DB, *AnalyticsService, *model.Account, *model.Inbox, *model.Contact, *model.User, *model.Team) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Team{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.Message{}, &model.ReportingEvent{}, &model.ReportingEventsRollup{}, &model.AgentBot{}, &model.AgentBotInbox{}, &model.Tag{}, &model.ConversationLabel{}, &model.BackgroundJob{}, )) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) account := &model.Account{Name: "Analytics"} require.NoError(t, db.Create(account).Error) user := &model.User{AccountID: account.ID, Name: "Agent", Email: "agent@example.com", Password: "secret", Active: true} require.NoError(t, db.Create(user).Error) require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: string(model.AccountUserRoleAgent)}).Error) team := &model.Team{AccountID: account.ID, Name: "Support"} require.NoError(t, db.Create(team).Error) inbox := &model.Inbox{AccountID: account.ID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: account.ID, Name: "Customer", Email: "customer@example.com"} require.NoError(t, db.Create(contact).Error) svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db)) return db, svc, account, inbox, contact, user, team } func TestAnalyticsLiveConversationMetricsAreDerivedFromConversations(t *testing.T) { db, svc, account, inbox, contact, user, team := setupAnalyticsP513Test(t) now := int64(1760000000) firstReply := now - 60 assigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, TeamID: &team.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} unassigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, TeamID: &team.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType, FirstReplyCreatedAt: &firstReply} pending := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusPending), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} otherAccount := &model.Account{Name: "Other"} require.NoError(t, db.Create(otherAccount).Error) other := &model.Conversation{AccountID: otherAccount.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} require.NoError(t, db.Create(assigned).Error) require.NoError(t, db.Create(unassigned).Error) require.NoError(t, db.Create(pending).Error) require.NoError(t, db.Create(other).Error) metrics, err := svc.GetConversationMetrics(context.Background(), account.ID) require.NoError(t, err) assert.Equal(t, int64(2), metrics.OpenCount) assert.Equal(t, int64(1), metrics.UnattendedCount) assert.Equal(t, int64(1), metrics.UnassignedCount) assert.Equal(t, int64(1), metrics.PendingCount) teamMetrics, err := svc.GetConversationMetricsForTeam(context.Background(), account.ID, team.ID) require.NoError(t, err) assert.Equal(t, int64(2), teamMetrics.OpenCount) assert.Equal(t, int64(0), teamMetrics.PendingCount) grouped, err := svc.GetGroupedConversationMetrics(context.Background(), account.ID, "assignee_id") require.NoError(t, err) require.Len(t, grouped, 2) assert.Equal(t, int64(1), grouped[0]["open"]) assert.Nil(t, grouped[0]["assignee_id"]) assert.Equal(t, user.ID, grouped[1]["assignee_id"]) assert.Equal(t, int64(1), grouped[1]["open"]) } func TestAnalyticsReportsUsePersistedConversationMessageAndEventRows(t *testing.T) { db, svc, account, inbox, contact, user, team := setupAnalyticsP513Test(t) since := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) until := since.Add(24 * time.Hour) resolvedAt := since.Add(3 * time.Hour) conv := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, TeamID: &team.ID, Status: string(model.ConversationStatusResolved), ResolvedAt: &resolvedAt, ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} require.NoError(t, db.Create(conv).Error) require.NoError(t, db.Model(conv).Updates(map[string]interface{}{"created_at": since.Add(time.Hour)}).Error) senderID := user.ID require.NoError(t, db.Create(&model.Message{Base: model.Base{CreatedAt: since.Add(2 * time.Hour)}, AccountID: account.ID, ConversationID: conv.ID, InboxID: inbox.ID, SenderID: &senderID, SenderType: "User", MessageType: string(model.MessageTypeOutgoing), Content: "hello"}).Error) require.NoError(t, db.Create(&model.Message{Base: model.Base{CreatedAt: since.Add(90 * time.Minute)}, AccountID: account.ID, ConversationID: conv.ID, InboxID: inbox.ID, MessageType: string(model.MessageTypeIncoming), Content: "hi"}).Error) bot := &model.AgentBot{AccountID: &account.ID, Name: "Bot"} require.NoError(t, db.Create(bot).Error) require.NoError(t, db.Create(&model.AgentBotInbox{AccountID: &account.ID, AgentBotID: bot.ID, InboxID: inbox.ID, Status: model.AgentBotInboxActive}).Error) tag := &model.Tag{AccountID: account.ID, Name: "vip"} require.NoError(t, db.Create(tag).Error) require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: conv.ID, TagID: tag.ID}).Error) require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(30 * time.Minute)}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 1800, ConversationID: &conv.ID, InboxID: &inbox.ID, UserID: &user.ID, EventStartTime: since, EventEndTime: since.Add(30 * time.Minute)}).Error) require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(time.Hour)}, AccountID: account.ID, Name: "conversation_bot_resolved", Value: 1, ConversationID: &conv.ID, InboxID: &inbox.ID, EventStartTime: since, EventEndTime: since.Add(time.Hour)}).Error) summary, err := svc.GetConversationsSummary(context.Background(), account.ID, since, until) require.NoError(t, err) summaryMap := summary.(map[string]interface{}) assert.Equal(t, int64(1), summaryMap["conversations_count"]) assert.Equal(t, int64(1), summaryMap["incoming_messages_count"]) assert.Equal(t, int64(1), summaryMap["outgoing_messages_count"]) assert.Equal(t, int64(1), summaryMap["resolutions_count"]) assert.Equal(t, 1800.0, summaryMap["avg_first_response_time"]) botSummary, err := svc.GetBotSummary(context.Background(), account.ID, since, until) require.NoError(t, err) assert.Equal(t, int64(1), botSummary.BotResolutionsCount) assert.Equal(t, int64(0), botSummary.BotHandoffsCount) assert.NotNil(t, botSummary.Previous) botMetrics, err := svc.GetBotMetrics(context.Background(), account.ID, since, until) require.NoError(t, err) botMap := botMetrics.(map[string]interface{}) assert.Equal(t, int64(1), botMap["conversation_count"]) assert.Equal(t, int64(1), botMap["message_count"]) assert.Equal(t, 100, botMap["resolution_rate"]) distribution, err := svc.GetFirstResponseTimeDistribution(context.Background(), account.ID, since, until) require.NoError(t, err) distMap := distribution.(map[string]map[string]int64) assert.Equal(t, int64(1), distMap["web_widget"]["0-1h"]) outgoing, err := svc.GetOutgoingMessagesCountGrouped(context.Background(), account.ID, since, until, "inbox") require.NoError(t, err) outRows := outgoing.([]map[string]interface{}) require.Len(t, outRows, 1) assert.Equal(t, inbox.ID, outRows[0]["id"]) assert.Equal(t, int64(1), outRows[0]["outgoing_messages_count"]) matrix, err := svc.GetInboxLabelMatrix(context.Background(), account.ID) require.NoError(t, err) matrixMap := matrix.(map[string]interface{}) assert.Equal(t, [][]int64{{1}}, matrixMap["matrix"]) } func TestAnalyticsTimeseriesAndRollupWorker(t *testing.T) { db, svc, account, inbox, contact, user, _ := setupAnalyticsP513Test(t) since := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC) until := since.Add(48 * time.Hour) conv := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &user.ID, Status: string(model.ConversationStatusOpen), ChannelType: inbox.ChannelType, Channel: inbox.ChannelType} require.NoError(t, db.Create(conv).Error) require.NoError(t, db.Model(conv).Updates(map[string]interface{}{"created_at": since.Add(2 * time.Hour)}).Error) require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: since.Add(3 * time.Hour)}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 120, ConversationID: &conv.ID, InboxID: &inbox.ID, UserID: &user.ID, EventStartTime: since, EventEndTime: since.Add(2 * time.Minute)}).Error) points, err := svc.GetTimeseries(context.Background(), account.ID, "conversations_count", since, until, "account", 0, "day", false) require.NoError(t, err) require.Len(t, points, 1) assert.Equal(t, float64(1), points[0].Value) assert.Equal(t, since.Unix(), points[0].Timestamp) avgPoints, err := svc.GetTimeseries(context.Background(), account.ID, "avg_first_response_time", since, until, "agent", user.ID, "day", false) require.NoError(t, err) require.Len(t, avgPoints, 1) assert.Equal(t, 120.0, avgPoints[0].Value) assert.Equal(t, int64(1), avgPoints[0].Count) wp := worker.NewWorkerPool(db) svc.SetWorkerPool(wp) job, err := EnqueueReportingRollupDay(context.Background(), wp, account.ID, since) require.NoError(t, err) require.NotNil(t, job) again, err := EnqueueReportingRollupDay(context.Background(), wp, account.ID, since) require.NoError(t, err) assert.Equal(t, job.ID, again.ID) processed, err := wp.ProcessOne(context.Background()) require.NoError(t, err) assert.True(t, processed) processed, err = wp.ProcessOne(context.Background()) require.NoError(t, err) assert.False(t, processed) var rollupCount int64 require.NoError(t, db.Model(&model.ReportingEventsRollup{}).Where("account_id = ? AND date = ?", account.ID, since).Count(&rollupCount).Error) assert.Greater(t, rollupCount, int64(0)) }