using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Implementation; using SeaHaven.Services.Validation; namespace SeaHavenIndustries.Tests; public class WorkOrderBoardCreateServiceTests { private static (ApplicationDbContext Context, WorkOrderBoardCreateService Service) CreateSut() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var context = new ApplicationDbContext(options); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData); var fieldLocks = new WorkOrderFieldLockService(context); var audit = new WorkOrderAuditService(context, fieldLocks); var validator = new WorkOrderBoardCreateValidation(); var service = new WorkOrderBoardCreateService(context, boardData, boardService, audit, validator); return (context, service); } [Fact] public async Task Create_Minimal_ReturnsIncomplete() { var (_, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5" }, "actor-1"); Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus); Assert.Equal("BK5", result.SiteCode); Assert.Equal(WorkOrderType.PM, result.WorkOrderType); Assert.False(string.IsNullOrWhiteSpace(result.WoNumber)); Assert.Equal(11, result.WoNumber!.Length); } [Fact] public async Task Create_WithManualWoNumber_NormalizesTo11Digits() { var (_, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WoNumber = "12345", WorkOrderType = WorkOrderType.PO, SiteCode = "DAL" }, "actor-1"); Assert.Equal("00000012345", result.WoNumber); } [Fact] public async Task Create_DuplicateWoNumber_Throws() { var (context, service) = CreateSut(); context.workOrders.Add(new WorkOrder { InternalWONumber = "00000099999", WorkerOrderNumber = "00000099999" }); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WoNumber = "99999", WorkOrderType = WorkOrderType.PM, SiteCode = "BK5" }, "actor-1")); Assert.Equal("DuplicateWoNumber", ex.Code); } [Fact] public async Task Create_AutoSchedulesWhenDateAndAssignee() { var (_, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", AssignTo = "dispatcher-1", ScheduledDate = new DateTime(2026, 6, 25) }, "actor-1"); Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus); Assert.Equal(new DateTime(2026, 6, 25), result.ScheduledDate); } [Fact] public async Task Create_WeekOnlyScheduling_SetsTargetWeek() { var (_, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", ScheduleWeekOnly = true, TargetWeek = new DateOnly(2026, 6, 22) }, "actor-1"); Assert.True(result.ScheduleWeekOnly); Assert.Equal(new DateOnly(2026, 6, 22), result.TargetWeek); } [Fact] public async Task Create_WeekOnlyWithoutTargetWeek_ThrowsValidation() { var (_, service) = CreateSut(); await Assert.ThrowsAsync(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", ScheduleWeekOnly = true }, "actor-1")); } [Fact] public async Task Create_WithVendor_CreatesPrimaryDispatch() { var (context, service) = CreateSut(); context.Vendors.Add(new Vendor { Id = 5, CompanyName = "Acme HVAC" }); await context.SaveChangesAsync(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", VendorId = 5, ApptDate = new DateTime(2026, 6, 26) }, "actor-1"); Assert.Equal(5, result.VendorId); Assert.NotNull(result.PrimaryDispatchId); } [Fact] public async Task Create_CreatesFieldLocksForPopulatedFields() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", Description = "Test WO" }, "actor-1"); var locks = await context.WorkOrderFieldLocks.ToListAsync(); Assert.Contains(locks, l => l.FieldName == "SiteCode"); Assert.Contains(locks, l => l.FieldName == "WorkOrderType"); Assert.Contains(locks, l => l.FieldName == "Description"); Assert.All(locks, l => Assert.Equal(result.Id, l.WorkOrderId)); } [Fact] public async Task Create_WritesCreateAuditEvent() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5" }, "actor-1"); var logs = await context.WorkOrderAuditLogs.ToListAsync(); Assert.Contains(logs, l => l.Action == "Create"); Assert.Contains(logs, l => l.Action == "FieldChanged" && l.FieldName == "SiteCode"); Assert.All(logs, l => Assert.Equal(result.Id, l.WorkOrderId)); Assert.True(result.Id > 0); } [Fact] public async Task Create_StructuredServiceNotes_PersistsAndReturnsPm() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", PrimaryService = "HVAC PM", ExtraServices = new List { "Filter change", "Coil clean", "Filter change" }, ServiceNotes = "Unit on roof" }, "actor-1"); Assert.Equal("HVAC PM", result.Pm); Assert.Equal("Unit on roof", result.ServiceNotes); Assert.Equal(new[] { "Filter change", "Coil clean" }, result.ExtraServices); var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); Assert.Equal("HVAC PM", wo.Trade); Assert.Equal("Unit on roof", wo.ServiceNotes); Assert.Contains("Filter change", wo.ExtraServices); Assert.Contains("Coil clean", wo.ExtraServices); Assert.Equal(1, System.Text.Json.JsonSerializer.Deserialize>(wo.ExtraServices!)! .Count(s => s == "Filter change")); } [Fact] public async Task Create_PrimaryServiceTakesPrecedenceOverTrade() { var (_, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.Reactive, SiteCode = "CHI", Trade = "Legacy Trade", PrimaryService = "Plumbing" }, "actor-1"); Assert.Equal("Plumbing", result.Pm); } [Fact] public async Task Create_ExtraServicesWithoutPrimary_ThrowsValidation() { var (_, service) = CreateSut(); await Assert.ThrowsAsync(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", ExtraServices = new List { "Filter change" } }, "actor-1")); } [Fact] public async Task Create_PocFreeText_WithoutContact_AppearsOnBoardRow() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.Reactive, SiteCode = "BK5", PocName = "Jane Site Lead", PocPhone = "+1 555-0100", PocNotes = "Call 30 min before" }, "actor-1"); Assert.Equal("Jane Site Lead", result.PocName); Assert.Equal("+1 555-0100", result.PocPhone); Assert.Equal("Call 30 min before", result.PocNotes); var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); Assert.Equal("Jane Site Lead", wo.PocName); Assert.Empty(context.WorkOrderContacts); } [Fact] public async Task Create_WithoutVendor_PersistsTechPhoneAndVendorNotesOnWorkOrder() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", TechPhone = "+1 555-0199", VendorNotes = "Gate code 4421" }, "actor-1"); Assert.Equal("+1 555-0199", result.TechPhone); var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); Assert.Equal("+1 555-0199", wo.TechPhone); Assert.Equal("Gate code 4421", wo.VendorNotes); Assert.Null(wo.PrimaryDispatchId); } [Fact] public async Task Create_WithVendor_PersistsTechPhoneAndVendorNotesOnDispatch() { var (context, service) = CreateSut(); context.Vendors.Add(new Vendor { Id = 55, CompanyName = "Acme", Phone = "+1 555-0000" }); await context.SaveChangesAsync(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", VendorId = 55, ApptDate = new DateTime(2026, 7, 18), TechPhone = "+1 555-0199", VendorNotes = "Park behind dock" }, "actor-1"); Assert.Equal("+1 555-0199", result.TechPhone); Assert.Equal(55, result.VendorId); var wo = await context.workOrders.Include(w => w.PrimaryDispatch).SingleAsync(w => w.Id == result.Id); Assert.Null(wo.TechPhone); Assert.Null(wo.VendorNotes); Assert.NotNull(wo.PrimaryDispatch); Assert.Equal("+1 555-0199", wo.PrimaryDispatch!.TechPhone); Assert.Equal("Park behind dock", wo.PrimaryDispatch.VendorNotes); } [Fact] public async Task Create_LegacyTradeAndDescription_StillWorks() { var (context, service) = CreateSut(); var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", Trade = "HVAC", Description = "pmNote\nPOC: Jane ยท +1 555\nVendor notes: gate" }, "actor-1"); Assert.Equal("HVAC", result.Pm); Assert.Null(result.PocName); var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id); Assert.Equal("HVAC", wo.Trade); Assert.Contains("Vendor notes:", wo.Description); Assert.Null(wo.PocName); Assert.Null(wo.ExtraServices); Assert.Null(wo.ServiceNotes); } [Fact] public async Task Create_UnknownVendorId_Throws() { var (_, service) = CreateSut(); var ex = await Assert.ThrowsAsync(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", VendorId = 999 }, "actor-1")); Assert.Equal("VendorNotFound", ex.Code); } }