using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GermanApp.Application.Models; using GermanApp.Application.Services; using GermanApp.Domain.Interfaces; using GermanApp.Infrastructure.Configuration; using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace GermanApp.Tests.Unit.Application.Services; [TestClass] public class MistralServiceTests { private Mock _mockConnector; private Mock> _mockConfigOptions; private MistralConfig _config; private MistralService _service; [TestInitialize] public void Setup() { _mockConnector = new Mock(); _mockConfigOptions = new Mock>(); _config = new MistralConfig { ApiKey = "test-api-key", BaseUrl = "https://api.mistral.ai/v1/", DefaultModel = "mistral-medium", TimeoutSeconds = 30, MaxRetries = 3 }; _mockConfigOptions.Setup(c => c.Value).Returns(_config); _service = new MistralService(_mockConnector.Object, _mockConfigOptions.Object); } [TestMethod] public async Task GenerateTextAsync_WithValidPrompt_ReturnsText() { // Arrange var prompt = "Tell me a joke"; var expectedText = "Why did the chicken cross the road?"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Model = "mistral-medium", Choices = new List { new MistralChoice { Text = expectedText, Index = 0 } }, Usage = new MistralUsage() }; _mockConnector.Setup(c => c.CompleteAsync( It.Is(r => r.Prompt == prompt && r.Model == "mistral-medium"), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateTextAsync(prompt, null, 0.7f, null, cancellationToken); // Assert Assert.AreEqual(expectedText, result); _mockConnector.Verify(c => c.CompleteAsync( It.IsAny(), cancellationToken), Times.Once); } [TestMethod] public async Task GenerateTextAsync_WithCustomModel_UsesCustomModel() { // Arrange var prompt = "Tell me a story"; var customModel = "mistral-small"; var expectedText = "Once upon a time..."; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedText } } }; _mockConnector.Setup(c => c.CompleteAsync( It.Is(r => r.Model == customModel), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateTextAsync(prompt, customModel, 0.7f, null, cancellationToken); // Assert Assert.AreEqual(expectedText, result); } [TestMethod] public async Task GenerateTextAsync_WithCustomParameters_UsesParameters() { // Arrange var prompt = "Test"; var temperature = 0.9f; var maxTokens = 100; var expectedText = "Test response"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedText } } }; _mockConnector.Setup(c => c.CompleteAsync( It.Is(r => r.Temperature == temperature && r.MaxTokens == maxTokens), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateTextAsync( prompt, null, temperature, maxTokens, cancellationToken); // Assert Assert.AreEqual(expectedText, result); } [TestMethod] public async Task GenerateTextAsync_WithNoChoices_ThrowsInvalidOperationException() { // Arrange var prompt = "Test"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List() }; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act & Assert try { await _service.GenerateTextAsync(prompt, null, 0.7f, null, cancellationToken); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException) { // Expected } } [TestMethod] public async Task GenerateTextAsync_WithNullTextInChoice_ReturnsEmptyString() { // Arrange var prompt = "Test"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = null } } }; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateTextAsync(prompt, null, 0.7f, null, cancellationToken); // Assert Assert.AreEqual(string.Empty, result); } [TestMethod] public async Task GenerateTextAsync_WhenConnectorThrows_ThrowsException() { // Arrange var prompt = "Test"; var cancellationToken = CancellationToken.None; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ThrowsAsync(new Exception("API Error")); // Act & Assert try { await _service.GenerateTextAsync(prompt, null, 0.7f, null, cancellationToken); Assert.Fail("Expected Exception was not thrown"); } catch (Exception) { // Expected } } [TestMethod] public async Task GenerateChatAsync_WithValidMessages_ReturnsResponse() { // Arrange var messages = new List<(string role, string content)> { ("system", "You are a helpful assistant"), ("user", "Hello") }; var expectedResponse = "Hi there! How can I help you?"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Model = "mistral-medium", Choices = new List { new MistralChoice { Index = 0, Message = new MistralChatMessage { Role = "assistant", Content = expectedResponse } } }, Usage = new MistralUsage() }; _mockConnector.Setup(c => c.ChatAsync( It.Is(r => r.Messages.Count == 2), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateChatAsync(messages, null, 0.7f, null, cancellationToken); // Assert Assert.AreEqual(expectedResponse, result); } [TestMethod] public async Task GenerateChatAsync_WithNoChoices_ThrowsInvalidOperationException() { // Arrange var messages = new List<(string role, string content)> { ("user", "Hello") }; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List() }; _mockConnector.Setup(c => c.ChatAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act & Assert try { await _service.GenerateChatAsync(messages, null, 0.7f, null, cancellationToken); Assert.Fail("Expected InvalidOperationException was not thrown"); } catch (InvalidOperationException) { // Expected } } [TestMethod] public async Task GenerateChatAsync_WithNullMessage_ReturnsEmpty() { // Arrange var messages = new List<(string role, string content)> { ("user", "Hello") }; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Message = null } } }; _mockConnector.Setup(c => c.ChatAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateChatAsync(messages, null, 0.7f, null, cancellationToken); // Assert Assert.AreEqual(string.Empty, result); } [TestMethod] public async Task GenerateStoryAsync_WithValidParameters_ReturnsStory() { // Arrange var level = "A1"; var topic = "Travel"; var vocabularyWords = new List { "Bahn", "Reise", "Stadt" }; var length = 200; var expectedStory = "Es war einmal eine Reise mit der Bahn..."; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedStory } } }; _mockConnector.Setup(c => c.CompleteAsync( It.Is(r => r.Model == "mistral-medium" && r.Temperature == 0.8f && r.MaxTokens == 1000), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateStoryAsync( level, topic, vocabularyWords, length, cancellationToken); // Assert Assert.AreEqual(expectedStory, result); _mockConnector.Verify(c => c.CompleteAsync( It.IsAny(), cancellationToken), Times.Once); } [TestMethod] public async Task GenerateStoryAsync_WithEmptyVocabulary_ReturnsStory() { // Arrange var level = "A1"; var topic = "Travel"; var vocabularyWords = new List(); var length = 200; var expectedStory = "A story without specific vocabulary..."; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedStory } } }; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateStoryAsync( level, topic, vocabularyWords, length, cancellationToken); // Assert Assert.AreEqual(expectedStory, result); } [TestMethod] public async Task GenerateWritingFeedbackAsync_WithValidParameters_ReturnsFeedback() { // Arrange var userText = "Ich heisse Anna und wohne in Berlin."; var level = "A1"; var expectedFeedback = "Great job! Your sentence structure is correct."; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Message = new MistralChatMessage { Content = expectedFeedback } } } }; _mockConnector.Setup(c => c.ChatAsync( It.Is(r => r.Model == "mistral-medium" && r.Temperature == 0.3f && r.MaxTokens == 800 && r.Messages.Count == 3), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateWritingFeedbackAsync( userText, level, null, cancellationToken); // Assert Assert.AreEqual(expectedFeedback, result); } [TestMethod] public async Task GenerateWritingFeedbackAsync_WithCustomPrompt_ReturnsFeedback() { // Arrange var userText = "Ich heisse Anna."; var level = "A1"; var customPrompt = "Focus on grammar mistakes"; var expectedFeedback = "Grammar: You should use 'heiße' instead of 'heisse'"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Message = new MistralChatMessage { Content = expectedFeedback } } } }; _mockConnector.Setup(c => c.ChatAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.GenerateWritingFeedbackAsync( userText, level, customPrompt, cancellationToken); // Assert Assert.AreEqual(expectedFeedback, result); } [TestMethod] public async Task TestConnectionAsync_WithSuccessfulResponse_ReturnsTrue() { // Arrange var expectedText = "test successful"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedText } } }; _mockConnector.Setup(c => c.CompleteAsync( It.Is(r => r.MaxTokens == 10), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.TestConnectionAsync(cancellationToken); // Assert Assert.IsTrue(result); } [TestMethod] public async Task TestConnectionAsync_WithDifferentCaseResponse_ReturnsTrue() { // Arrange var expectedText = "TEST SUCCESSFUL"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedText } } }; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.TestConnectionAsync(cancellationToken); // Assert Assert.IsTrue(result); } [TestMethod] public async Task TestConnectionAsync_WithWrongResponse_ReturnsFalse() { // Arrange var expectedText = "Wrong response"; var cancellationToken = CancellationToken.None; var response = new MistralResponse { Choices = new List { new MistralChoice { Text = expectedText } } }; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ReturnsAsync(response); // Act var result = await _service.TestConnectionAsync(cancellationToken); // Assert Assert.IsFalse(result); } [TestMethod] public async Task TestConnectionAsync_WhenConnectorThrows_ReturnsFalse() { // Arrange var cancellationToken = CancellationToken.None; _mockConnector.Setup(c => c.CompleteAsync( It.IsAny(), cancellationToken)) .ThrowsAsync(new Exception("API Error")); // Act var result = await _service.TestConnectionAsync(cancellationToken); // Assert Assert.IsFalse(result); } [TestMethod] public void BuildStoryPrompt_WithValidParameters_ReturnsPrompt() { // This is a private method, but we can test it indirectly through GenerateStoryAsync // The test above already verifies the prompt building works } [TestMethod] public void BuildFeedbackSystemPrompt_WithValidParameters_ReturnsPrompt() { // This is a private method, but we can test it indirectly through GenerateWritingFeedbackAsync // The test above already verifies the prompt building works } }