using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.AI; using Moq; using NexusReader.Application.DTOs.AI; using NexusReader.Application.DTOs.User; using NexusReader.Application.Queries.Library; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using Pgvector; using Xunit; namespace NexusReader.Application.Tests.Queries; public class QueryTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; private readonly Mock> _dbContextFactoryMock; private readonly Mock>> _embeddingGeneratorMock; public QueryTests() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); _contextOptions = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; // Seed initial database schema using var context = new AppDbContext(_contextOptions); context.Database.EnsureCreated(); _dbContextFactoryMock = new Mock>(); _dbContextFactoryMock.Setup(f => f.CreateDbContextAsync(It.IsAny())) .ReturnsAsync(() => new AppDbContext(_contextOptions)); _dbContextFactoryMock.Setup(f => f.CreateDbContext()) .Returns(() => new AppDbContext(_contextOptions)); _embeddingGeneratorMock = new Mock>>(); } [Fact] public async Task GetMyEbooksQuery_WithPopulatedDescription_ReturnsCorrectDescription() { // Arrange using (var context = new AppDbContext(_contextOptions)) { var user = new NexusUser { Id = "user-123", UserName = "testuser", Email = "test@example.com", TenantId = "tenant-123", SubscriptionPlanId = 1 }; context.Users.Add(user); var author = new Author { Id = 1, Name = "Adam Mickiewicz" }; context.Authors.Add(author); var ebook = new Ebook { Id = Guid.NewGuid(), UserId = "user-123", Title = "Pan Tadeusz", AuthorId = author.Id, Description = "A Polish epic poem written by Adam Mickiewicz.", CoverUrl = "cover.png", Progress = 42.5, LastChapter = "Księga I", LastChapterIndex = 1, AddedDate = DateTime.UtcNow, LastReadDate = DateTime.UtcNow, FilePath = "dummy.epub" }; context.Ebooks.Add(ebook); await context.SaveChangesAsync(); } var handler = new GetMyEbooksQueryHandler(_dbContextFactoryMock.Object); var query = new GetMyEbooksQuery("user-123"); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().HaveCount(1); result.Value.First().Title.Should().Be("Pan Tadeusz"); result.Value.First().Description.Should().Be("A Polish epic poem written by Adam Mickiewicz."); result.Value.First().Progress.Should().Be(42.5); } [Fact] public async Task SearchLibrarySemanticallyQuery_WithEmptyQueryText_ReturnsFailure() { // Arrange var handler = new SearchLibrarySemanticallyQueryHandler(_dbContextFactoryMock.Object, _embeddingGeneratorMock.Object); var query = new SearchLibrarySemanticallyQuery("", "tenant-123"); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeFalse(); result.Errors.First().Message.Should().Be("Query text cannot be empty."); } [Fact] public async Task SearchLibrarySemanticallyQuery_WithNoResults_TriggersFallback1536Embedding() { // Arrange // Mock 768-dim primary embedding generator response var embedding768 = new Embedding(new float[768]); var mockResponse768 = new GeneratedEmbeddings>(new List> { embedding768 }); _embeddingGeneratorMock.Setup(g => g.GenerateAsync( It.Is>(s => s.Contains("test")), It.Is(o => o.Dimensions == 768), It.IsAny())) .ReturnsAsync(mockResponse768); // Mock 1536-dim fallback embedding generator response var embedding1536 = new Embedding(new float[1536]); var mockResponse1536 = new GeneratedEmbeddings>(new List> { embedding1536 }); _embeddingGeneratorMock.Setup(g => g.GenerateAsync( It.Is>(s => s.Contains("test")), It.Is(o => o.Dimensions == 1536), It.IsAny())) .ReturnsAsync(mockResponse1536); // Seed one legacy cache entry using (var context = new AppDbContext(_contextOptions)) { var cacheEntry = new SemanticKnowledgeCache { TenantId = "tenant-123", ContentHash = "hash-123", OriginalText = "Fallback Cache Content Snippet", Vector = new Vector(new float[1536]), PromptVersion = "1", CreatedAt = DateTime.UtcNow }; context.SemanticKnowledgeCache.Add(cacheEntry); await context.SaveChangesAsync(); } var handler = new SearchLibrarySemanticallyQueryHandler(_dbContextFactoryMock.Object, _embeddingGeneratorMock.Object); var query = new SearchLibrarySemanticallyQuery("test", "tenant-123"); // Act var result = await handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().HaveCount(1); result.Value.First().Snippet.Should().Be("Fallback Cache Content Snippet"); result.Value.First().ContentHash.Should().Be("hash-123"); } public void Dispose() { _connection.Close(); _connection.Dispose(); } }