package service import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/repository" ) type fakeWhatsAppCallProvider struct { initiateID string permissionRequired bool permissionMessageID string preAccepts int accepts int rejects int terminates int } func (f *fakeWhatsAppCallProvider) InitiateCall(context.Context, *channelmodel.ChannelWhatsApp, string, string) (string, error) { if f.permissionRequired { return "", ErrWhatsAppCallPermissionRequired } if f.initiateID == "" { return "wacid_outbound", nil } return f.initiateID, nil } func (f *fakeWhatsAppCallProvider) PreAcceptCall(context.Context, *channelmodel.ChannelWhatsApp, string, string) error { f.preAccepts++ return nil } func (f *fakeWhatsAppCallProvider) AcceptCall(context.Context, *channelmodel.ChannelWhatsApp, string, string) error { f.accepts++ return nil } func (f *fakeWhatsAppCallProvider) RejectCall(context.Context, *channelmodel.ChannelWhatsApp, string) error { f.rejects++ return nil } func (f *fakeWhatsAppCallProvider) TerminateCall(context.Context, *channelmodel.ChannelWhatsApp, string) error { f.terminates++ return nil } func (f *fakeWhatsAppCallProvider) SendCallPermissionRequest(context.Context, *channelmodel.ChannelWhatsApp, string, string) (string, error) { if f.permissionMessageID == "" { return "wamid.req_xyz", nil } return f.permissionMessageID, nil } func setupWhatsAppCallServiceTest(t *testing.T, provider *fakeWhatsAppCallProvider) (*gorm.DB, *WhatsAppCallService, *model.Account, *model.Inbox, *model.Contact, *model.Conversation) { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.Call{}, &model.Message{}, &model.Attachment{}, &channelmodel.ChannelWhatsApp{})) account := &model.Account{Name: "Voice Account", Status: "active"} require.NoError(t, db.Create(account).Error) inbox := &model.Inbox{AccountID: account.ID, Name: "WhatsApp", ChannelType: "whatsapp", ChannelID: 1, ChannelConfig: `{"voice_enabled":true}`} require.NoError(t, db.Create(inbox).Error) waChannel := &channelmodel.ChannelWhatsApp{AccountID: account.ID, InboxID: inbox.ID, PhoneNumber: "+15550000000", PhoneNumberID: "phone-1", BusinessAccountID: "waba-1", AccessToken: "token", Provider: "whatsapp_cloud", ProviderConfig: `{"calling_enabled":true}`} require.NoError(t, db.Create(waChannel).Error) inbox.ChannelID = waChannel.ID require.NoError(t, db.Save(inbox).Error) contact := &model.Contact{AccountID: account.ID, Name: "Ada", PhoneNumber: "+15551234567"} require.NoError(t, db.Create(contact).Error) displayID := uint(42) conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, DisplayID: &displayID, Status: "open", ChannelType: "whatsapp", Channel: "whatsapp"} require.NoError(t, db.Create(conversation).Error) return db, NewWhatsAppCallService(repository.NewWhatsAppCallRepo(db), provider), account, inbox, contact, conversation } func TestWhatsAppCallService_InitiateCreatesOutboundCallAndMessage(t *testing.T) { provider := &fakeWhatsAppCallProvider{initiateID: "wacid_outbound"} db, svc, account, _, _, conversation := setupWhatsAppCallServiceTest(t, provider) result, err := svc.Initiate(context.Background(), account.ID, WhatsAppCallInitiateRequest{ConversationID: *conversation.DisplayID, SDPOffer: "sdp_offer", AgentID: 7}) require.NoError(t, err) require.Equal(t, "wacid_outbound", result.Call.ProviderCallID) var call model.Call require.NoError(t, db.First(&call, result.Call.ID).Error) require.Equal(t, "whatsapp", call.Provider) require.Equal(t, "outgoing", call.Direction) require.Equal(t, "ringing", call.Status) require.NotNil(t, call.MessageID) var message model.Message require.NoError(t, db.First(&message, *call.MessageID).Error) require.Equal(t, "voice_call", message.ContentType) require.Equal(t, "outgoing", message.MessageType) } func TestWhatsAppCallService_InitiatePermissionRequestRecordsConversation(t *testing.T) { provider := &fakeWhatsAppCallProvider{permissionRequired: true, permissionMessageID: "wamid.req_xyz"} db, svc, account, _, _, conversation := setupWhatsAppCallServiceTest(t, provider) result, err := svc.Initiate(context.Background(), account.ID, WhatsAppCallInitiateRequest{ConversationID: *conversation.DisplayID, SDPOffer: "sdp_offer", AgentID: 7}) require.NoError(t, err) require.Equal(t, "permission_requested", result.PermissionStatus) var stored model.Conversation require.NoError(t, db.First(&stored, conversation.ID).Error) attrs := conversationAttributes(&stored) require.Equal(t, "wamid.req_xyz", attrs["call_permission_request_message_id"]) require.NotEmpty(t, attrs["call_permission_requested_at"]) } func TestWhatsAppCallService_AcceptRejectTerminateAndRecording(t *testing.T) { provider := &fakeWhatsAppCallProvider{} db, svc, account, inbox, contact, conversation := setupWhatsAppCallServiceTest(t, provider) call := &model.Call{AccountID: account.ID, InboxID: inbox.ID, ConversationID: conversation.ID, ContactID: contact.ID, Provider: "whatsapp", Direction: "incoming", ProviderCallID: "wacid_in", Status: "ringing", CallerType: "Contact", CallerID: contact.ID, CallDirection: "inbound"} require.NoError(t, db.Create(call).Error) message := &model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: conversation.ID, ContentType: "voice_call", MessageType: "incoming", Content: "WhatsApp voice call"} require.NoError(t, db.Create(message).Error) require.NoError(t, db.Model(call).Update("message_id", message.ID).Error) accepted, err := svc.Accept(context.Background(), account.ID, call.ID, 8, "sdp_answer") require.NoError(t, err) require.Equal(t, "in_progress", accepted.Status) require.Equal(t, 1, provider.preAccepts) require.Equal(t, 1, provider.accepts) require.NotNil(t, accepted.StartedAt) terminated, err := svc.Terminate(context.Background(), account.ID, call.ID, 8) require.NoError(t, err) require.Equal(t, "completed", terminated.Status) require.Equal(t, 1, provider.terminates) status, err := svc.UploadRecording(context.Background(), account.ID, call.ID, "call.webm", 100) require.NoError(t, err) require.Equal(t, "uploaded", status) status, err = svc.UploadRecording(context.Background(), account.ID, call.ID, "call.webm", 100) require.NoError(t, err) require.Equal(t, "already_uploaded", status) } func TestWhatsAppCallService_InitiateValidatesReferenceGuards(t *testing.T) { provider := &fakeWhatsAppCallProvider{} db, svc, account, inbox, contact, conversation := setupWhatsAppCallServiceTest(t, provider) _, err := svc.Initiate(context.Background(), account.ID, WhatsAppCallInitiateRequest{ConversationID: *conversation.DisplayID, AgentID: 7}) require.True(t, errors.Is(err, ErrWhatsAppCallSDPOfferRequired)) require.NoError(t, db.Model(contact).Update("phone_number", "").Error) _, err = svc.Initiate(context.Background(), account.ID, WhatsAppCallInitiateRequest{ConversationID: *conversation.DisplayID, SDPOffer: "sdp_offer", AgentID: 7}) require.True(t, errors.Is(err, ErrWhatsAppCallContactPhoneRequired)) require.NoError(t, db.Model(contact).Update("phone_number", "+15551234567").Error) require.NoError(t, db.Model(inbox).Update("channel_config", `{"voice_enabled":false}`).Error) require.NoError(t, db.Model(&channelmodel.ChannelWhatsApp{}).Where("inbox_id = ?", inbox.ID).Update("provider_config", `{"calling_enabled":false}`).Error) _, err = svc.Initiate(context.Background(), account.ID, WhatsAppCallInitiateRequest{ConversationID: *conversation.DisplayID, SDPOffer: "sdp_offer", AgentID: 7}) require.True(t, errors.Is(err, ErrWhatsAppCallNotEnabled)) } func TestWhatsAppCallService_TerminateUsesNoAnswerBeforeProgress(t *testing.T) { provider := &fakeWhatsAppCallProvider{} db, svc, account, inbox, contact, conversation := setupWhatsAppCallServiceTest(t, provider) call := &model.Call{AccountID: account.ID, InboxID: inbox.ID, ConversationID: conversation.ID, ContactID: contact.ID, Provider: "whatsapp", Direction: "outgoing", ProviderCallID: "wacid_out", Status: "ringing", CallerType: "User", CallerID: 7, CallDirection: "outbound", StartedAt: ptrTime(time.Now())} require.NoError(t, db.Create(call).Error) terminated, err := svc.Terminate(context.Background(), account.ID, call.ID, 7) require.NoError(t, err) require.Equal(t, "no_answer", terminated.Status) } func ptrTime(t time.Time) *time.Time { return &t }