using Data.SeaHavenIndustries; using FluentAssertions; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class LocationDataServiceTests { private static ApplicationDbContext NewContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static async Task SeedAsync(ApplicationDbContext ctx) { ctx.Locations.AddRange( new Locations { Id = 1, Name = "Alpha Site", City = "Austin", State = "TX" }, new Locations { Id = 2, Name = "Beta Site", City = "Dallas", State = "TX" }, new Locations { Id = 3, Name = "Gamma Yard", City = "Kansas City", State = "MO" }, new Locations { Id = 4, Name = "Delta Hub", City = "Indianapolis", State = "indiana" }, new Locations { Id = 5, Name = "Epsilon Depot", City = "Houston", State = "TX" }, new Locations { Id = 6, Name = "Zeta Lot", City = "Nowhere", State = null }); await ctx.SaveChangesAsync(); } [Fact] public async Task GetListPagedAsync_WithoutStates_ReturnsAllRows() { await using var ctx = NewContext(); await SeedAsync(ctx); var (items, totalCount) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None); totalCount.Should().Be(6); items.Should().HaveCount(6); } [Fact] public async Task GetListPagedAsync_AppliesStatesFilterBeforeCountAndPaging() { await using var ctx = NewContext(); await SeedAsync(ctx); var (page1, totalCount) = await new LocationDataService(ctx) .GetListPagedAsync(1, 2, null, new[] { "TX" }, CancellationToken.None); totalCount.Should().Be(3, "count must reflect the state filter, not the whole table"); page1.Should().HaveCount(2); page1.Select(l => l.Name).Should().ContainInOrder("Alpha Site", "Beta Site"); var (page2, totalCountAgain) = await new LocationDataService(ctx) .GetListPagedAsync(2, 2, null, new[] { "TX" }, CancellationToken.None); totalCountAgain.Should().Be(3); page2.Select(l => l.Name).Should().ContainSingle("Epsilon Depot"); } [Fact] public async Task GetListPagedAsync_MultipleStates_KeepSearchFilter() { await using var ctx = NewContext(); await SeedAsync(ctx); var (items, totalCount) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, "Dallas", new[] { "TX", "MO" }, CancellationToken.None); totalCount.Should().Be(1); items.Should().ContainSingle().Which.Name.Should().Be("Beta Site"); } [Fact] public async Task GetListPagedAsync_StatesFilter_ExcludesNullAndNonMatchingStates() { await using var ctx = NewContext(); await SeedAsync(ctx); var (items, totalCount) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, new[] { "IN", "INDIANA", "MO", "MISSOURI" }, CancellationToken.None); totalCount.Should().Be(2); items.Select(l => l.State).Should().BeEquivalentTo("indiana", "MO"); } private static async Task SeedRegistryAsync(ApplicationDbContext ctx) { ctx.Accounts.AddRange( new Accounts { Id = 1, Name = "Acme Co", IsDeleted = false }, new Accounts { Id = 2, Name = "Borealis LLC", IsDeleted = false }); ctx.Locations.AddRange( new Locations { Id = 10, Name = "STL8", Address1 = "9 River Rd", City = "St. Louis", State = "MO", AccountId = 1 }, new Locations { Id = 11, Name = "DFW8", Address1 = "2 Prairie Ave", City = "Dallas", State = "TX", AccountId = 2 }, new Locations { Id = 12, Name = "IND9", Address1 = "5 Circle Blvd", City = "Indianapolis", State = "IN", AccountId = 1 }, new Locations { Id = 13, Name = "MIA2", Address1 = "1 Bay Dr", City = "Miami", State = "FL", AccountId = 2 }); ctx.Contacts.AddRange( new Contacts { Id = 100, LocationId = 10, SiteContactOrder = 0, FirstName = "Cara Lane", PhoneNumber = "555-0001" }, new Contacts { Id = 101, LocationId = 10, SiteContactOrder = 1, FirstName = "Alan Ford", PhoneNumber = "555-0002" }, new Contacts { Id = 102, LocationId = 11, SiteContactOrder = 1, FirstName = "Zoe Park", PhoneNumber = "555-0003" }, new Contacts { Id = 103, LocationId = 11, SiteContactOrder = 0, FirstName = "Removed Poc", PhoneNumber = "555-0004", IsDeleted = true }, new Contacts { Id = 104, LocationId = 12, SiteContactOrder = 0, FirstName = "Bob Quinn", PhoneNumber = "555-0005" }); await ctx.SaveChangesAsync(); } [Theory] [InlineData("STL", new[] { 10 }, "site code")] [InlineData("Borealis", new[] { 11, 13 }, "client name")] [InlineData("Prairie", new[] { 11 }, "street address")] [InlineData("Miami", new[] { 13 }, "city")] public async Task GetListPagedAsync_SearchMatchesCodeClientAddressAndCity(string term, int[] expectedIds, string becauseField) { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, totalCount) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, term, null, CancellationToken.None); totalCount.Should().Be(expectedIds.Length, $"search must match the {becauseField}"); items.Select(l => l.Id).Should().BeEquivalentTo(expectedIds); } [Theory] [InlineData("code", 11, 12, 13, 10)] [InlineData("client", 10, 12, 11, 13)] [InlineData("address", 13, 11, 12, 10)] [InlineData("city", 11, 12, 13, 10)] [InlineData("state", 13, 12, 10, 11)] [InlineData("poc", 13, 12, 10, 11)] public async Task GetListPagedAsync_SortByAllowlistedColumn_OrdersAscending_WithIdTiebreak( string sortBy, params int[] expectedIds) { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, _) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None, sortBy, "asc"); items.Select(l => l.Id).Should().ContainInOrder(expectedIds); } [Fact] public async Task GetListPagedAsync_SortByPoc_IgnoresDeletedContacts_AndSortsByFirstActiveContact() { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, _) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None, "poc", "asc"); // DFW8's order-0 row is soft deleted, so its POC is "Zoe Park" (order 1) and it must // sort last; including the deleted row ("Removed Poc") would instead place it second. items.Select(l => l.Id).Should().ContainInOrder(13, 12, 10, 11); } [Fact] public async Task GetListPagedAsync_SortByClient_Desc_ReversesAccountOrder() { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, _) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None, "client", "desc"); items.Select(l => l.Id).Should().ContainInOrder(new[] { 11, 13, 10, 12 }); } [Fact] public async Task GetListPagedAsync_UnknownSortColumn_FallsBackToLegacyCodeOrder() { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, _) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None, "not-a-column", "sideways"); items.Select(l => l.Name).Should().ContainInOrder("DFW8", "IND9", "MIA2", "STL8"); } [Fact] public async Task GetListPagedAsync_EqualSortValues_BreakTiesById_AcrossPages() { await using var ctx = NewContext(); ctx.Locations.AddRange( new Locations { Id = 20, Name = "HOU1", City = "Dallas" }, new Locations { Id = 21, Name = "AUS3", City = "Dallas" }, new Locations { Id = 22, Name = "ELP4", City = "Dallas" }); await ctx.SaveChangesAsync(); var service = new LocationDataService(ctx); var (page1, totalCount) = await service.GetListPagedAsync(1, 2, null, null, CancellationToken.None, "city", "asc"); var (page2, _) = await service.GetListPagedAsync(2, 2, null, null, CancellationToken.None, "city", "asc"); totalCount.Should().Be(3); page1.Select(l => l.Id).Should().ContainInOrder(20, 21); page2.Select(l => l.Id).Should().ContainInOrder(22); } [Fact] public async Task GetListPagedAsync_ListLoadIncludesAccountNameForProjection() { await using var ctx = NewContext(); await SeedRegistryAsync(ctx); var (items, _) = await new LocationDataService(ctx) .GetListPagedAsync(1, 10, null, null, CancellationToken.None); items.Single(l => l.Id == 10).Account!.Name.Should().Be("Acme Co"); } }