package service import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gochat/gochat/internal/model" ) // ========== GetSummary ========== func TestAnalyticsService_GetSummary_空结果(t *testing.T) { _, _, _, svc := setupAnalyticsService(t) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC) resp, err := svc.GetSummary(context.Background(), 1, since, until) require.NoError(t, err) assert.NotNil(t, resp) assert.Empty(t, resp.Metrics) } func TestAnalyticsService_GetSummary_单metric(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricFirstResponse r.Count = 3 r.SumValue = 360.0 r.SumValueBusinessHours = 180.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) resp, err := svc.GetSummary(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, resp.Metrics, 1) assert.Equal(t, "first_response", resp.Metrics[0].Metric) assert.Equal(t, int64(3), resp.Metrics[0].Count) assert.InDelta(t, 120.0, resp.Metrics[0].AverageValue, 0.01) // 360/3=120 assert.InDelta(t, 60.0, resp.Metrics[0].AverageBusinessHours, 0.01) // 180/3=60 } func TestAnalyticsService_GetSummary_多metric合并(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) // Create two rollups with the same metric, they should be merged createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricFirstResponse r.Count = 2 r.SumValue = 240.0 r.SumValueBusinessHours = 120.0 r.Date = time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) }) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricFirstResponse r.Count = 3 r.SumValue = 360.0 r.SumValueBusinessHours = 180.0 r.Date = time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC) }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) resp, err := svc.GetSummary(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, resp.Metrics, 1) assert.Equal(t, "first_response", resp.Metrics[0].Metric) assert.Equal(t, int64(5), resp.Metrics[0].Count) // 2+3 assert.InDelta(t, 120.0, resp.Metrics[0].AverageValue, 0.01) // (240+360)/(2+3)=120 assert.InDelta(t, 60.0, resp.Metrics[0].AverageBusinessHours, 0.01) // (120+180)/(2+3)=60 } func TestAnalyticsService_GetSummary_不同metric类型(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricFirstResponse r.Count = 2 r.SumValue = 240.0 r.SumValueBusinessHours = 120.0 }) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricResolutionTime r.Count = 4 r.SumValue = 800.0 r.SumValueBusinessHours = 400.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) resp, err := svc.GetSummary(context.Background(), account.ID, since, until) require.NoError(t, err) assert.Len(t, resp.Metrics, 2) // two distinct metric types } // ========== GetAgentMetrics ========== func TestAnalyticsService_GetAgentMetrics_空结果(t *testing.T) { _, _, _, svc := setupAnalyticsService(t) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC) dims, err := svc.GetAgentMetrics(context.Background(), 1, since, until) require.NoError(t, err) assert.Empty(t, dims) } func TestAnalyticsService_GetAgentMetrics_有数据(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.DimensionType = model.DimensionAgent r.DimensionID = user.ID r.Metric = model.MetricFirstResponse r.Count = 2 r.SumValue = 200.0 r.SumValueBusinessHours = 100.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) dims, err := svc.GetAgentMetrics(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, dims, 1) assert.Equal(t, user.ID, dims[0].DimensionID) assert.Equal(t, "agent", dims[0].DimensionType) require.Len(t, dims[0].Metrics, 1) assert.Equal(t, int64(2), dims[0].Metrics[0].Count) assert.InDelta(t, 100.0, dims[0].Metrics[0].AverageValue, 0.01) // 200/2=100 } // ========== GetInboxMetrics ========== func TestAnalyticsService_GetInboxMetrics_有数据(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) inbox := createTestInbox(t, db, account.ID, "web_widget") createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.DimensionType = model.DimensionInbox r.DimensionID = inbox.ID r.Metric = model.MetricReplyTime r.Count = 3 r.SumValue = 450.0 r.SumValueBusinessHours = 225.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) dims, err := svc.GetInboxMetrics(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, dims, 1) assert.Equal(t, inbox.ID, dims[0].DimensionID) assert.Equal(t, "inbox", dims[0].DimensionType) } // ========== GetLabelMetrics ========== func TestAnalyticsService_GetLabelMetrics_有数据(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) // LabelMetrics uses DimensionAccount internally createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.DimensionType = model.DimensionAccount r.DimensionID = account.ID r.Metric = model.MetricFirstResponse r.Count = 1 r.SumValue = 100.0 r.SumValueBusinessHours = 50.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) dims, err := svc.GetLabelMetrics(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, dims, 1) assert.Equal(t, account.ID, dims[0].DimensionID) } // ========== GetTeamMetrics ========== func TestAnalyticsService_GetTeamMetrics_空结果(t *testing.T) { _, _, _, svc := setupAnalyticsService(t) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC) dims, err := svc.GetTeamMetrics(context.Background(), 999, since, until) require.NoError(t, err) assert.Empty(t, dims) } func TestAnalyticsService_GetTeamMetrics_有数据(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.DimensionType = model.DimensionTeam r.DimensionID = account.ID // team is approximated as account r.Metric = model.MetricResolutionTime r.Count = 10 r.SumValue = 500.0 r.SumValueBusinessHours = 250.0 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) dims, err := svc.GetTeamMetrics(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, dims, 1) assert.Equal(t, "team", dims[0].DimensionType) } // ========== GetConversationTraffic ========== func TestAnalyticsService_GetConversationTraffic_空结果(t *testing.T) { _, _, _, svc := setupAnalyticsService(t) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC) points, err := svc.GetConversationTraffic(context.Background(), 1, since, until) require.NoError(t, err) assert.Empty(t, points) } func TestAnalyticsService_GetConversationTraffic_有数据(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) date1 := time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) date2 := time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricResolutionsCount r.Date = date1 r.Count = 5 }) createTestRollup(t, db, account.ID, func(r *model.ReportingEventsRollup) { r.Metric = model.MetricResolutionsCount r.Date = date2 r.Count = 8 }) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) points, err := svc.GetConversationTraffic(context.Background(), account.ID, since, until) require.NoError(t, err) require.Len(t, points, 2) // Verify dates and counts appear in the output found10 := false found15 := false for _, p := range points { if p.Date.Format("2006-01-02") == "2026-01-10" { assert.Equal(t, int64(5), p.Count) found10 = true } if p.Date.Format("2006-01-02") == "2026-01-15" { assert.Equal(t, int64(8), p.Count) found15 = true } } assert.True(t, found10, "expected traffic point for 2026-01-10") assert.True(t, found15, "expected traffic point for 2026-01-15") } // ========== GetConversationMetrics ========== func TestAnalyticsService_GetConversationMetrics_空结果(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) metrics, err := svc.GetConversationMetrics(context.Background(), account.ID) require.NoError(t, err) assert.NotNil(t, metrics) assert.Equal(t, int64(0), metrics.ResolvedCount) } func TestAnalyticsService_GetConversationMetrics_有resolved(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) now := time.Now() startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) // Create resolution events for today for i := 0; i < 3; i++ { createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameResolutionTime e.Value = 300.0 e.ValueInBusinessHours = 150.0 e.CreatedAt = startOfDay.Add(time.Duration(i) * time.Hour) }) } metrics, err := svc.GetConversationMetrics(context.Background(), account.ID) require.NoError(t, err) assert.NotNil(t, metrics) assert.Equal(t, int64(3), metrics.ResolvedCount) } // ========== RecordEvent ========== func TestAnalyticsService_RecordEvent_成功(t *testing.T) { db, eventRepo, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) event := &model.ReportingEvent{ AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 120.5, ValueInBusinessHours: 60.0, } err := svc.RecordEvent(context.Background(), event) require.NoError(t, err) assert.NotZero(t, event.ID) // ID should be assigned after creation // Verify it's persisted found, err := eventRepo.FindByMetric(context.Background(), account.ID, model.MetricNameFirstResponse, time.Now().Add(-24*time.Hour), time.Now().Add(24*time.Hour)) require.NoError(t, err) require.Len(t, found, 1) assert.Equal(t, 120.5, found[0].Value) } func TestAnalyticsService_RecordEvent_带可选字段(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) inbox := createTestInbox(t, db, account.ID, "web_widget") user := createTestUser(t, db, account.ID) event := &model.ReportingEvent{ AccountID: account.ID, InboxID: &inbox.ID, UserID: &user.ID, Name: model.MetricNameReplyTime, Value: 45.0, ValueInBusinessHours: 22.5, } err := svc.RecordEvent(context.Background(), event) require.NoError(t, err) assert.NotZero(t, event.ID) assert.Equal(t, inbox.ID, *event.InboxID) assert.Equal(t, user.ID, *event.UserID) } // ========== RollupDaily ========== func TestAnalyticsService_RollupDaily_成功(t *testing.T) { db, _, rollupRepo, svc := setupAnalyticsService(t) account := createTestAccount(t, db) date := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) // Create events for that day createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameFirstResponse e.Value = 120.0 e.ValueInBusinessHours = 60.0 e.CreatedAt = date.Add(2 * time.Hour) }) createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameFirstResponse e.Value = 240.0 e.ValueInBusinessHours = 120.0 e.CreatedAt = date.Add(5 * time.Hour) }) err := svc.RollupDaily(context.Background(), account.ID, date) require.NoError(t, err) // Verify rollups were created for the first_response metric since := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 5, 20, 23, 59, 59, 0, time.UTC) rollups, err := rollupRepo.FindByAccountAndDateRange(context.Background(), account.ID, since, until) require.NoError(t, err) assert.NotEmpty(t, rollups, "expected at least one rollup record to be created") // Find the account-dimension rollup for first_response var accountRollup *model.ReportingEventsRollup for _, r := range rollups { if r.DimensionType == model.DimensionAccount && r.Metric == model.MetricFirstResponse { accountRollup = &r break } } require.NotNil(t, accountRollup, "expected account-level first_response rollup") assert.Equal(t, int64(2), accountRollup.Count) assert.InDelta(t, 360.0, accountRollup.SumValue, 0.01) // 120+240 assert.InDelta(t, 180.0, accountRollup.SumValueBusinessHours, 0.01) // 60+120 } func TestAnalyticsService_RollupDaily_无事件(t *testing.T) { db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) date := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) err := svc.RollupDaily(context.Background(), account.ID, date) require.NoError(t, err) // should not error even with no events } func TestAnalyticsService_RollupDaily_多metric类型(t *testing.T) { db, _, rollupRepo, svc := setupAnalyticsService(t) account := createTestAccount(t, db) date := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) // Create events with different metric names createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameFirstResponse e.Value = 100.0 e.ValueInBusinessHours = 50.0 e.CreatedAt = date.Add(1 * time.Hour) }) createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameResolutionTime e.Value = 500.0 e.ValueInBusinessHours = 250.0 e.CreatedAt = date.Add(3 * time.Hour) }) err := svc.RollupDaily(context.Background(), account.ID, date) require.NoError(t, err) since := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 5, 20, 23, 59, 59, 0, time.UTC) rollups, err := rollupRepo.FindByAccountAndDateRange(context.Background(), account.ID, since, until) require.NoError(t, err) // We should have rollups for at least 2 different metrics across 4 dimension types metricSet := map[model.RollupMetric]bool{} for _, r := range rollups { metricSet[r.Metric] = true } assert.True(t, metricSet[model.MetricFirstResponse], "expected first_response rollup") assert.True(t, metricSet[model.MetricResolutionTime], "expected resolution_time rollup") } // ========== groupEventsByDimension (indirect via RollupDaily) ========== func TestAnalyticsService_RollupDaily_agent维度分组(t *testing.T) { db, _, rollupRepo, svc := setupAnalyticsService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) date := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) // Create event with a user_id (agent dimension) createTestReportingEvent(t, db, account.ID, func(e *model.ReportingEvent) { e.Name = model.MetricNameFirstResponse e.UserID = &user.ID e.Value = 200.0 e.ValueInBusinessHours = 100.0 e.CreatedAt = date.Add(2 * time.Hour) }) err := svc.RollupDaily(context.Background(), account.ID, date) require.NoError(t, err) since := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 5, 20, 23, 59, 59, 0, time.UTC) rollups, err := rollupRepo.FindByAccountAndDateRange(context.Background(), account.ID, since, until) require.NoError(t, err) // Find the agent-dimension rollup var agentRollup *model.ReportingEventsRollup for _, r := range rollups { if r.DimensionType == model.DimensionAgent && r.DimensionID == user.ID { agentRollup = &r break } } require.NotNil(t, agentRollup, "expected agent-dimension rollup with user's ID") assert.Equal(t, int64(1), agentRollup.Count) } // ========== PG-only feature tests (skipped with SQLite) ========== // The following tests exercise features that rely on PG-specific SQL // (ILIKE, date_trunc, NOW(), etc.). They are skipped under SQLite. func TestAnalyticsService_GetSummary_PG日期范围查询(t *testing.T) { skipIfSQLite(t) // This would test date_trunc/ILIKE-based queries that only work on PG. // Under SQLite it is automatically skipped. db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) since := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until := time.Date(2026, 1, 31, 23, 59, 59, 0, time.UTC) resp, err := svc.GetSummary(context.Background(), account.ID, since, until) require.NoError(t, err) assert.NotNil(t, resp) } func TestAnalyticsService_RollupDaily_PG复杂聚合(t *testing.T) { skipIfSQLite(t) // This would test PG-only aggregation features. db, _, _, svc := setupAnalyticsService(t) account := createTestAccount(t, db) date := time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) err := svc.RollupDaily(context.Background(), account.ID, date) require.NoError(t, err) }