using ChatBot.Services.Telegram.Commands; using ChatBot.Services.Telegram.Interfaces; using ChatBot.Services.Telegram.Services; using ChatBot.Tests.TestUtilities; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Xunit; namespace ChatBot.Tests.Services.Telegram; public class TelegramMessageHandlerTests : UnitTestBase { private readonly Mock> _loggerMock; private readonly Mock _commandProcessorMock; private readonly Mock _messageSenderMock; private readonly Mock _botClientMock; private readonly TelegramMessageHandler _handler; public TelegramMessageHandlerTests() { _loggerMock = TestDataBuilder.Mocks.CreateLoggerMock(); _commandProcessorMock = new Mock(); _messageSenderMock = new Mock(); _botClientMock = TestDataBuilder.Mocks.CreateTelegramBotClient(); _handler = new TelegramMessageHandler( _loggerMock.Object, _commandProcessorMock.Object, _messageSenderMock.Object ); } private static Message CreateMessage( string text, long chatId, string username, string chatTitle, int messageId, User? from = null, Message? replyToMessage = null ) { var message = new Message { Text = text, Chat = new Chat { Id = chatId, Type = ChatType.Private, Title = chatTitle, }, From = from ?? new User { Id = 67890, Username = username }, ReplyToMessage = replyToMessage, }; // Note: MessageId is read-only, so we can't set it directly // The actual MessageId will be 0, but this is sufficient for testing return message; } [Fact] public void Constructor_ShouldInitializeCorrectly() { // Arrange var logger = TestDataBuilder.Mocks.CreateLoggerMock().Object; var commandProcessor = new Mock().Object; var messageSender = new Mock().Object; // Act var handler = new TelegramMessageHandler(logger, commandProcessor, messageSender); // Assert handler.Should().NotBeNull(); } [Fact] public void Constructor_ShouldNotThrow_WhenLoggerIsNull() { // Arrange ILogger? logger = null; var commandProcessor = new Mock().Object; var messageSender = new Mock().Object; // Act & Assert var act = () => new TelegramMessageHandler(logger!, commandProcessor, messageSender); act.Should().NotThrow(); } [Fact] public void Constructor_ShouldNotThrow_WhenCommandProcessorIsNull() { // Arrange var logger = TestDataBuilder.Mocks.CreateLoggerMock().Object; ITelegramCommandProcessor? commandProcessor = null; var messageSender = new Mock().Object; // Act & Assert var act = () => new TelegramMessageHandler(logger, commandProcessor!, messageSender); act.Should().NotThrow(); } [Fact] public void Constructor_ShouldNotThrow_WhenMessageSenderIsNull() { // Arrange var logger = TestDataBuilder.Mocks.CreateLoggerMock().Object; var commandProcessor = new Mock().Object; ITelegramMessageSender? messageSender = null; // Act & Assert var act = () => new TelegramMessageHandler(logger, commandProcessor, messageSender!); act.Should().NotThrow(); } [Fact] public async Task HandleUpdateAsync_ShouldReturnEarly_WhenUpdateMessageIsNull() { // Arrange var update = new Update { Message = null }; // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); } [Fact] public async Task HandleUpdateAsync_ShouldReturnEarly_WhenMessageTextIsNull() { // Arrange var message = CreateMessage(null!, 12345, "testuser", "Test Chat", 1); var update = new Update { Message = message }; // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); } [Fact] public async Task HandleUpdateAsync_ShouldProcessMessage_WhenValidMessage() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var messageId = 1; var message = CreateMessage(messageText, chatId, username, chatTitle, messageId); var update = new Update { Message = message }; var expectedResponse = "Hello! How can I help you?"; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( messageText, chatId, username, chatType, chatTitle, null, It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( messageText, chatId, username, chatType, chatTitle, null, It.IsAny() ), Times.Once ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( _botClientMock.Object, chatId, expectedResponse, It.IsAny(), It.IsAny(), 3 ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldNotSendMessage_WhenResponseIsEmpty() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var message = CreateMessage(messageText, chatId, username, chatTitle, 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync(string.Empty); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Once ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); } [Fact] public async Task HandleUpdateAsync_ShouldNotSendMessage_WhenResponseIsNull() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var message = CreateMessage(messageText, chatId, username, chatTitle, 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync((string)null!); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Once ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ), Times.Never ); } [Fact] public async Task HandleUpdateAsync_ShouldUseUsername_WhenFromHasUsername() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var message = CreateMessage(messageText, chatId, username, chatTitle, 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync("Response"); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( messageText, chatId, username, chatType, chatTitle, null, It.IsAny() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldUseFirstName_WhenFromHasNoUsername() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var firstName = "TestUser"; var chatType = "Private"; var chatTitle = "Test Chat"; var from = new User { Id = 67890, Username = null, FirstName = firstName, }; var message = CreateMessage(messageText, chatId, firstName, chatTitle, 1, from); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync("Response"); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( messageText, chatId, firstName, chatType, chatTitle, null, It.IsAny() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldUseUnknown_WhenFromIsNull() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var chatType = "Private"; var chatTitle = "Test Chat"; var message = CreateMessage(messageText, chatId, "Unknown", chatTitle, 1, null); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync("Response"); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( messageText, chatId, "Unknown", chatType, chatTitle, null, It.IsAny() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldHandleReplyMessage() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var messageId = 1; var replyToMessageId = 2; var replyToUserId = 67890L; var replyToUsername = "originaluser"; var replyToMessage = CreateMessage( "Original message", chatId, replyToUsername, chatTitle, replyToMessageId ); var message = CreateMessage( messageText, chatId, username, chatTitle, messageId, null, replyToMessage ); var update = new Update { Message = message }; var expectedResponse = "Response to reply"; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( messageText, chatId, username, chatType, chatTitle, It.Is(r => r.UserId == replyToUserId && r.Username == replyToUsername ), It.IsAny() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldPassCancellationToken() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var cancellationToken = new CancellationToken(); var message = CreateMessage(messageText, chatId, username, chatTitle, 1); var update = new Update { Message = message }; var expectedResponse = "Response"; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, cancellationToken); // Assert _commandProcessorMock.Verify( x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), cancellationToken ), Times.Once ); _messageSenderMock.Verify( x => x.SendMessageWithRetry( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), cancellationToken, It.IsAny() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldLogError_WhenExceptionOccurs() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var chatType = "Private"; var chatTitle = "Test Chat"; var message = CreateMessage(messageText, chatId, username, chatTitle, 1); var update = new Update { Message = message }; var exception = new Exception("Test exception"); _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ThrowsAsync(exception); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), It.Is( (v, t) => v.ToString()!.Contains("Error handling update from chat") ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldLogInformation_WhenMessageReceived() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var message = CreateMessage(messageText, chatId, username, "Test Chat", 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync("Response"); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( $"Message from @{username} in chat {chatId}: \"{messageText}\"" ) ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldLogInformation_WhenResponseSent() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var response = "Hello! How can I help you?"; var message = CreateMessage(messageText, chatId, username, "Test Chat", 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync(response); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( $"Response sent to @{username} in chat {chatId}: \"{response}\"" ) ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task HandleUpdateAsync_ShouldLogInformation_WhenNoResponseSent() { // Arrange var messageText = "Hello bot"; var chatId = 12345L; var username = "testuser"; var message = CreateMessage(messageText, chatId, username, "Test Chat", 1); var update = new Update { Message = message }; _commandProcessorMock .Setup(x => x.ProcessMessageAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny() ) ) .ReturnsAsync(string.Empty); // Act await _handler.HandleUpdateAsync(_botClientMock.Object, update, CancellationToken.None); // Assert _loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( $"No response sent to @{username} in chat {chatId} (AI chose to ignore message)" ) ), It.IsAny(), It.IsAny>() ), Times.Once ); } }