using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.Extensions.Options; using Moq; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using SeaHaven.Services.Validation; using Xunit; namespace Api.SeaHavenIndustries.Tests; internal static class TestAsyncEnumerableHelper { public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) { foreach (var item in source) yield return item; await Task.CompletedTask; } } public class SyncServiceBehaviorTests { [Fact] public async Task SyncWorkOrders_CreatesNewAndUpdatesExisting() { var data = new Mock(); var external = new Mock(); var existingWO = new WorkOrder { Id = 5, ExternalWorkOrderId = "EXT-1", WorkerOrderTitle = "Old" }; data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny())) .ReturnsAsync("10000005"); data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-1", It.IsAny())) .ReturnsAsync(existingWO); data.Setup(d => d.GetWorkOrderByExternalIdAsync("EXT-2", It.IsAny())) .ReturnsAsync((WorkOrder?)null); data.Setup(d => d.ResolveLocationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((int?)null); data.Setup(d => d.GetLocationCountAsync(It.IsAny())) .ReturnsAsync(3); var items = new List> { new() { ["work_order_id"] = "EXT-1", ["description"] = "Updated Title", ["wo_status"] = "in_progress" }, new() { ["work_order_id"] = "EXT-2", ["description"] = "New WO", ["wo_status"] = "new", ["severity"] = "3" } }; external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny())) .Returns(items.ToAsyncEnumerable()); var accountResolver = new Mock(); accountResolver .Setup(r => r.TryResolveFromCustomerAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(1); var service = new SyncService(data.Object, external.Object, accountResolver.Object); var result = await service.SyncWorkOrdersAsync(CancellationToken.None); Assert.Equal(2, result.Synced); Assert.Equal(1, result.Created); Assert.Equal(1, result.Updated); Assert.Equal(3, result.LocationsCreated); Assert.Equal("Updated Title", existingWO.WorkerOrderTitle); Assert.Equal("In Progress", existingWO.Status); data.Verify(d => d.EnqueueWorkOrder(It.Is(w => w.ExternalWorkOrderId == "EXT-2" && w.AccountId == 1)), Times.Once); } [Fact] public async Task SyncWorkOrders_MapsStatusAndSeverity() { var data = new Mock(); var external = new Mock(); data.Setup(d => d.GetLastInternalWONumberAsync(It.IsAny())).ReturnsAsync((string?)null); data.Setup(d => d.GetWorkOrderByExternalIdAsync(It.IsAny(), It.IsAny())).ReturnsAsync((WorkOrder?)null); data.Setup(d => d.ResolveLocationAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync((int?)null); data.Setup(d => d.GetLocationCountAsync(It.IsAny())).ReturnsAsync(0); var items = new List> { new() { ["work_order_id"] = "W1", ["wo_status"] = "completed", ["severity"] = "2", ["customer"] = "Acme" } }; external.Setup(e => e.ScanWorkOrdersAsync(It.IsAny())).Returns(items.ToAsyncEnumerable()); var accountResolver = new Mock(); accountResolver .Setup(r => r.TryResolveFromCustomerAsync("Acme", It.IsAny())) .ReturnsAsync(42); await new SyncService(data.Object, external.Object, accountResolver.Object).SyncWorkOrdersAsync(CancellationToken.None); data.Verify(d => d.EnqueueWorkOrder(It.Is(w => w.Status == "Done" && w.Priority == "Sev 2" && w.Severity == "2" && w.AccountId == 42)), Times.Once); } [Fact] public async Task SyncComments_SkipsExistingAndOrphans() { var data = new Mock(); var external = new Mock(); var wo = new WorkOrder { Id = 10, ExternalWorkOrderId = "WO-1" }; data.Setup(d => d.CommentExistsByExternalIdAsync("C-EXISTS", It.IsAny())).ReturnsAsync(true); data.Setup(d => d.CommentExistsByExternalIdAsync("C-NEW", It.IsAny())).ReturnsAsync(false); data.Setup(d => d.CommentExistsByExternalIdAsync("C-ORPHAN", It.IsAny())).ReturnsAsync(false); data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-1", It.IsAny())).ReturnsAsync(wo); data.Setup(d => d.GetWorkOrderByExternalIdAsync("WO-MISSING", It.IsAny())).ReturnsAsync((WorkOrder?)null); var items = new List> { new() { ["comment_id"] = "C-EXISTS", ["work_order_id"] = "WO-1" }, new() { ["comment_id"] = "C-NEW", ["work_order_id"] = "WO-1", ["text"] = "Hello" }, new() { ["comment_id"] = "C-ORPHAN", ["work_order_id"] = "WO-MISSING" } }; external.Setup(e => e.ScanCommentsAsync(It.IsAny())).Returns(items.ToAsyncEnumerable()); var result = await new SyncService(data.Object, external.Object, Mock.Of()).SyncCommentsAsync(CancellationToken.None); Assert.Equal(2, result.Synced); Assert.Equal(1, result.Created); Assert.Equal(2, result.Skipped); data.Verify(d => d.EnqueueComment(It.Is(c => c.ExternalCommentId == "C-NEW")), Times.Once); } } public class WorkOrderDispatchServiceBehaviorTests { [Fact] public async Task VerifyDispatch_ThrowsWhenChecklistIncomplete() { var dispatchData = CreateVerifyDispatchMocks( out var dispatch, incompleteCount: 1, checklistComplete: false, hasCustomerSignoff: true, hasVendorSignoff: true); dispatch.Id = 1; var service = CreateDispatchService(dispatchData); var ex = await Assert.ThrowsAsync(() => service.VerifyDispatchAsync(1, "user1")); Assert.Contains("1 checklist item(s) incomplete", ex.Missing); } [Fact] public async Task VerifyDispatch_ThrowsWhenVendorSignoffMissing() { var dispatchData = CreateVerifyDispatchMocks( out var dispatch, incompleteCount: 0, checklistComplete: true, hasCustomerSignoff: true, hasVendorSignoff: false); dispatch.Id = 1; var service = CreateDispatchService(dispatchData); var ex = await Assert.ThrowsAsync(() => service.VerifyDispatchAsync(1, "user1")); Assert.Contains("Missing Vendor sign-off", ex.Missing); } [Fact] public async Task VerifyDispatch_SucceedsWhenAllComplete() { var dispatchData = CreateVerifyDispatchMocks( out var dispatch, incompleteCount: 0, checklistComplete: true, hasCustomerSignoff: true, hasVendorSignoff: true); dispatch.Id = 1; var userMock = new Mock(); userMock.Setup(u => u.GetByIdAsync("user1")).ReturnsAsync(new ApplicationUser { FirstName = "Jane", LastName = "Doe" }); var service = CreateDispatchService(dispatchData, userMock: userMock); var result = await service.VerifyDispatchAsync(1, "user1"); Assert.NotNull(result); Assert.Equal("Dispatch verified", result!.Message); Assert.Equal("Verified", dispatch.Status); Assert.Equal("Jane Doe", dispatch.VerifiedBy); } private static Mock CreateVerifyDispatchMocks( out Dispatch dispatch, int incompleteCount, bool checklistComplete, bool hasCustomerSignoff, bool hasVendorSignoff) { dispatch = new Dispatch { Id = 1, DispatchNumber = "DSP-00001", WorkOrderId = 10 }; var mock = new Mock(); mock.Setup(d => d.GetByIdAsync(1)).ReturnsAsync(dispatch); var checklist = checklistComplete ? new List { new() { IsCompleted = true } } : new List { new() { IsCompleted = false } }; mock.Setup(d => d.GetChecklistItemsAsync(1)).ReturnsAsync(checklist); var signoffs = new List(); if (hasCustomerSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Customer" }); if (hasVendorSignoff) signoffs.Add(new DispatchSignoff { SignoffType = "Vendor" }); mock.Setup(d => d.GetSignoffsAsync(1)).ReturnsAsync(signoffs); return mock; } private static WorkOrderDispatchService CreateDispatchService( Mock? dispatchDataMock = null, Mock? userMock = null) { return new WorkOrderDispatchService( Mock.Of(), (dispatchDataMock ?? new Mock()).Object, Mock.Of(), (userMock ?? new Mock()).Object, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Microsoft.Extensions.Options.Options.Create(new FrontendOptions())); } } public class WorkOrderServiceBehaviorTests { [Fact] public async Task ChangeStatus_AddsAuditWhenStatusDiffers() { var wo = new WorkOrder { Id = 5, Status = "Open" }; var woData = new Mock(); woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo); var service = CreateWorkOrderService(woData); var result = await service.ChangeStatusAsync(5, "In Progress", "user1"); Assert.NotNull(result); Assert.Equal("In Progress", wo.Status); woData.Verify(d => d.AddAuditLogAsync(It.Is(a => a.FieldName == "Status" && a.OldValue == "Open" && a.NewValue == "In Progress")), Times.Once); } [Fact] public async Task ChangeStatus_DoesNotAuditWhenStatusSame() { var wo = new WorkOrder { Id = 5, Status = "Open" }; var woData = new Mock(); woData.Setup(d => d.GetByIdAsync(5)).ReturnsAsync(wo); await CreateWorkOrderService(woData).ChangeStatusAsync(5, "Open", "user1"); woData.Verify(d => d.AddAuditLogAsync(It.IsAny()), Times.Never); } [Fact] public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesComments() { var woData = new Mock(); woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true); var commentData = new Mock(); commentData.Setup(c => c.GetByWorkOrderIdAsync(5)) .ReturnsAsync(new List { new() { Id = 1 }, new() { Id = 2 } }); var quotesData = new Mock(); var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1"); Assert.True(result); quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once); commentData.Verify(c => c.DeleteAsync(1), Times.Once); commentData.Verify(c => c.DeleteAsync(2), Times.Once); } private static WorkOrderService CreateWorkOrderService( Mock? woData = null, Mock? commentData = null, Mock? quotesData = null) { return new WorkOrderService( (woData ?? new Mock()).Object, (commentData ?? new Mock()).Object, Mock.Of(), (quotesData ?? new Mock()).Object, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); } }