package reporting import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" ) func newTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate( &model.ReportingEvent{}, &model.ReportingEventsRollup{}, &model.Conversation{}, &model.Message{}, ) require.NoError(t, err) return db } func TestNewReportingService(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) require.NotNil(t, svc) assert.Equal(t, db, svc.db) } func TestReportingService_CreateEvent(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() event := &ReportingEvent{ AccountID: 1, Name: "first_response", Value: 120.5, EventStartTime: now, EventEndTime: now, } err := svc.CreateEvent(ctx, event) require.NoError(t, err) assert.NotZero(t, event.ID) } func TestReportingService_GetByID(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() event := &ReportingEvent{ AccountID: 1, Name: "first_response", Value: 100.0, EventStartTime: now, EventEndTime: now, } err := svc.CreateEvent(ctx, event) require.NoError(t, err) retrieved, err := svc.GetEvent(ctx, event.ID) require.NoError(t, err) assert.Equal(t, event.Name, retrieved.Name) assert.Equal(t, event.Value, retrieved.Value) } func TestReportingService_GetByID_NotFound(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() _, err := svc.GetEvent(ctx, 99999) assert.Error(t, err) assert.ErrorIs(t, err, gorm.ErrRecordNotFound) } func TestReportingService_ListEventsByAccount(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() for i := 0; i < 3; i++ { err := svc.CreateEvent(ctx, &ReportingEvent{ AccountID: 1, Name: "first_response", Value: float64(i * 10), EventStartTime: now, EventEndTime: now, }) require.NoError(t, err) } // Different account err := svc.CreateEvent(ctx, &ReportingEvent{ AccountID: 2, Name: "first_response", Value: 0, EventStartTime: now, EventEndTime: now, }) require.NoError(t, err) events, err := svc.ListEventsByAccount(ctx, 1, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Len(t, events, 3) } func TestReportingService_ListEventsByAccountAndName(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{ AccountID: 1, Name: "first_response", Value: 100, EventStartTime: now, EventEndTime: now, })) require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{ AccountID: 1, Name: "resolution_time", Value: 200, EventStartTime: now, EventEndTime: now, })) require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{ AccountID: 1, Name: "first_response", Value: 150, EventStartTime: now, EventEndTime: now, })) events, err := svc.ListEventsByAccountAndName(ctx, 1, "first_response", now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Len(t, events, 2) } func TestReportingService_CreateRollup(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() rollup := &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5, SumValue: 100.0, SumValueBusinessHours: 80.0, } err := svc.CreateRollup(ctx, rollup) require.NoError(t, err) assert.NotZero(t, rollup.ID) } func TestReportingService_CreateRollup_Upsert(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() rollup := &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5, SumValue: 100.0, SumValueBusinessHours: 80.0, } err := svc.CreateRollup(ctx, rollup) require.NoError(t, err) // Upsert with same unique key — should update not create new rollup2 := &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 10, SumValue: 200.0, SumValueBusinessHours: 160.0, } err = svc.CreateRollup(ctx, rollup2) require.NoError(t, err) // Should still be only one record rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Len(t, rollups, 1) } func TestReportingService_GetRollups(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5, })) require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAgent, DimensionID: 2, Metric: MetricFirstResponse, Count: 3, })) rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Len(t, rollups, 1) assert.Equal(t, MetricResolutionsCount, rollups[0].Metric) } func TestReportingService_GetRollupsByMetric(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5, })) require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricFirstResponse, Count: 3, })) rollups, err := svc.GetRollupsByMetric(ctx, 1, DimensionAccount, 1, MetricResolutionsCount, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Len(t, rollups, 1) assert.Equal(t, MetricResolutionsCount, rollups[0].Metric) } func TestReportingService_DeleteRollupsByDate(t *testing.T) { db := newTestDB(t) svc := NewReportingService(db) ctx := context.Background() now := time.Now() err := svc.CreateRollup(ctx, &ReportingEventsRollup{ AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5, }) require.NoError(t, err) err = svc.DeleteRollupsByDate(ctx, 1, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour)) require.NoError(t, err) assert.Empty(t, rollups) } func TestNewRawDataSource(t *testing.T) { ds := NewRawDataSource() require.NotNil(t, ds) } func TestGetMetricDefinition_Found(t *testing.T) { def, ok := GetMetricDefinition("avg_first_response_time") require.True(t, ok) assert.Equal(t, AggregateAverage, def.AggregateType) assert.Equal(t, "first_response", def.RawEventName) assert.Equal(t, MetricFirstResponse, def.RollupMetric) } func TestGetMetricDefinition_NotFound(t *testing.T) { _, ok := GetMetricDefinition("nonexistent_metric") assert.False(t, ok) } func TestAllMetricKeys(t *testing.T) { keys := AllMetricKeys() assert.NotEmpty(t, keys) // Should contain all registered metrics assert.Contains(t, keys, "avg_first_response_time") assert.Contains(t, keys, "avg_resolution_time") assert.Contains(t, keys, "resolutions_count") } func TestReportMetricRegistry_AllMetricsHaveDefinitions(t *testing.T) { for key, def := range ReportMetricRegistry { assert.NotEmpty(t, def.RawEventName, "metric %s should have RawEventName", key) assert.NotEmpty(t, def.SummaryKey, "metric %s should have SummaryKey", key) assert.NotEmpty(t, string(def.RollupMetric), "metric %s should have RollupMetric", key) } } // --- ReportingEventListener Tests --- func TestNewReportingEventListener(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) require.NotNil(t, l) assert.Equal(t, "reporting_event_listener", l.Name()) } func TestReportingEventListener_OnEvent_UnknownEvent(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() event := &channel.ChannelEvent{Type: "unknown.event", Data: map[string]interface{}{}} err := l.OnEvent(ctx, event) assert.NoError(t, err) } func TestReportingEventListener_OnConversationCreated(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() conv := &model.Conversation{ Base: model.Base{ID: 1}, AccountID: 10, InboxID: 20, Status: "open", } event := &channel.ChannelEvent{ Type: channel.EventConversationCreated, AccountID: 10, InboxID: 20, Data: map[string]interface{}{"conversation": conv}, } err := l.OnEvent(ctx, event) require.NoError(t, err) // Verify reporting event was created var count int64 l.db.WithContext(ctx).Model(&model.ReportingEvent{}).Where("account_id = ?", 10).Count(&count) assert.True(t, count > 0, "should have created a reporting event") } func TestReportingEventListener_OnConversationCreated_NoConversation(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() event := &channel.ChannelEvent{ Type: channel.EventConversationCreated, Data: map[string]interface{}{}, } err := l.OnEvent(ctx, event) assert.Error(t, err) } func TestReportingEventListener_OnConversationResolved(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() conv := &model.Conversation{ Base: model.Base{ID: 1}, AccountID: 10, InboxID: 20, Status: "resolved", } event := &channel.ChannelEvent{ Type: channel.EventConversationResolved, AccountID: 10, InboxID: 20, Data: map[string]interface{}{"conversation": conv}, } err := l.OnEvent(ctx, event) require.NoError(t, err) } func TestReportingEventListener_OnConversationAssigned(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() conv := &model.Conversation{ Base: model.Base{ID: 1}, AccountID: 10, InboxID: 20, Status: "open", } event := &channel.ChannelEvent{ Type: channel.EventConversationAssigned, AccountID: 10, InboxID: 20, Data: map[string]interface{}{"conversation": conv}, } err := l.OnEvent(ctx, event) require.NoError(t, err) } func TestReportingEventListener_OnMessageCreated_Outgoing(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() msg := &model.Message{ Base: model.Base{ID: 1}, ConversationID: 1, AccountID: 10, InboxID: 20, MessageType: "outgoing", SenderType: "agent", } event := &channel.ChannelEvent{ Type: channel.EventMessageCreated, AccountID: 10, InboxID: 20, Data: map[string]interface{}{"message": msg}, } err := l.OnEvent(ctx, event) require.NoError(t, err) } func TestReportingEventListener_OnMessageCreated_Incoming(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() msg := &model.Message{ Base: model.Base{ID: 1}, ConversationID: 1, AccountID: 10, InboxID: 20, MessageType: "incoming", SenderType: "contact", } event := &channel.ChannelEvent{ Type: channel.EventMessageCreated, AccountID: 10, InboxID: 20, Data: map[string]interface{}{"message": msg}, } err := l.OnEvent(ctx, event) // Incoming messages should be ignored — no error, no event assert.NoError(t, err) } func TestReportingEventListener_OnMessageCreated_NoMessage(t *testing.T) { db := newTestDB(t) l := NewReportingEventListener(db) ctx := context.Background() event := &channel.ChannelEvent{ Type: channel.EventMessageCreated, Data: map[string]interface{}{}, } err := l.OnEvent(ctx, event) // No message in event — returns nil (not all message events carry a message) assert.NoError(t, err) }