using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging.Abstractions; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; namespace SeaHavenIndustries.Tests; public class WorkOrderWeekRolledTests { private static readonly DateOnly SourceWeekStart = new(2026, 6, 16); private static readonly DateOnly SourceWeekEnd = new(2026, 6, 20); private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new ApplicationDbContext(options); } private static WorkOrderWeekRolledService CreateService(ApplicationDbContext context) { var dataService = new WorkOrderDomainJobDataService(context); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context))); return new WorkOrderWeekRolledService( dataService, audit, NullLogger.Instance); } [Fact] public async Task WeekRolled_IncrementsCarriedOver_ForEligibleWo() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); await context.SaveChangesAsync(); var service = CreateService(context); var result = await service.ProcessWeekRolledAsync(SourceWeekStart); Assert.Equal(1, result.Processed); Assert.Equal(0, result.Skipped); Assert.Equal(0, result.Failed); var wo = await context.workOrders.FindAsync(1); Assert.Equal(1, wo!.CarriedOver); } [Fact] public async Task WeekRolled_SkipsTerminalStatuses() { await using var context = CreateContext(); context.workOrders.AddRange( new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Completed }, new WorkOrder { Id = 2, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Canceled }); await context.SaveChangesAsync(); var service = CreateService(context); var result = await service.ProcessWeekRolledAsync(SourceWeekStart); Assert.Equal(0, result.Processed); Assert.Equal(0, result.Failed); } [Fact] public async Task WeekRolled_Idempotent_SecondRunSkips() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); await context.SaveChangesAsync(); var service = CreateService(context); var first = await service.ProcessWeekRolledAsync(SourceWeekStart); var second = await service.ProcessWeekRolledAsync(SourceWeekStart); Assert.Equal(1, first.Processed); Assert.Equal(0, second.Processed); Assert.Equal(0, second.Failed); var wo = await context.workOrders.FindAsync(1); Assert.Equal(1, wo!.CarriedOver); } [Fact] public async Task WeekRolled_WritesAuditEvent() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.InProgress, CarriedOver = 2 }); await context.SaveChangesAsync(); var service = CreateService(context); await service.ProcessWeekRolledAsync(SourceWeekStart); var logs = await context.WorkOrderAuditLogs.ToListAsync(); Assert.Single(logs); Assert.Equal("WeekRolled", logs[0].Action); Assert.Equal("System", logs[0].EventType); Assert.Equal("System", logs[0].ActorType); Assert.Equal("CarriedOver", logs[0].FieldName); Assert.Equal("2", logs[0].OldValue); Assert.Equal("3", logs[0].NewValue); Assert.Equal("week:2026-06-16", logs[0].CorrelationId); var locks = await context.WorkOrderFieldLocks.ToListAsync(); Assert.Empty(locks); } [Fact] public async Task WeekRolled_ConcurrentWithReschedule_DoesNotDuplicate() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); await context.SaveChangesAsync(); var dataService = new WorkOrderDomainJobDataService(context); var correlationId = WorkOrderOperationalWeek.BuildWeekCorrelationId(SourceWeekStart); var first = await dataService.TryProcessWeekRolledAsync(1, SourceWeekStart, correlationId); var second = await dataService.TryProcessWeekRolledAsync(1, SourceWeekStart, correlationId); Assert.True(first.Processed); Assert.True(second.Skipped); var wo = await context.workOrders.FindAsync(1); Assert.Equal(1, wo!.CarriedOver); } [Fact] public async Task WeekRolled_FailureReRun_IsSafeAfterPartialLedger() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); await context.SaveChangesAsync(); var dataService = new WorkOrderDomainJobDataService(context); var correlationId = WorkOrderOperationalWeek.BuildWeekCorrelationId(SourceWeekStart); await dataService.TryProcessWeekRolledAsync(1, SourceWeekStart, correlationId); var service = CreateService(context); var rerun = await service.ProcessWeekRolledAsync(SourceWeekStart); Assert.Equal(0, rerun.Processed); Assert.Equal(0, rerun.Failed); var wo = await context.workOrders.FindAsync(1); Assert.Equal(1, wo!.CarriedOver); } [Fact] public async Task WeekRolled_ProcessesAllEligible_WhenMoreThanBatchSize() { // WorkOrderWeekRolledService.BatchSize is 500; more than one page must all be carried over. const int eligibleCount = 501; await using var context = CreateContext(); for (var i = 1; i <= eligibleCount; i++) { context.workOrders.Add(new WorkOrder { Id = i, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); } await context.SaveChangesAsync(); var service = CreateService(context); var result = await service.ProcessWeekRolledAsync(SourceWeekStart); Assert.Equal(eligibleCount, result.Processed); Assert.Equal(0, result.Skipped); Assert.Equal(0, result.Failed); var carriedOverCount = await context.workOrders.CountAsync(w => w.CarriedOver == 1); Assert.Equal(eligibleCount, carriedOverCount); var ledgerCount = await context.WorkOrderWeekRolledLedgers.CountAsync(); Assert.Equal(eligibleCount, ledgerCount); var auditCount = await context.WorkOrderAuditLogs.CountAsync(l => l.Action == "WeekRolled"); Assert.Equal(eligibleCount, auditCount); } [Fact] public async Task PastDueCache_SyncsOperationalFlags() { await using var context = CreateContext(); var pastDate = DateTime.UtcNow.Date.AddDays(-3); var futureDate = DateTime.UtcNow.Date.AddDays(3); context.workOrders.AddRange( new WorkOrder { Id = 1, DueDate = pastDate, ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Scheduled }, new WorkOrder { Id = 2, DueDate = pastDate, ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Completed, OperationalFlags = OperationalFlags.PastDue }, new WorkOrder { Id = 3, DueDate = futureDate, ScheduledDate = futureDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.PastDue }); await context.SaveChangesAsync(); var dataService = new WorkOrderDomainJobDataService(context); var result = await dataService.RefreshPastDueFlagsAsync(DateTime.UtcNow); Assert.Equal(1, result.SetCount); Assert.Equal(2, result.ClearedCount); var wo1 = await context.workOrders.FindAsync(1); var wo2 = await context.workOrders.FindAsync(2); var wo3 = await context.workOrders.FindAsync(3); Assert.True(wo1!.OperationalFlags.HasFlag(OperationalFlags.PastDue)); Assert.False(wo2!.OperationalFlags.HasFlag(OperationalFlags.PastDue)); Assert.False(wo3!.OperationalFlags.HasFlag(OperationalFlags.PastDue)); } [Fact] public async Task Board_ReflectsCarriedOver_AfterWeekRolled() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Scheduled, CarriedOver = 0 }); await context.SaveChangesAsync(); var service = CreateService(context); await service.ProcessWeekRolledAsync(SourceWeekStart); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData, WorkOrderAccountTestHelpers.Resolver(context)); var row = await boardService.GetBoardRowAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin()); Assert.NotNull(row); Assert.Equal(1, row!.CarriedOver); } [Fact] public async Task Board_IsPastDue_UnaffectedByCache() { await using var context = CreateContext(); var pastDate = DateTime.UtcNow.Date.AddDays(-2); context.workOrders.Add(new WorkOrder { Id = 1, DueDate = DateTime.UtcNow.Date.AddDays(5), ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.None }); await context.SaveChangesAsync(); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData, WorkOrderAccountTestHelpers.Resolver(context)); var row = await boardService.GetBoardRowAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin()); Assert.NotNull(row); Assert.True(row!.IsPastDue); Assert.True(WorkOrderDerivedFields.IsPastDue(pastDate, LifecycleStatus.Scheduled)); } [Fact] public async Task Board_IsPastDue_IgnoresStalePastDueFlag() { await using var context = CreateContext(); var futureDate = DateTime.UtcNow.Date.AddDays(5); context.workOrders.Add(new WorkOrder { Id = 1, DueDate = DateTime.UtcNow.Date.AddDays(-3), ScheduledDate = futureDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.PastDue }); await context.SaveChangesAsync(); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData, WorkOrderAccountTestHelpers.Resolver(context)); var row = await boardService.GetBoardRowAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin()); Assert.NotNull(row); Assert.False(row!.IsPastDue); } }