using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Moq; using PowderCoating.Application.DTOs.Job; using PowderCoating.Application.Interfaces; using PowderCoating.Application.Services; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Core.Interfaces; using PowderCoating.Infrastructure.Data; using PowderCoating.Infrastructure.Repositories; using PowderCoating.Web.Controllers; using PowderCoating.Web.Hubs; namespace PowderCoating.UnitTests; public class QuoteAndReworkControllerFlowTests { [Fact] public async Task UpdateQuoteStatus_ApprovedQuote_CopiesItemLevelFieldsIntoCreatedJob() { await using var context = CreateContext(); SeedQuoteConversionData(context); await context.SaveChangesAsync(); var quoteStatuses = await context.QuoteStatusLookups.OrderBy(s => s.Id).ToListAsync(); var lookupCache = new Mock(); lookupCache .Setup(x => x.GetQuoteStatusLookupsAsync(1)) .ReturnsAsync(quoteStatuses); var tenantContext = CreateTenantContext(); var controller = new QuotesController( new UnitOfWork(context), Mock.Of(), Mock.Of(), CreateUserManager().Object, Mock.Of>(), Mock.Of(), tenantContext.Object, Mock.Of(), lookupCache.Object, Mock.Of(), Mock.Of(), new JobItemAssemblyService(), Mock.Of(), new ConfigurationBuilder().Build(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); var result = await controller.UpdateQuoteStatus(new UpdateQuoteStatusRequest { QuoteId = 1, StatusId = 2 }); Assert.IsType(result); var quote = await context.Quotes.SingleAsync(); Assert.Equal(3, quote.QuoteStatusId); Assert.True(quote.ConvertedToJobId.HasValue); var job = await context.Jobs.SingleAsync(); Assert.Equal(quote.Id, job.QuoteId); var jobItem = await context.JobItems.SingleAsync(); Assert.True(jobItem.IsSalesItem); Assert.Equal("MUG-01", jobItem.Sku); Assert.False(jobItem.IncludePrepCost); var jobCoat = await context.JobItemCoats.SingleAsync(); Assert.Equal(1.5m, jobCoat.PowderToOrder); Assert.Equal(50, jobCoat.InventoryItemId); var jobItemPrep = await context.JobItemPrepServices.SingleAsync(); Assert.Equal(77, jobItemPrep.BlastSetupId); var jobPrep = await context.JobPrepServices.SingleAsync(); Assert.Equal(5, jobPrep.PrepServiceId); } [Fact] public async Task AddReworkRecord_CopiesFullItemShapeToReworkJob() { await using var context = CreateContext(); SeedReworkData(context); await context.SaveChangesAsync(); var lookupCache = new Mock(); lookupCache .Setup(x => x.GetJobStatusLookupsAsync(1)) .ReturnsAsync(await context.JobStatusLookups.ToListAsync()); lookupCache .Setup(x => x.GetJobPriorityLookupsAsync(1)) .ReturnsAsync(await context.JobPriorityLookups.ToListAsync()); var tenantContext = CreateTenantContext(); var mapper = new Mock(); mapper .Setup(x => x.Map(It.IsAny())) .Returns(source => { var record = Assert.IsType(source); return new ReworkRecordDto { Id = record.Id, JobId = record.JobId, JobItemId = record.JobItemId, ReworkJobId = record.ReworkJobId }; }); var controller = new JobsController( new UnitOfWork(context), mapper.Object, Mock.Of(), CreateUserManager().Object, Mock.Of>(), tenantContext.Object, Mock.Of(), lookupCache.Object, Mock.Of(), Mock.Of(), Mock.Of(), new JobItemAssemblyService(), Mock.Of>(), Mock.Of>()); var result = await controller.AddReworkRecord(new CreateReworkRecordDto { JobId = 1, JobItemId = 10, ReworkType = ReworkType.InternalDefect, Reason = ReworkReason.InsufficientCoverage, DefectDescription = "Thin coverage on one edge", DiscoveredBy = ReworkDiscoveredBy.Internal, DiscoveredDate = new DateTime(2026, 5, 9), EstimatedReworkCost = 65m }); Assert.IsType(result); var reworkJob = await context.Jobs.SingleAsync(j => j.IsReworkJob); Assert.Equal(1, reworkJob.OriginalJobId); var reworkItem = await context.JobItems.SingleAsync(i => i.JobId == reworkJob.Id); Assert.True(reworkItem.IsSalesItem); Assert.Equal("GATE-BRZ", reworkItem.Sku); Assert.False(reworkItem.IncludePrepCost); Assert.Equal(140m, reworkItem.UnitPrice); Assert.Equal(140m, reworkItem.TotalPrice); var reworkCoat = await context.JobItemCoats.SingleAsync(c => c.JobItemId == reworkItem.Id); Assert.Equal(2.75m, reworkCoat.PowderToOrder); var reworkPrep = await context.JobItemPrepServices.SingleAsync(p => p.JobItemId == reworkItem.Id); Assert.Equal(88, reworkPrep.BlastSetupId); } private static void SeedQuoteConversionData(ApplicationDbContext context) { context.Customers.Add(new Customer { Id = 1, CompanyId = 1, CompanyName = "Acme Fabrication" }); context.InventoryItems.Add(new InventoryItem { Id = 50, CompanyId = 1, SKU = "POW-1", Name = "Gloss Black", ColorCode = "RAL9005", Finish = "Gloss", Category = "Powder", UnitOfMeasure = "lbs" }); context.QuoteStatusLookups.AddRange( new QuoteStatusLookup { Id = 1, CompanyId = 1, StatusCode = "DRAFT", DisplayName = "Draft" }, new QuoteStatusLookup { Id = 2, CompanyId = 1, StatusCode = "APPROVED", DisplayName = "Approved" }, new QuoteStatusLookup { Id = 3, CompanyId = 1, StatusCode = "CONVERTED", DisplayName = "Converted" }); context.JobStatusLookups.Add(new JobStatusLookup { Id = 10, CompanyId = 1, StatusCode = "APPROVED", DisplayName = "Approved" }); context.JobPriorityLookups.AddRange( new JobPriorityLookup { Id = 20, CompanyId = 1, PriorityCode = "NORMAL", DisplayName = "Normal" }, new JobPriorityLookup { Id = 21, CompanyId = 1, PriorityCode = "RUSH", DisplayName = "Rush" }); context.PrepServices.Add(new PrepService { Id = 5, CompanyId = 1, ServiceName = "Sandblasting", DisplayOrder = 1, IsActive = true, RequiresBlastSetup = true }); context.Quotes.Add(new Quote { Id = 1, CompanyId = 1, QuoteNumber = "Q-1001", CustomerId = 1, QuoteStatusId = 1, Total = 50m, ShopSuppliesAmount = 2m, ShopSuppliesPercent = 4m }); context.QuoteItems.Add(new QuoteItem { Id = 100, QuoteId = 1, CompanyId = 1, Description = "Merch mug", Quantity = 2m, SurfaceAreaSqFt = 10m, IsSalesItem = true, Sku = "MUG-01", UnitPrice = 25m, TotalPrice = 50m, IncludePrepCost = false, EstimatedMinutes = 12 }); context.QuoteItemCoats.Add(new QuoteItemCoat { Id = 101, QuoteItemId = 100, CompanyId = 1, CoatName = "Top Coat", Sequence = 1, InventoryItemId = 50, ColorName = "Old Name", ColorCode = "OLD", Finish = "Old", CoverageSqFtPerLb = 30m, TransferEfficiency = 65m, PowderToOrder = 1.5m }); context.QuoteItemPrepServices.Add(new QuoteItemPrepService { Id = 102, QuoteItemId = 100, CompanyId = 1, PrepServiceId = 5, EstimatedMinutes = 9, BlastSetupId = 77 }); } private static void SeedReworkData(ApplicationDbContext context) { context.Customers.Add(new Customer { Id = 1, CompanyId = 1, CompanyName = "Acme Fabrication" }); context.JobStatusLookups.Add(new JobStatusLookup { Id = 1, CompanyId = 1, StatusCode = "PENDING", DisplayName = "Pending" }); context.JobPriorityLookups.Add(new JobPriorityLookup { Id = 2, CompanyId = 1, PriorityCode = "NORMAL", DisplayName = "Normal" }); context.Jobs.Add(new Job { Id = 1, CompanyId = 1, JobNumber = "JOB-2605-0001", Description = "Original gate job", CustomerId = 1, JobStatusId = 1, JobPriorityId = 2 }); context.JobItems.Add(new JobItem { Id = 10, JobId = 1, CompanyId = 1, Description = "Gate panel", Quantity = 1m, SurfaceArea = 22m, SurfaceAreaSqFt = 22m, CatalogItemId = 8, IsSalesItem = true, Sku = "GATE-BRZ", UnitPrice = 140m, TotalPrice = 140m, LaborCost = 56m, IncludePrepCost = false, EstimatedMinutes = 90, ColorName = "Bronze", ColorCode = "BZ-22", Finish = "Textured" }); context.JobItemCoats.Add(new JobItemCoat { Id = 11, JobItemId = 10, CompanyId = 1, CoatName = "Top Coat", Sequence = 1, CoverageSqFtPerLb = 26m, TransferEfficiency = 70m, PowderToOrder = 2.75m }); context.JobItemPrepServices.Add(new JobItemPrepService { Id = 12, JobItemId = 10, CompanyId = 1, PrepServiceId = 3, EstimatedMinutes = 45, BlastSetupId = 88 }); } private static Mock> CreateUserManager() { var store = new Mock>(); return new Mock>( store.Object, null!, null!, null!, null!, null!, null!, null!, null!); } private static Mock CreateTenantContext() { var tenantContext = new Mock(); tenantContext.Setup(x => x.GetCurrentCompanyId()).Returns(1); tenantContext.Setup(x => x.IsSuperAdmin()).Returns(true); tenantContext.Setup(x => x.IsPlatformAdmin()).Returns(true); return tenantContext; } private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; var identity = new ClaimsIdentity( [new Claim(ClaimTypes.Role, "SuperAdmin")], "Test"); var principal = new ClaimsPrincipal(identity); byte[]? noBytes = null; var sessionMock = new Mock(); sessionMock.Setup(s => s.TryGetValue(It.IsAny(), out noBytes)).Returns(false); var httpContextMock = new Mock(); httpContextMock.SetupGet(c => c.User).Returns(principal); httpContextMock.SetupGet(c => c.Session).Returns(sessionMock.Object); var accessor = new Mock(); accessor.SetupGet(a => a.HttpContext).Returns(httpContextMock.Object); return new ApplicationDbContext(options, accessor.Object, null!); } }