using ChatBot.Models.Configuration; using ChatBot.Services; using ChatBot.Services.Interfaces; using ChatBot.Tests.TestUtilities; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; namespace ChatBot.Tests.Services; public class ChatServiceTests : UnitTestBase { private readonly Mock> _loggerMock; private readonly Mock _aiServiceMock; private readonly Mock _sessionStorageMock; private readonly Mock _compressionServiceMock; private readonly AISettings _aiSettings; private readonly ChatService _chatService; public ChatServiceTests() { _loggerMock = TestDataBuilder.Mocks.CreateLoggerMock(); _aiServiceMock = TestDataBuilder.Mocks.CreateAIServiceMock(); _sessionStorageMock = TestDataBuilder.Mocks.CreateSessionStorageMock(); _compressionServiceMock = TestDataBuilder.Mocks.CreateCompressionServiceMock(); _aiSettings = TestDataBuilder.Configurations.CreateAISettings(); var optionsMock = TestDataBuilder.Mocks.CreateOptionsMock(_aiSettings); _chatService = new ChatService( _loggerMock.Object, _aiServiceMock.Object, _sessionStorageMock.Object, optionsMock.Object, _compressionServiceMock.Object ); } [Fact] public void GetOrCreateSession_ShouldCreateNewSession_WhenSessionDoesNotExist() { // Arrange var chatId = 12345L; var chatType = "private"; var chatTitle = "Test Chat"; // Act var session = _chatService.GetOrCreateSession(chatId, chatType, chatTitle); // Assert session.Should().NotBeNull(); session.ChatId.Should().Be(chatId); session.ChatType.Should().Be(chatType); session.ChatTitle.Should().Be(chatTitle); _sessionStorageMock.Verify(x => x.GetOrCreate(chatId, chatType, chatTitle), Times.Once); } [Fact] public void GetOrCreateSession_ShouldSetCompressionService_WhenCompressionIsEnabled() { // Arrange var chatId = 12345L; _aiSettings.EnableHistoryCompression = true; // Act var session = _chatService.GetOrCreateSession(chatId); // Assert session.Should().NotBeNull(); _sessionStorageMock.Verify(x => x.GetOrCreate(chatId, "private", ""), Times.Once); } [Fact] public async Task ProcessMessageAsync_ShouldProcessMessageSuccessfully_WhenValidInput() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.AtLeastOnce ); _aiServiceMock.Verify( x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ), Times.Once ); } [Fact] public async Task ProcessMessageAsync_ShouldReturnEmptyString_WhenAIResponseIsEmptyMarker() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var emptyResponse = "{empty}"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(emptyResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().BeEmpty(); } [Fact] public async Task ProcessMessageAsync_ShouldReturnErrorMessage_WhenAIResponseIsNull() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync((string)null!); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be("Извините, произошла ошибка при генерации ответа."); } [Fact] public async Task ProcessMessageAsync_ShouldHandleException_WhenErrorOccurs() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ThrowsAsync(new Exception("Test exception")); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения."); // Verify that error was logged // In a real test, we would verify the logger calls } [Fact] public async Task UpdateSessionParametersAsync_ShouldUpdateSession_WhenSessionExists() { // Arrange var chatId = 12345L; var newModel = "llama3.2"; var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId); _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); // Act await _chatService.UpdateSessionParametersAsync(chatId, newModel); // Assert session.Model.Should().Be(newModel); _sessionStorageMock.Verify(x => x.SaveSessionAsync(session), Times.Once); } [Fact] public async Task UpdateSessionParametersAsync_ShouldNotUpdate_WhenSessionDoesNotExist() { // Arrange var chatId = 12345L; var newModel = "llama3.2"; _sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null); // Act await _chatService.UpdateSessionParametersAsync(chatId, newModel); // Assert _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.Never ); } [Fact] public async Task ClearHistoryAsync_ShouldClearHistory_WhenSessionExists() { // Arrange var chatId = 12345L; var session = TestDataBuilder.ChatSessions.CreateSessionWithMessages(chatId, 5); _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); // Act await _chatService.ClearHistoryAsync(chatId); // Assert session.GetMessageCount().Should().Be(0); _sessionStorageMock.Verify(x => x.SaveSessionAsync(session), Times.Once); } [Fact] public void GetSession_ShouldReturnSession_WhenSessionExists() { // Arrange var chatId = 12345L; var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId); _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); // Act var result = _chatService.GetSession(chatId); // Assert result.Should().Be(session); } [Fact] public void GetSession_ShouldReturnNull_WhenSessionDoesNotExist() { // Arrange var chatId = 12345L; _sessionStorageMock.Setup(x => x.Get(chatId)).Returns((ChatBot.Models.ChatSession?)null); // Act var result = _chatService.GetSession(chatId); // Assert result.Should().BeNull(); } [Fact] public void RemoveSession_ShouldReturnTrue_WhenSessionExists() { // Arrange var chatId = 12345L; _sessionStorageMock.Setup(x => x.Remove(chatId)).Returns(true); // Act var result = _chatService.RemoveSession(chatId); // Assert result.Should().BeTrue(); _sessionStorageMock.Verify(x => x.Remove(chatId), Times.Once); } [Fact] public void GetActiveSessionsCount_ShouldReturnCorrectCount() { // Arrange var expectedCount = 5; _sessionStorageMock.Setup(x => x.GetActiveSessionsCount()).Returns(expectedCount); // Act var result = _chatService.GetActiveSessionsCount(); // Assert result.Should().Be(expectedCount); } [Fact] public void CleanupOldSessions_ShouldReturnCleanedCount() { // Arrange var hoursOld = 24; var expectedCleaned = 3; _sessionStorageMock.Setup(x => x.CleanupOldSessions(hoursOld)).Returns(expectedCleaned); // Act var result = _chatService.CleanupOldSessions(hoursOld); // Assert result.Should().Be(expectedCleaned); _sessionStorageMock.Verify(x => x.CleanupOldSessions(hoursOld), Times.Once); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null!)] public async Task ProcessMessageAsync_ShouldHandleEmptyOrNullMessage(string? message) { // Arrange var chatId = 12345L; var username = "testuser"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync( chatId, username, message ?? string.Empty ); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.AtLeastOnce ); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null!)] public async Task ProcessMessageAsync_ShouldHandleEmptyOrNullUsername(string? username) { // Arrange var chatId = 12345L; var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync( chatId, username ?? string.Empty, message ); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.AtLeastOnce ); } [Fact] public async Task ProcessMessageAsync_ShouldHandleSessionStorageException() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; _sessionStorageMock .Setup(x => x.GetOrCreate(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new Exception("Database connection failed")); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения."); _loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), It.Is( (v, t) => v.ToString()!.Contains("Error processing message") ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task ProcessMessageAsync_ShouldHandleAIServiceException() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ThrowsAsync(new HttpRequestException("AI service unavailable")); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения."); _loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), It.Is( (v, t) => v.ToString()!.Contains("Error processing message") ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task ProcessMessageAsync_ShouldHandleCancellationToken() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var cts = new CancellationTokenSource(); cts.Cancel(); // Cancel immediately // Setup AI service to throw OperationCanceledException when cancellation is requested _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ThrowsAsync(new OperationCanceledException("Operation was canceled")); // Act var result = await _chatService.ProcessMessageAsync( chatId, username, message, cancellationToken: cts.Token ); // Assert result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения."); } [Fact] public async Task ProcessMessageAsync_ShouldLogCorrectInformation() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync( chatId, username, message, "group", "Test Group" ); // Assert result.Should().Be(expectedResponse); _loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( "Processing message from user testuser in chat 12345 (group): Hello, bot!" ) ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task ProcessMessageAsync_ShouldLogDebugForResponseLength() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be(expectedResponse); _loggerMock.Verify( x => x.Log( LogLevel.Debug, It.IsAny(), It.Is( (v, t) => v.ToString()!.Contains("AI response generated for chat 12345 (length:") ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task ProcessMessageAsync_ShouldLogEmptyResponseMarker() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; var emptyResponse = "{empty}"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(emptyResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().BeEmpty(); _loggerMock.Verify( x => x.Log( LogLevel.Information, It.IsAny(), It.Is( (v, t) => v.ToString()! .Contains( "AI returned empty response marker for chat 12345, ignoring message" ) ), It.IsAny(), It.IsAny>() ), Times.Once ); } [Fact] public async Task UpdateSessionParametersAsync_ShouldHandleSessionStorageException() { // Arrange var chatId = 12345L; var newModel = "llama3.2"; var session = TestDataBuilder.ChatSessions.CreateBasicSession(chatId); _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); _sessionStorageMock .Setup(x => x.SaveSessionAsync(It.IsAny())) .ThrowsAsync(new Exception("Database save failed")); // Act & Assert var act = async () => await _chatService.UpdateSessionParametersAsync(chatId, newModel); await act.Should().ThrowAsync().WithMessage("Database save failed"); } [Fact] public async Task ClearHistoryAsync_ShouldHandleSessionStorageException() { // Arrange var chatId = 12345L; var session = TestDataBuilder.ChatSessions.CreateSessionWithMessages(chatId, 5); _sessionStorageMock.Setup(x => x.Get(chatId)).Returns(session); _sessionStorageMock .Setup(x => x.SaveSessionAsync(It.IsAny())) .ThrowsAsync(new Exception("Database save failed")); // Act & Assert var act = async () => await _chatService.ClearHistoryAsync(chatId); await act.Should().ThrowAsync().WithMessage("Database save failed"); } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(int.MinValue)] public void CleanupOldSessions_ShouldHandleInvalidHoursOld(int hoursOld) { // Arrange var expectedCleaned = 0; _sessionStorageMock.Setup(x => x.CleanupOldSessions(hoursOld)).Returns(expectedCleaned); // Act var result = _chatService.CleanupOldSessions(hoursOld); // Assert result.Should().Be(expectedCleaned); _sessionStorageMock.Verify(x => x.CleanupOldSessions(hoursOld), Times.Once); } [Theory] [InlineData(long.MaxValue)] [InlineData(long.MinValue)] [InlineData(0)] [InlineData(-1)] public async Task ProcessMessageAsync_ShouldHandleExtremeChatIds(long chatId) { // Arrange var username = "testuser"; var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify(x => x.GetOrCreate(chatId, "private", ""), Times.Once); } [Fact] public async Task ProcessMessageAsync_ShouldHandleVeryLongMessage() { // Arrange var chatId = 12345L; var username = "testuser"; var veryLongMessage = new string('A', 10000); // Very long message var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, veryLongMessage); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.AtLeastOnce ); } [Fact] public async Task ProcessMessageAsync_ShouldHandleVeryLongUsername() { // Arrange var chatId = 12345L; var veryLongUsername = new string('U', 1000); // Very long username var message = "Hello, bot!"; var expectedResponse = "Hello! How can I help you?"; _aiServiceMock .Setup(x => x.GenerateChatCompletionWithCompressionAsync( It.IsAny>(), It.IsAny() ) ) .ReturnsAsync(expectedResponse); // Act var result = await _chatService.ProcessMessageAsync(chatId, veryLongUsername, message); // Assert result.Should().Be(expectedResponse); _sessionStorageMock.Verify( x => x.SaveSessionAsync(It.IsAny()), Times.AtLeastOnce ); } [Fact] public async Task ProcessMessageAsync_ShouldHandleCompressionServiceException() { // Arrange var chatId = 12345L; var username = "testuser"; var message = "Hello, bot!"; _aiSettings.EnableHistoryCompression = true; _compressionServiceMock .Setup(x => x.ShouldCompress(It.IsAny(), It.IsAny())) .Throws(new Exception("Compression service failed")); // Act var result = await _chatService.ProcessMessageAsync(chatId, username, message); // Assert result.Should().Be("Извините, произошла ошибка при обработке вашего сообщения."); _loggerMock.Verify( x => x.Log( LogLevel.Error, It.IsAny(), It.Is( (v, t) => v.ToString()!.Contains("Error processing message") ), It.IsAny(), It.IsAny>() ), Times.Once ); } }