using Microsoft.EntityFrameworkCore; using RolemasterDb.App.Data; using RolemasterDb.App.Domain; using RolemasterDb.App.Features; namespace RolemasterDb.ImportTool.Tests; public sealed class LookupServiceCurationIntegrationTests { [Fact] public async Task Lookup_service_surfaces_and_persists_curated_state_and_source_image_metadata() { var databasePath = CreateEmptyDatabasePath(); var repositoryRoot = CreateTemporaryRepositoryRoot(); var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"))); await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2); WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png"); var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator); var resultId = await GetResultIdAsync(databasePath); var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(initialResponse); Assert.False(initialResponse!.IsCurated); Assert.Equal(2, initialResponse.SourcePageNumber); Assert.Equal($"/api/tables/critical/slash/cells/{resultId}/source-image", initialResponse.SourceImageUrl); var updateRequest = new CriticalCellUpdateRequest( initialResponse.RawCellText, initialResponse.QuickParseInput, initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, true, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches); var updatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, updateRequest); Assert.NotNull(updatedResponse); Assert.True(updatedResponse!.IsCurated); var tableDetail = await lookupService.GetCriticalTableAsync("slash"); Assert.NotNull(tableDetail); Assert.Contains(tableDetail!.Cells, item => item.ResultId == resultId && item.IsCurated); var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(reopenedResponse); Assert.True(reopenedResponse!.IsCurated); Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl); } [Fact] public async Task Lookup_service_clears_curated_state_when_any_content_is_edited() { var databasePath = CreateEmptyDatabasePath(); var repositoryRoot = CreateTemporaryRepositoryRoot(); var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"))); await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2); WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png"); var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator); var resultId = await GetResultIdAsync(databasePath); var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(initialResponse); var markCuratedRequest = new CriticalCellUpdateRequest( initialResponse!.RawCellText, initialResponse.QuickParseInput, initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, true, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches); var curatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, markCuratedRequest); Assert.NotNull(curatedResponse); Assert.True(curatedResponse!.IsCurated); var editedRequest = markCuratedRequest with { DescriptionText = "Edited description after curation." }; var editedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, editedRequest); Assert.NotNull(editedResponse); Assert.False(editedResponse!.IsCurated); var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(reopenedResponse); Assert.False(reopenedResponse!.IsCurated); Assert.Equal("Edited description after curation.", reopenedResponse.DescriptionText); } [Fact] public async Task Lookup_service_allows_marking_uncurated_reparsed_content_as_curated() { var databasePath = CreateEmptyDatabasePath(); var repositoryRoot = CreateTemporaryRepositoryRoot(); var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"))); await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2); WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png"); var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator); var resultId = await GetResultIdAsync(databasePath); var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(initialResponse); Assert.False(initialResponse!.IsCurated); var reparsedResponse = await lookupService.ReparseCriticalCellAsync( "slash", resultId, new CriticalCellUpdateRequest( initialResponse.RawCellText, "Edited quick parse input.", initialResponse.DescriptionText, initialResponse.RawAffixText, initialResponse.ParseStatus, initialResponse.ParsedJson, initialResponse.IsCurated, initialResponse.IsDescriptionOverridden, initialResponse.IsRawAffixTextOverridden, initialResponse.AreEffectsOverridden, initialResponse.AreBranchesOverridden, initialResponse.Effects, initialResponse.Branches)); Assert.NotNull(reparsedResponse); Assert.False(reparsedResponse!.IsCurated); var curatedResponse = await lookupService.UpdateCriticalCellAsync( "slash", resultId, new CriticalCellUpdateRequest( reparsedResponse.RawCellText, reparsedResponse.QuickParseInput, reparsedResponse.DescriptionText, reparsedResponse.RawAffixText, reparsedResponse.ParseStatus, reparsedResponse.ParsedJson, true, reparsedResponse.IsDescriptionOverridden, reparsedResponse.IsRawAffixTextOverridden, reparsedResponse.AreEffectsOverridden, reparsedResponse.AreBranchesOverridden, reparsedResponse.Effects, reparsedResponse.Branches)); Assert.NotNull(curatedResponse); Assert.True(curatedResponse!.IsCurated); Assert.Equal("Edited quick parse input.", curatedResponse.QuickParseInput); var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId); Assert.NotNull(reopenedResponse); Assert.True(reopenedResponse!.IsCurated); Assert.Equal("Edited quick parse input.", reopenedResponse.QuickParseInput); } [Fact] public async Task Lookup_service_resolves_source_image_paths_only_when_artifacts_exist() { var databasePath = CreateEmptyDatabasePath(); var repositoryRoot = CreateTemporaryRepositoryRoot(); var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"))); await SeedCriticalResultAsync(databasePath, "slash/cells/missing.png", 1); var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator); var resultId = await GetResultIdAsync(databasePath); var missingPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId); Assert.Null(missingPath); WriteSourceImage(repositoryRoot, "slash/cells/missing.png"); var resolvedPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId); Assert.NotNull(resolvedPath); Assert.True(File.Exists(resolvedPath)); } private static async Task SeedCriticalResultAsync(string databasePath, string sourceImagePath, int sourcePageNumber) { await using var dbContext = CreateDbContext(databasePath); await dbContext.Database.EnsureCreatedAsync(); await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext); var table = new CriticalTable { Slug = "slash", DisplayName = "Slash Critical Strike Table", Family = "standard", SourceDocument = "Slash.pdf" }; var column = new CriticalColumn { CriticalTable = table, ColumnKey = "B", Label = "B", Role = "severity", SortOrder = 2 }; var rollBand = new CriticalRollBand { CriticalTable = table, Label = "36-40", MinRoll = 36, MaxRoll = 40, SortOrder = 8 }; var result = new CriticalResult { CriticalTable = table, CriticalColumn = column, CriticalRollBand = rollBand, IsCurated = false, RawCellText = "Imported raw cell text", DescriptionText = "Imported description", ParseStatus = "partial", ParsedJson = "{}", SourcePageNumber = sourcePageNumber, SourceImagePath = sourceImagePath, SourceImageCropJson = "{\"pageNumber\":1}" }; result.Effects.Add(new CriticalEffect { EffectCode = CriticalEffectCodes.DirectHits, ValueInteger = 5, IsPermanent = false, SourceType = "symbol", SourceText = "+5H" }); dbContext.CriticalTables.Add(table); dbContext.CriticalResults.Add(result); await dbContext.SaveChangesAsync(); } private static void WriteSourceImage(string repositoryRoot, string relativePath) { var fullPath = Path.Combine(repositoryRoot, "artifacts", "import", "critical", relativePath.Replace('/', Path.DirectorySeparatorChar)); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); File.WriteAllBytes(fullPath, [137, 80, 78, 71]); } private static async Task GetResultIdAsync(string databasePath) { await using var dbContext = CreateDbContext(databasePath); return await dbContext.CriticalResults .Where(item => item.CriticalTable.Slug == "slash") .Select(item => item.Id) .SingleAsync(); } private static RolemasterDbContext CreateDbContext(string databasePath) { var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={databasePath}") .Options; return new RolemasterDbContext(options); } private static IDbContextFactory CreateDbContextFactory(string databasePath) { var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={databasePath}") .Options; return new TestRolemasterDbContextFactory(options); } private static string CreateEmptyDatabasePath() => Path.Combine(Path.GetTempPath(), $"lookup-service-curation-{Guid.NewGuid():N}.db"); private static string CreateTemporaryRepositoryRoot() { var repositoryRoot = Path.Combine(Path.GetTempPath(), $"rolemaster-repo-{Guid.NewGuid():N}"); Directory.CreateDirectory(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")); File.WriteAllText(Path.Combine(repositoryRoot, "RolemasterDB.slnx"), string.Empty); return repositoryRoot; } }