using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Configuration; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Xunit; namespace SeaHavenIndustries.Tests; public sealed class WorkOrderUpliftEvidenceServiceTests { private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static async Task SeedWorkOrderAsync(ApplicationDbContext context) { await WorkOrderAccountTestHelpers.EnsureAccountAsync(context); context.Users.Add(new ApplicationUser { Id = "dispatcher-1", UserName = "dispatcher-1", FirstName = "Alex", LastName = "Dispatcher", }); context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" }); context.Dispatches.Add(new Dispatch { Id = 10, VendorId = 1, WorkOrderId = 1, NTEAmount = 1000m, DispatchNumber = "DIS-10", Status = "Scheduled", }); var workOrder = new WorkOrder { Id = 1, InternalWONumber = "10000000001", PrimaryDispatchId = 10, AccountId = 1, WorkOrderType = WorkOrderType.PM, }; context.workOrders.Add(workOrder); await context.SaveChangesAsync(); return workOrder; } private static (WorkOrderUpliftEvidenceService Service, MemoryStorage Storage) NewService( ApplicationDbContext context, bool passWhenScannerUnavailable = false) { var storage = new MemoryStorage(); var service = new WorkOrderUpliftEvidenceService( new WorkOrderDetailDataService(context), new DispatchDataService(context), new VendorDocumentDataService(context), storage, WorkOrderAccountTestHelpers.Resolver(context), Options.Create(new VendorDocumentsOptions { PassWhenScannerUnavailable = passWhenScannerUnavailable, })); return (service, storage); } private static ClaimsPrincipal Dispatcher() => WorkOrderAccountTestHelpers.OrgWideAdmin("dispatcher-1"); private static FormFile FormFile(byte[] bytes, string fileName, string contentType) { return new FormFile(new MemoryStream(bytes), 0, bytes.Length, "file", fileName) { Headers = new HeaderDictionary(), ContentType = contentType, }; } [Fact] public async Task UploadAsync_StoresPendingUpliftEvidenceForThePrimaryDispatch() { await using var context = CreateContext(); var workOrder = await SeedWorkOrderAsync(context); var (service, storage) = NewService(context); var pdf = "%PDF-1.4"u8.ToArray(); var uploaded = await service.UploadAsync( workOrder.Id, FormFile(pdf, "quote.pdf", "application/pdf"), Dispatcher(), CancellationToken.None); var document = Assert.Single(context.VendorCompletionDocuments); Assert.Equal(document.Id, uploaded!.Id); Assert.Equal("Pending", uploaded.ScanStatus); Assert.Null(document.ScannedAt); Assert.Equal(VendorDocumentPurpose.UpliftEvidence, document.Purpose); Assert.Equal(1, document.VendorId); Assert.Equal(10, document.DispatchId); Assert.Equal("quote.pdf", document.OriginalFileName); Assert.Null(document.ReplacesDocumentId); var saved = Assert.Single(storage.Saved); Assert.Equal((1, 10), (saved.VendorId, saved.DispatchId)); var audit = Assert.Single(context.WorkOrderAuditLogs); Assert.Equal("uplift_evidence_uploaded", audit.Action); Assert.Equal("internal", audit.ActorType); Assert.Equal("dispatcher-1", audit.UserId); } [Fact] public async Task UploadAsync_PassWhenScannerUnavailable_StoresPassedEvidence() { await using var context = CreateContext(); var workOrder = await SeedWorkOrderAsync(context); var (service, _) = NewService(context, passWhenScannerUnavailable: true); var uploaded = await service.UploadAsync( workOrder.Id, FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"), Dispatcher(), CancellationToken.None); var document = Assert.Single(context.VendorCompletionDocuments); Assert.Equal("Passed", uploaded!.ScanStatus); Assert.Equal("Passed", document.ScanStatus); Assert.NotNull(document.ScannedAt); Assert.Equal("Processing", document.ReviewStatus); } [Fact] public async Task UploadAsync_MismatchedSignature_SavesNothing() { await using var context = CreateContext(); var workOrder = await SeedWorkOrderAsync(context); var (service, storage) = NewService(context); var ex = await Assert.ThrowsAsync(() => service.UploadAsync( workOrder.Id, FormFile("not a pdf"u8.ToArray(), "quote.pdf", "application/pdf"), Dispatcher(), CancellationToken.None)); Assert.Contains("signature", ex.Message, StringComparison.OrdinalIgnoreCase); Assert.Empty(context.VendorCompletionDocuments); Assert.Empty(storage.Saved); } [Fact] public async Task GetStatusAsync_ReturnsScanStatusOnlyForUpliftEvidenceOnTheDispatch() { await using var context = CreateContext(); var workOrder = await SeedWorkOrderAsync(context); var (service, _) = NewService(context); var uploaded = await service.UploadAsync( workOrder.Id, FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"), Dispatcher(), CancellationToken.None); var pending = await service.GetStatusAsync(workOrder.Id, uploaded!.Id, Dispatcher(), CancellationToken.None); Assert.Equal("Pending", pending!.ScanStatus); Assert.Equal("quote.pdf", pending.OriginalFileName); var stored = Assert.Single(context.VendorCompletionDocuments); stored.ScanStatus = "Passed"; context.VendorCompletionDocuments.Add(new VendorCompletionDocument { Id = 90, VendorId = 1, DispatchId = 10, WorkOrderId = workOrder.Id, OriginalFileName = "done.pdf", StoredFileName = "done.pdf", ContentType = "application/pdf", Purpose = "Completion", ScanStatus = "Passed", Version = 1, }); await context.SaveChangesAsync(); var passed = await service.GetStatusAsync(workOrder.Id, uploaded.Id, Dispatcher(), CancellationToken.None); Assert.Equal("Passed", passed!.ScanStatus); Assert.Null(await service.GetStatusAsync(workOrder.Id, 90, Dispatcher(), CancellationToken.None)); } private sealed class MemoryStorage : IVendorDocumentStoragePort { public List<(int VendorId, int DispatchId, string Name)> Saved { get; } = new(); public async Task SaveAsync( int vendorId, int dispatchId, string storedFileName, Stream content, CancellationToken cancellationToken) { using var buffer = new MemoryStream(); await content.CopyToAsync(buffer, cancellationToken); Saved.Add((vendorId, dispatchId, storedFileName)); } public Stream OpenRead(int vendorId, int dispatchId, string storedFileName) => throw new NotSupportedException(); public void Delete(int vendorId, int dispatchId, string storedFileName) { } } }