using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; 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()) .Options; return new ApplicationDbContext(options); } private static WorkOrderWeekRolledService CreateService(ApplicationDbContext context) { var dataService = new WorkOrderDomainJobDataService(context); var audit = new WorkOrderAuditService(context, new WorkOrderFieldLockService(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.Complete }, new WorkOrder { Id = 2, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Canceled }, new WorkOrder { Id = 3, ScheduledDate = new DateTime(2026, 6, 18), LifecycleStatus = LifecycleStatus.Closed }); 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(1, second.Skipped); Assert.Equal(0, second.Processed); 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(1, rerun.Skipped); var wo = await context.workOrders.FindAsync(1); Assert.Equal(1, wo!.CarriedOver); } [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, ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Scheduled }, new WorkOrder { Id = 2, ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Complete, OperationalFlags = OperationalFlags.PastDue }, new WorkOrder { Id = 3, 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_IsPastDue_UnaffectedByCache() { await using var context = CreateContext(); var pastDate = DateTime.UtcNow.Date.AddDays(-2); context.workOrders.Add(new WorkOrder { Id = 1, ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.None }); await context.SaveChangesAsync(); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData); var row = await boardService.GetBoardRowAsync(1); Assert.NotNull(row); Assert.True(row!.IsPastDue); Assert.False(WorkOrderDerivedFields.IsPastDue(pastDate, LifecycleStatus.Scheduled) == false); } [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, ScheduledDate = futureDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.PastDue }); await context.SaveChangesAsync(); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData); var row = await boardService.GetBoardRowAsync(1); Assert.NotNull(row); Assert.False(row!.IsPastDue); } }