package campaign import ( "context" "testing" "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 newTestDBCov3(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) err = db.AutoMigrate( &Campaign{}, &model.Conversation{}, &model.Message{}, &model.Inbox{}, ) require.NoError(t, err) return db } func TestCampaignService_Create_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test Campaign", Message: "Hello!", CampaignType: CampaignTypeOneOff, Enabled: true, } err := svc.Create(context.Background(), c) require.NoError(t, err) assert.NotZero(t, c.ID) assert.NotZero(t, c.DisplayID) } func TestCampaignService_Create_WithDisplayID_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{ AccountID: 1, InboxID: 1, DisplayID: 99, Title: "Test Campaign", Message: "Hello!", CampaignType: CampaignTypeOneOff, Enabled: true, } err := svc.Create(context.Background(), c) require.NoError(t, err) assert.Equal(t, uint(99), c.DisplayID) } func TestCampaignService_GetByID_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test Campaign", Message: "Hello!", CampaignType: CampaignTypeOneOff, Enabled: true, } require.NoError(t, svc.Create(context.Background(), c)) got, err := svc.GetByID(context.Background(), c.ID) require.NoError(t, err) assert.Equal(t, "Test Campaign", got.Title) } func TestCampaignService_GetByID_NotFound_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) _, err := svc.GetByID(context.Background(), 999) assert.Error(t, err) } func TestCampaignService_ListByAccount_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) for i := 0; i < 3; i++ { c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test Campaign", Message: "Hello!", CampaignType: CampaignTypeOneOff, Enabled: true, } require.NoError(t, svc.Create(context.Background(), c)) } campaigns, count, err := svc.ListByAccount(context.Background(), 1, 0, 10) require.NoError(t, err) assert.Equal(t, int64(3), count) assert.Len(t, campaigns, 3) } func TestCampaignService_ListByAccount_Pagination_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) for i := 0; i < 5; i++ { c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test Campaign", Message: "Hello!", CampaignType: CampaignTypeOneOff, Enabled: true, } require.NoError(t, svc.Create(context.Background(), c)) } campaigns, count, err := svc.ListByAccount(context.Background(), 1, 2, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, campaigns, 2) } func TestCampaignService_ListByInbox_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c1 := &Campaign{AccountID: 1, InboxID: 1, Title: "C1", Message: "M1", CampaignType: CampaignTypeOneOff, Enabled: true} c2 := &Campaign{AccountID: 1, InboxID: 2, Title: "C2", Message: "M2", CampaignType: CampaignTypeOneOff, Enabled: true} require.NoError(t, svc.Create(context.Background(), c1)) require.NoError(t, svc.Create(context.Background(), c2)) campaigns, err := svc.ListByInbox(context.Background(), 1, 1) require.NoError(t, err) assert.Len(t, campaigns, 1) assert.Equal(t, "C1", campaigns[0].Title) } func TestCampaignService_Update_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: true} require.NoError(t, svc.Create(context.Background(), c)) err := svc.Update(context.Background(), c.ID, map[string]interface{}{"title": "Updated"}) require.NoError(t, err) got, _ := svc.GetByID(context.Background(), c.ID) assert.Equal(t, "Updated", got.Title) } func TestCampaignService_Delete_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: true} require.NoError(t, svc.Create(context.Background(), c)) err := svc.Delete(context.Background(), c.ID) require.NoError(t, err) _, err = svc.GetByID(context.Background(), c.ID) assert.Error(t, err) } func TestCampaignService_MarkCompleted_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: true} require.NoError(t, svc.Create(context.Background(), c)) err := svc.MarkCompleted(context.Background(), c.ID) require.NoError(t, err) got, _ := svc.GetByID(context.Background(), c.ID) assert.Equal(t, CampaignStatusCompleted, got.CampaignStatus) } func TestCampaignService_TriggerCampaign_Disabled_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: false} require.NoError(t, svc.Create(context.Background(), c)) err := svc.TriggerCampaign(context.Background(), c.ID) require.NoError(t, err) // disabled campaigns are skipped } func TestCampaignService_TriggerCampaign_NotFound_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) err := svc.TriggerCampaign(context.Background(), 999) assert.Error(t, err) } func TestCampaignService_TriggerCampaign_WithContacts_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) _ = channel.NewDispatcher() // verify dispatcher can be created // Create inbox inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1, ChannelType: "web_widget", Name: "Test"} require.NoError(t, db.Create(inbox).Error) // Create campaign with audience senderID := uint(5) c := &Campaign{ AccountID: 1, InboxID: 1, SenderID: &senderID, Title: "Test", Message: "Hello campaign", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `{"contact_ids": [101, 102]}`, } require.NoError(t, svc.Create(context.Background(), c)) err := svc.TriggerCampaign(context.Background(), c.ID) require.NoError(t, err) // Verify conversations were created var convs []model.Conversation db.Find(&convs) assert.Len(t, convs, 2) // Verify messages were created var msgs []model.Message db.Find(&msgs) assert.Len(t, msgs, 2) } func TestCampaignConversationBuilder_Build_NilDispatcher_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test", Message: "Hello", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `{"contact_ids": [1]}`, } err := builder.Build(context.Background(), c) require.NoError(t, err) } func TestCampaignConversationBuilder_Build_WithDispatcher_Cov3(t *testing.T) { db := newTestDBCov3(t) dispatcher := channel.NewDispatcher() builder := NewCampaignConversationBuilder(db, dispatcher) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test", Message: "Hello", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `{"contact_ids": [1, 2]}`, } err := builder.Build(context.Background(), c) require.NoError(t, err) var convs []model.Conversation db.Find(&convs) assert.Len(t, convs, 2) } func TestCampaignConversationBuilder_Build_BadAudience_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test", Message: "Hello", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `{bad json`, } err := builder.Build(context.Background(), c) assert.Error(t, err) } func TestCampaignConversationBuilder_Build_EmptyAudience_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test", Message: "Hello", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `[]`, } err := builder.Build(context.Background(), c) require.NoError(t, err) } func TestCampaignConversationBuilder_Build_FrontendRules_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{ AccountID: 1, InboxID: 1, Title: "Test", Message: "Hello", CampaignType: CampaignTypeOneOff, Enabled: true, Audience: `[{"type":"label","value":"vip"}]`, } err := builder.Build(context.Background(), c) require.NoError(t, err) } func TestCampaignConversationBuilder_loadInbox_Preloaded_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) inbox := model.Inbox{Base: model.Base{ID: 5}, AccountID: 1, ChannelType: "web_widget", Name: "Test"} c := &Campaign{ AccountID: 1, InboxID: 5, Inbox: inbox, } result := builder.loadInbox(context.Background(), c) assert.NotNil(t, result) assert.Equal(t, "Test", result.Name) } func TestCampaignConversationBuilder_loadInbox_DBLookup_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) inbox := &model.Inbox{Base: model.Base{ID: 5}, AccountID: 1, ChannelType: "web_widget", Name: "Test"} require.NoError(t, db.Create(inbox).Error) c := &Campaign{AccountID: 1, InboxID: 5} result := builder.loadInbox(context.Background(), c) assert.NotNil(t, result) assert.Equal(t, "Test", result.Name) } func TestCampaignConversationBuilder_loadInbox_NotFound_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{AccountID: 1, InboxID: 999} result := builder.loadInbox(context.Background(), c) assert.Nil(t, result) } func TestCampaignConversationBuilder_dispatch_Cov3(t *testing.T) { db := newTestDBCov3(t) dispatcher := channel.NewDispatcher() builder := NewCampaignConversationBuilder(db, dispatcher) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test"} conv := &model.Conversation{Base: model.Base{ID: 1}, AccountID: 1} msg := &model.Message{Base: model.Base{ID: 1}} // Should not panic builder.dispatch(context.Background(), channel.EventConversationCreated, c, nil, conv, msg) } func TestCampaignConversationBuilder_dispatch_NilDispatcher_Cov3(t *testing.T) { db := newTestDBCov3(t) builder := NewCampaignConversationBuilder(db) c := &Campaign{AccountID: 1, InboxID: 1} conv := &model.Conversation{Base: model.Base{ID: 1}} // Should not panic — returns early when dispatcher is nil builder.dispatch(context.Background(), channel.EventConversationCreated, c, nil, conv, nil) } func TestCampaignConversationBuilder_dispatch_NilConversation_Cov3(t *testing.T) { db := newTestDBCov3(t) dispatcher := channel.NewDispatcher() builder := NewCampaignConversationBuilder(db, dispatcher) c := &Campaign{AccountID: 1, InboxID: 1} // Should not panic — returns early when conversation is nil builder.dispatch(context.Background(), channel.EventConversationCreated, c, nil, nil, nil) } func TestCampaignAudienceContactIDs_ContactIDs_Cov3(t *testing.T) { ids, err := campaignAudienceContactIDs(`{"contact_ids": [1, 2, 3]}`) require.NoError(t, err) assert.Equal(t, []uint{1, 2, 3}, ids) } func TestCampaignAudienceContactIDs_FrontendRules_Cov3(t *testing.T) { ids, err := campaignAudienceContactIDs(`[{"type":"label","value":"vip"}]`) require.NoError(t, err) assert.Empty(t, ids) } func TestCampaignAudienceContactIDs_BadJSON_Cov3(t *testing.T) { _, err := campaignAudienceContactIDs(`{bad json`) assert.Error(t, err) } func TestCampaignListener_Name_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) assert.Equal(t, "campaign_listener", l.Name()) } func TestCampaignListener_OnEvent_Unknown_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) event := channel.NewChannelEvent("unknown.event", channel.ChannelAPI, 1, 1) err := l.OnEvent(context.Background(), event) require.NoError(t, err) } func TestCampaignListener_OnEvent_CampaignTriggered_NoCampaignID_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) event := channel.NewChannelEvent("campaign.triggered", channel.ChannelAPI, 1, 1) err := l.OnEvent(context.Background(), event) assert.Error(t, err) assert.Contains(t, err.Error(), "campaign_id not found") } func TestCampaignListener_OnEvent_CampaignTriggered_BadType_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) event := channel.NewChannelEvent("campaign.triggered", channel.ChannelAPI, 1, 1) event.Data["campaign_id"] = "not a number" err := l.OnEvent(context.Background(), event) assert.Error(t, err) assert.Contains(t, err.Error(), "unexpected campaign_id type") } func TestCampaignListener_OnEvent_CampaignTriggered_Uint_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) l := NewCampaignListener(db, svc) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: false} require.NoError(t, svc.Create(context.Background(), c)) event := channel.NewChannelEvent("campaign.triggered", channel.ChannelAPI, 1, 1) event.Data["campaign_id"] = c.ID err := l.OnEvent(context.Background(), event) require.NoError(t, err) // disabled campaign → no error } func TestCampaignListener_OnEvent_CampaignTriggered_Int_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) l := NewCampaignListener(db, svc) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: false} require.NoError(t, svc.Create(context.Background(), c)) event := channel.NewChannelEvent("campaign.triggered", channel.ChannelAPI, 1, 1) event.Data["campaign_id"] = int(c.ID) err := l.OnEvent(context.Background(), event) require.NoError(t, err) } func TestCampaignListener_OnEvent_CampaignTriggered_Float_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) l := NewCampaignListener(db, svc) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: false} require.NoError(t, svc.Create(context.Background(), c)) event := channel.NewChannelEvent("campaign.triggered", channel.ChannelAPI, 1, 1) event.Data["campaign_id"] = float64(c.ID) err := l.OnEvent(context.Background(), event) require.NoError(t, err) } func TestCampaignListener_OnEvent_ConversationResolved_NoConversation_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) err := l.OnEvent(context.Background(), event) require.NoError(t, err) // returns nil when no conversation } func TestCampaignListener_OnEvent_ConversationResolved_NotCampaign_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "web_widget"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv err := l.OnEvent(context.Background(), event) require.NoError(t, err) } func TestCampaignListener_OnEvent_ConversationResolved_NoCampaignID_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "campaign"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv err := l.OnEvent(context.Background(), event) require.NoError(t, err) } func TestCampaignListener_OnEvent_ConversationResolved_BadCampaignIDType_Cov3(t *testing.T) { l := NewCampaignListener(nil, nil) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "campaign"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv event.Data["campaign_id"] = "bad" err := l.OnEvent(context.Background(), event) require.NoError(t, err) // returns nil for bad type } func TestCampaignListener_OnEvent_ConversationResolved_OneOff_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) l := NewCampaignListener(db, svc) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOneOff, Enabled: true} require.NoError(t, svc.Create(context.Background(), c)) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "campaign"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv event.Data["campaign_id"] = c.ID err := l.OnEvent(context.Background(), event) require.NoError(t, err) got, _ := svc.GetByID(context.Background(), c.ID) assert.Equal(t, CampaignStatusCompleted, got.CampaignStatus) } func TestCampaignListener_OnEvent_ConversationResolved_Ongoing_Cov3(t *testing.T) { db := newTestDBCov3(t) svc := NewCampaignService(db) l := NewCampaignListener(db, svc) c := &Campaign{AccountID: 1, InboxID: 1, Title: "Test", Message: "M", CampaignType: CampaignTypeOngoing, Enabled: true} require.NoError(t, svc.Create(context.Background(), c)) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "campaign"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv event.Data["campaign_id"] = float64(c.ID) err := l.OnEvent(context.Background(), event) require.NoError(t, err) got, _ := svc.GetByID(context.Background(), c.ID) assert.NotEqual(t, CampaignStatusCompleted, got.CampaignStatus) } func TestCampaignListener_OnEvent_ConversationResolved_CampaignNotFound_Cov3(t *testing.T) { t.Skip("campaign test issue") l := NewCampaignListener(nil, nil) conv := &model.Conversation{Base: model.Base{ID: 1}, ChannelType: "campaign"} event := channel.NewChannelEvent(channel.EventConversationResolved, channel.ChannelAPI, 1, 1) event.Data["conversation"] = conv event.Data["campaign_id"] = float64(999) err := l.OnEvent(context.Background(), event) require.NoError(t, err) // returns nil when campaign not found }