package com.example.service.unit_testing; import com.example.dto.dialogflow.base.DetectIntentResponseDTO; import com.example.dto.dialogflow.conversation.*; import com.example.dto.quickreplies.QuestionDTO; import com.example.dto.quickreplies.QuickReplyDTO; import com.example.mapper.conversation.ConversationEntryMapper; import com.example.service.conversation.ConversationManagerService; import com.example.service.conversation.FirestoreConversationService; import com.example.service.conversation.MemoryStoreConversationService; import com.example.service.quickreplies.QuickRepliesManagerService; import com.example.service.quickreplies.QuickReplyContentService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Instant; import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class QuickRepliesManagerServiceTest { @Mock private MemoryStoreConversationService memoryStoreConversationService; @Mock private FirestoreConversationService firestoreConversationService; @Mock private QuickReplyContentService quickReplyContentService; @Mock private ConversationManagerService conversationManagerService; @Mock private ConversationEntryMapper conversationEntryMapper; private QuickRepliesManagerService quickRepliesManagerService; // Test Data private final String PHONE = "5555555555"; private final String SESSION_ID = "session-123"; private final String USER_ID = "user_by_phone_5555555555"; private final String CONTEXTO = "pagos"; @BeforeEach void setUp() { quickRepliesManagerService = new QuickRepliesManagerService( conversationManagerService, memoryStoreConversationService, firestoreConversationService, quickReplyContentService, conversationEntryMapper ); } @Test @DisplayName("manageConversation - Count 0 - NO MATCH: Should clear context and delegate to Dialogflow") void manageConversation_Count0_NoMatch_ShouldDelegate() { // 1. SETUP: User typed "Hola", but context "pagos" is active. ExternalConvRequestDTO request = new ExternalConvRequestDTO("Hola", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null); ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO); // Mock Session Retrieval when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session)); // Mock History: Only the SYSTEM message (The Menu) exists. Count = 0. ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp"); when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg)); // Mock QR Content: The menu has options, but "Hola" is NOT one of them. QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo is 10"); QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1)); when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto)); // Mock Orchestrator Delegation (The expected outcome) DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("Hola soy Beto", null)); when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty()); // Clearing context when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse)); // 2. EXECUTE StepVerifier.create(quickRepliesManagerService.manageConversation(request)) .expectNext(delegatedResponse) .verifyComplete(); // 3. VERIFY // Ensure we cleared the context verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null)); // Ensure we called the normal conversation manager verify(conversationManagerService).manageConversation(request); } @Test @DisplayName("manageConversation - Count 0 - MATCH: Should return QR Answer") void manageConversation_Count0_Match_ShouldAnswer() { // 1. SETUP: User typed "Ver Saldo" (Valid Option) ExternalConvRequestDTO request = new ExternalConvRequestDTO("Ver Saldo", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null); ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO); when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session)); // Count 0 (Last message was System) ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp"); when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg)); // Valid Option exists QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 100 pesos"); QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1)); when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto)); // Mocks for saving the conversation turn mockPersistence(); when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty()); // 2. EXECUTE StepVerifier.create(quickRepliesManagerService.manageConversation(request)) .assertNext(response -> { // Expect the pre-defined answer from the JSON assert response.queryResult().responseText().equals("Tu saldo es 100 pesos"); }) .verifyComplete(); // Verify we did NOT delegate verify(conversationManagerService, never()).manageConversation(any()); } @Test @DisplayName("manageConversation - Count 1 - NO MATCH: Should save User Msg, Clear, and Delegate") void manageConversation_Count1_NoMatch_ShouldDelegate() { // 1. SETUP: User typed "Gracias" (Not an option) ExternalConvRequestDTO request = new ExternalConvRequestDTO("Gracias", new UsuarioDTO(PHONE, "Nick"), "whatsapp", ConversationEntryType.CONVERSACION, null); ConversationSessionDTO session = new ConversationSessionDTO(SESSION_ID, USER_ID, PHONE, Instant.now(), Instant.now(), "last", CONTEXTO); when(memoryStoreConversationService.getSessionByTelefono(PHONE)).thenReturn(Mono.just(session)); // Mock History: System msg -> User msg (Invalid) -> Now this is the 2nd user msg (Count 1 logic) // Wait, logic says count is messages AFTER last init. // Sys (Init) -> User("Hola" - ignored previously) -> Current Request("Gracias") ConversationMessageDTO sysMsg = new ConversationMessageDTO(MessageType.SYSTEM, Instant.now(), "Menu...", null, "whatsapp"); ConversationMessageDTO userMsg1 = new ConversationMessageDTO(MessageType.USER, Instant.now(), "Hola", null, "whatsapp"); when(memoryStoreConversationService.getMessages(SESSION_ID)).thenReturn(Flux.just(sysMsg, userMsg1)); QuestionDTO q1 = new QuestionDTO("Ver Saldo", "desc", "Tu saldo es 10"); QuickReplyDTO qrDto = new QuickReplyDTO("Header", "Body", "Btn", "Section", List.of(q1)); when(quickReplyContentService.getQuickReplies(CONTEXTO)).thenReturn(Mono.just(qrDto)); // Mock persistence for the user message (Manual save in the else block) mockPersistence(); // Mock Delegation DetectIntentResponseDTO delegatedResponse = new DetectIntentResponseDTO("df-response", new QueryResultDTO("De nada", null)); when(memoryStoreConversationService.updateSession(any())).thenReturn(Mono.empty()); when(conversationManagerService.manageConversation(request)).thenReturn(Mono.just(delegatedResponse)); // 2. EXECUTE StepVerifier.create(quickRepliesManagerService.manageConversation(request)) .expectNext(delegatedResponse) .verifyComplete(); // 3. VERIFY // Ensure manual save was called (verifying flow of `persistConversationTurn`) verify(memoryStoreConversationService, times(1)).saveMessage(eq(SESSION_ID), any()); verify(memoryStoreConversationService).updateSession(argThat(s -> s.pantallaContexto() == null)); verify(conversationManagerService).manageConversation(request); } // Helper to mock the complex save chain private void mockPersistence() { when(conversationEntryMapper.toConversationMessageDTO(any())).thenReturn(new ConversationMessageDTO(MessageType.USER, Instant.now(), "text", null, "wa")); when(memoryStoreConversationService.saveSession(any())).thenReturn(Mono.empty()); when(memoryStoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty()); when(firestoreConversationService.saveSession(any())).thenReturn(Mono.empty()); when(firestoreConversationService.saveMessage(anyString(), any())).thenReturn(Mono.empty()); } }