using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Helpers; using SeaHaven.DataServices.Implementation; using SeaHaven.DataServices.Interfaces; namespace SeaHavenIndustries.Tests; public sealed class WorkOrderServiceRegistryKeywordSearchRelationalTests { [Fact] public async Task Search_MatchesCurrentAndHistoricalServiceLabelsAndPreservesExistingAliases() { await using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var context = new SqliteSearchTestDbContext(options); await context.Database.EnsureCreatedAsync(); context.Services.AddRange( new Service { Id = 10, Name = "Chiller Maintenance Original", NormalizedName = "CHILLER MAINTENANCE ORIGINAL", Trade = "HVAC", IconKey = "hvac", IsActive = true }, new Service { Id = 20, Name = "Roof Maintenance Current", NormalizedName = "ROOF MAINTENANCE CURRENT", Trade = "Roofing", IconKey = "roofing", IsActive = true }, new Service { Id = 30, Name = "Pump Service Current", NormalizedName = "PUMP SERVICE CURRENT", Trade = "Plumbing", IconKey = "plumbing", IsActive = false }); context.Vendors.Add(new Vendor { Id = 40, CompanyName = "Acme Vendor Group", ContactName = "Morgan Technician" }); context.Dispatches.Add(new Dispatch { Id = 50, VendorId = 40, Status = "Sent" }); context.workOrders.AddRange( new WorkOrder { Id = 1, ServiceId = 10, ServiceNameSnapshot = "Legacy Chiller Label", Trade = "HVAC", Problem = "Cooling noise", LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.PM, ScheduledDate = new DateTime(2026, 9, 15), PrimaryDispatchId = 50, TechnicianAssigned = true }, new WorkOrder { Id = 2, ServiceId = 20, ServiceNameSnapshot = "Frozen Roof Label", Trade = "Roofing", Problem = "Roof leak", LifecycleStatus = LifecycleStatus.Completed, WorkOrderType = WorkOrderType.PM, ScheduledDate = new DateTime(2026, 9, 15) }, new WorkOrder { Id = 3, ServiceId = 30, ServiceNameSnapshot = "Historical Pump Label", Trade = "Plumbing", Problem = "Pump noise", LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.PM, ScheduledDate = new DateTime(2026, 9, 15) }, new WorkOrder { Id = 4, Trade = "Legacy Boiler Service", Problem = "Oil leak", LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.Reactive, ScheduledDate = new DateTime(2026, 9, 15) }, new WorkOrder { Id = 5, Trade = "Unlinked vendor WO", LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.Reactive, ScheduledDate = new DateTime(2026, 9, 15), PrimaryDispatchId = 50, TechnicianAssigned = false }); await context.SaveChangesAsync(); var renamedService = await context.Services.SingleAsync(service => service.Id == 10); renamedService.Name = "Northstar Chiller Service"; renamedService.NormalizedName = "NORTHSTAR CHILLER SERVICE"; await context.SaveChangesAsync(); async Task SearchIds(string search) => await WorkOrderBoardSearchFilter .Apply(context.workOrders.AsNoTracking(), search) .Select(workOrder => workOrder.Id) .OrderBy(id => id) .ToArrayAsync(); Assert.Equal(new[] { 1 }, await SearchIds("Northstar Chiller Service")); Assert.Equal(new[] { 1 }, await SearchIds("Legacy Chiller Label")); Assert.Equal(new[] { 2 }, await SearchIds("Frozen Roof Label")); Assert.Equal(new[] { 2 }, await SearchIds("Roof Maintenance Current")); Assert.Equal(new[] { 3 }, await SearchIds("Historical Pump Label")); Assert.Equal(new[] { 3 }, await SearchIds("Pump Service Current")); Assert.Equal(new[] { 4 }, await SearchIds("Legacy Boiler Service")); Assert.Equal(new[] { 4 }, await SearchIds("Oil leak")); Assert.Equal(new[] { 1, 5 }, await SearchIds("Acme Vendor Group")); Assert.Equal(new[] { 1 }, await SearchIds("Morgan Technician")); var advancedSearch = new WorkOrderAdvancedSearchDataService(context); var matchingFacet = await advancedSearch.SearchAsync(CreateQuery( search: "Northstar Chiller Service", pmTypes: new[] { "Northstar Chiller Service" })); Assert.Equal(new[] { 1 }, matchingFacet.Rows.Select(row => row.Id)); Assert.Equal("Northstar Chiller Service", Assert.Single(matchingFacet.Rows).Trade); var conflictingFacet = await advancedSearch.SearchAsync(CreateQuery( search: "Northstar Chiller Service", pmTypes: new[] { "Roof Maintenance Current" })); Assert.Empty(conflictingFacet.Rows); Assert.Equal(0, conflictingFacet.TotalCount); } private static WorkOrderAdvancedSearchQuery CreateQuery(string search, IReadOnlyList? pmTypes = null) => new( Search: search, DateFrom: new DateOnly(2026, 9, 1), DateTo: new DateOnly(2026, 9, 30), UnscheduledOnly: false, Sites: null, Regions: null, Types: null, Overdue: false, Dispatchers: null, Statuses: null, PmTypes: pmTypes, VendorIds: null, DocStatuses: null, Severities: null, Rescheduled: false, CarriedOver: false, AddOn: false, AvetaOnly: false, FlagColors: null, InternalOnly: false, HasUplift: false, UpliftStatuses: null, MyWorkOrders: false, CurrentUserId: null, Page: 0, PageSize: 100, SortBy: "scheduledDate", SortDir: "asc"); private sealed class SqliteSearchTestDbContext : ApplicationDbContext { public SqliteSearchTestDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); foreach (var index in builder.Model.GetEntityTypes().SelectMany(entity => entity.GetIndexes())) { if (index.GetFilter() != null) index.SetFilter(null); } foreach (var entityType in new[] { typeof(WorkOrder), typeof(Dispatch) }) { var property = builder.Entity(entityType).Property("RowVersion").Metadata; property.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never; property.IsConcurrencyToken = false; } } } }