using System.Security.Claims; using Api.SeaHavenIndustries.Controllers; using Api.SeaHavenIndustries.DTOs; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using SeaHaven.DataServices.Implementation; using SeaHaven.DataServices.Models; using SeaHaven.Services.Implementation; using SeaHaven.Services.Validation; namespace SeaHavenIndustries.Tests; public sealed class WorkOrderEditAddonRescheduleTests { [Fact] public async Task EditWorkorder_RescheduleIntoEarlierWeek_EnablesAndAuditsAddOn() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 6, 29), IsAddOn = false }); await context.SaveChangesAsync(); var result = await CreateController(context).Editworkorder(new EditWorkorder_DTO { Id = 1, ScheduledDate = new DateTime(2026, 6, 25) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.Equal(new DateTime(2026, 6, 25), persisted.ScheduledDate); Assert.True(persisted.IsAddOn); Assert.Equal(1, persisted.RescheduleCount); var audit = await context.WorkOrderAuditLogs .Where(x => x.WorkOrderId == 1) .ToListAsync(); Assert.Single(audit); Assert.Contains(audit, x => x.FieldName?.Contains("Schedule On", StringComparison.Ordinal) == true); Assert.Contains(audit, x => x.FieldName?.Contains("IsAddOn", StringComparison.Ordinal) == true); Assert.Contains(audit, x => x.FieldName?.Contains("RescheduleCount: 0 → 1", StringComparison.Ordinal) == true); } [Fact] public async Task EditWorkorder_RescheduleOlderWorkOrderIntoWeekPastCutoff_EnablesAndAuditsAddOn() { // SH-412 retest: the WO was created weeks before the target week's cutoff, so the // creation-time rule kept Add-On off. It arrives in the week when it is rescheduled. await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 7, 8), IsAddOn = false }); await context.SaveChangesAsync(); var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO { Id = 1, ScheduledDate = new DateTime(2026, 6, 25) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.True(persisted.IsAddOn); var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1); Assert.Contains("IsAddOn: False → True", audit.FieldName); } [Fact] public async Task EditWorkorder_RescheduleIntoWeekBeforeCutoff_ClearsAndAuditsAddOn() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 23, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 6, 24), IsAddOn = true }); await context.SaveChangesAsync(); var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO { Id = 1, ScheduledDate = new DateTime(2026, 7, 1) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.False(persisted.IsAddOn); var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1); Assert.Contains("IsAddOn: True → False", audit.FieldName); } [Fact] public async Task EditWorkorder_RescheduleWithinSameWeek_KeepsAddOn() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 7, 6), IsAddOn = false }); await context.SaveChangesAsync(); var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO { Id = 1, ScheduledDate = new DateTime(2026, 7, 9) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.False(persisted.IsAddOn); var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1); Assert.DoesNotContain("IsAddOn", audit.FieldName); } [Fact] public async Task EditWorkorder_WithoutScheduledDate_LeavesScheduleAndAddOnUntouched() { // The edit form sends only changed fields: a null Schedule On means "not sent", never // "clear". Clearing happens through the board patch, which turns Add-On off. await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 23, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 6, 24), IsAddOn = true }); await context.SaveChangesAsync(); var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO { Id = 1, DueDate = new DateTime(2026, 6, 30) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.Equal(new DateTime(2026, 6, 24), persisted.ScheduledDate); Assert.True(persisted.IsAddOn); Assert.DoesNotContain( await context.WorkOrderAuditLogs.Where(x => x.WorkOrderId == 1).ToListAsync(), x => x.FieldName?.Contains("IsAddOn", StringComparison.Ordinal) == true); } [Fact] public async Task EditWorkorder_FirstSchedule_SetsOriginalWeekAndPromotesIncomplete() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc), LifecycleStatus = LifecycleStatus.Incomplete }); await context.SaveChangesAsync(); var result = await CreateController(context).Editworkorder(new EditWorkorder_DTO { Id = 1, ScheduledDate = new DateTime(2026, 7, 1) }); Assert.IsType(result); var persisted = await context.workOrders.SingleAsync(x => x.Id == 1); Assert.Equal(new DateOnly(2026, 7, 1), persisted.OriginalDate); Assert.Equal(new DateOnly(2026, 6, 29), persisted.OriginalWeek); Assert.Equal(0, persisted.RescheduleCount); Assert.Equal(LifecycleStatus.Scheduled, persisted.LifecycleStatus); } [Fact] public async Task EditWorkorder_Reschedule_WaitsForWorkOrderMutationGateAndReadsCommittedSchedule() { const int workOrderId = 41201; var databaseName = Guid.NewGuid().ToString(); await using (var seed = CreateContext(databaseName)) { seed.workOrders.Add(new WorkOrder { Id = workOrderId, CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc), ScheduledDate = new DateTime(2026, 7, 6), OriginalDate = new DateOnly(2026, 7, 6), IsAddOn = false }); await seed.SaveChangesAsync(); } // A concurrent reschedule holds the work order's mutation gate and commits its own // date while the edit is waiting. await using var holderContext = CreateContext(databaseName); var held = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var holder = new UpliftDataService(holderContext).ExecuteWorkOrderMutationAsync( workOrderId, async ct => { held.SetResult(); await release.Task; var workOrder = await holderContext.workOrders.SingleAsync(x => x.Id == workOrderId, ct); workOrder.ScheduledDate = new DateTime(2026, 6, 29); workOrder.RescheduleCount++; await holderContext.SaveChangesAsync(ct); return true; }, CancellationToken.None); await held.Task; await using var editContext = CreateContext(databaseName); var edit = CreateController(editContext).Editworkorder(new EditWorkorder_DTO { Id = workOrderId, ScheduledDate = new DateTime(2026, 6, 25) }); await Task.Delay(200); Assert.False(edit.IsCompleted); release.SetResult(); await holder; Assert.IsType(await edit); await using var verify = CreateContext(databaseName); var persisted = await verify.workOrders.SingleAsync(x => x.Id == workOrderId); Assert.Equal(new DateTime(2026, 6, 25), persisted.ScheduledDate); Assert.Equal(2, persisted.RescheduleCount); Assert.True(persisted.IsAddOn); var audit = await verify.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == workOrderId); Assert.Contains("Schedule On: 2026-06-29 → 2026-06-25", audit.FieldName); Assert.Contains("RescheduleCount: 1 → 2", audit.FieldName); } private static ApplicationDbContext CreateContext(string? databaseName = null) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName ?? Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } // Wed 2026-06-24: the week of 06-22 is past its cutoff, the week of 06-29 is not. private sealed class FixedTimeProvider : TimeProvider { public override DateTimeOffset GetUtcNow() => new(2026, 6, 24, 10, 0, 0, TimeSpan.Zero); } private static WorkOrderController CreateController( ApplicationDbContext context, TimeProvider? timeProvider = null) { var accountResolver = WorkOrderAccountTestHelpers.Resolver(context); var workOrderService = new WorkOrderService( new WorkOrderDataService(context), new CommentDataService(context), new UserDataService(context), new QuotesDataService(context), fileStorage: null!, new CreateWorkOrderValidation(), new UpdateWorkOrderValidation(), accountResolver, timeProvider); return new WorkOrderController( workOrderService, accountResolver, NullLogger.Instance) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, "dispatcher-1") }, authenticationType: "test")) } } }; } }