using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NexusReader.Application.Abstractions.Persistence; using NexusReader.Application.Queries.Recommendations; using NexusReader.Infrastructure.Queries; using Xunit; namespace NexusReader.Application.Tests.Queries; public class GetContextualRecommendationsQueryTests { private readonly Mock _readingStateStoreMock; private readonly Mock _libraryStoreMock; private readonly Mock _vectorSearchStoreMock; private readonly Mock> _loggerMock; private readonly GetContextualRecommendationsQueryHandler _handler; public GetContextualRecommendationsQueryTests() { _readingStateStoreMock = new Mock(); _libraryStoreMock = new Mock(); _vectorSearchStoreMock = new Mock(); _loggerMock = new Mock>(); _handler = new GetContextualRecommendationsQueryHandler( _readingStateStoreMock.Object, _libraryStoreMock.Object, _vectorSearchStoreMock.Object, _loggerMock.Object ); } [Fact] public async Task Handle_WithNoActiveReadingState_ReturnsEmptyRecommendations() { // Arrange var userId = "user-123"; _readingStateStoreMock.Setup(s => s.GetActiveReadingStateAsync(userId, It.IsAny())) .ReturnsAsync((null, null, null)); var query = new GetContextualRecommendationsQuery(userId); // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Recommendations.Should().BeEmpty(); } [Fact] public async Task Handle_WithActiveReadingState_PerformsSimilaritySearchAndReturnsRecommendations() { // Arrange var userId = "user-123"; var activeEbookId = Guid.NewGuid(); var activeChapterId = "chapter-1"; var tenantId = "tenant-abc"; var chapterContent = "Active chapter content description"; _readingStateStoreMock.Setup(s => s.GetActiveReadingStateAsync(userId, It.IsAny())) .ReturnsAsync((activeEbookId, activeChapterId, tenantId)); _readingStateStoreMock.Setup(s => s.GetChapterContentAsync(activeChapterId, It.IsAny())) .ReturnsAsync(chapterContent); // Mock vector search results using clean VectorChunk list var targetEbookId1 = Guid.NewGuid(); var targetEbookId2 = Guid.NewGuid(); var mockChunks = new List { new VectorChunk( Content: "Result pattern details", EbookId: targetEbookId1.ToString(), Score: 0.88, MetadataJson: "", BookTitle: "Clean Architecture deep dive", ChapterTitle: "Chapter 3: Result Pattern" ), new VectorChunk( Content: "Performance optimizations", EbookId: targetEbookId2.ToString(), Score: 0.72, MetadataJson: "", BookTitle: "Advanced C# 14", ChapterTitle: "Chapter 5: Span and Performance" ) }; _vectorSearchStoreMock.Setup(v => v.SearchGlobalExcludeAsync( chapterContent, tenantId, activeEbookId, 2, It.IsAny())) .ReturnsAsync(mockChunks); // User owns the second book but not the first one _libraryStoreMock.Setup(l => l.GetOwnedBookIdsAsync(userId, It.IsAny())) .ReturnsAsync(new List { targetEbookId2 }); var query = new GetContextualRecommendationsQuery(userId); // Act var result = await _handler.Handle(query, CancellationToken.None); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Recommendations.Should().HaveCount(2); var firstRec = result.Value.Recommendations.First(); firstRec.BookTitle.Should().Be("Clean Architecture deep dive"); firstRec.ChapterTitle.Should().Be("Chapter 3: Result Pattern"); firstRec.MatchPercentage.Should().Be(88); firstRec.IsPremiumUpsell.Should().BeTrue(); // User does not own book 1 firstRec.TargetBookId.Should().Be(targetEbookId1); var secondRec = result.Value.Recommendations.Last(); secondRec.BookTitle.Should().Be("Advanced C# 14"); secondRec.ChapterTitle.Should().Be("Chapter 5: Span and Performance"); secondRec.MatchPercentage.Should().Be(72); secondRec.IsPremiumUpsell.Should().BeFalse(); // User owns book 2 secondRec.TargetBookId.Should().Be(targetEbookId2); } }