using ClosedXML.Excel; using JdeScoping.ExcelIO.Configuration; using JdeScoping.ExcelIO.Generators; using JdeScoping.ExcelIO.Helpers; using JdeScoping.ExcelIO.Models.Reporting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using Shouldly; using Xunit; namespace JdeScoping.ExcelIO.Tests; /// /// Integration tests that generate actual .xlsx files and verify structure with ClosedXML. /// public class ExcelExportIntegrationTests { private readonly ExcelExportService _service; private readonly ILogger _logger; private readonly IOptions _options; public ExcelExportIntegrationTests() { _logger = Substitute.For>(); _options = Options.Create(new ExcelExportOptions { CriteriaSheetPassword = "TestCriteriaPass", DataSheetPassword = "TestDataPass" }); var cache = new OutputColumnCache(); var tableWriter = new AttributeTableWriter(cache); var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter); _service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter); } #region Sheet Count Tests [Fact] public async Task GenerateAsync_MinimalSearch_HasTwoSheets() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.Count.ShouldBe(2); } [Fact] public async Task GenerateAsync_WithMisData_HasFourSheets() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.Count.ShouldBe(4); } #endregion #region Sheet Name Tests [Fact] public async Task GenerateAsync_CreatesSearchCriteriaSheet() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.TryGetWorksheet("Search Criteria", out _).ShouldBeTrue(); } [Fact] public async Task GenerateAsync_CreatesSearchResultsSheet() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.TryGetWorksheet("Search Results", out _).ShouldBeTrue(); } [Fact] public async Task GenerateAsync_WithMisData_CreatesMisInfoSheet() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.TryGetWorksheet("MIS Info", out _).ShouldBeTrue(); } [Fact] public async Task GenerateAsync_WithMisData_CreatesInvestigationSheet() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.TryGetWorksheet("Investigation", out _).ShouldBeTrue(); } #endregion #region Column Header Tests - Search Results [Fact] public async Task GenerateAsync_SearchResultsSheet_Has19ColumnHeaders() { var search = CreateMinimalSearchModel(); search.Results.Add(CreateSampleSearchResult()); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); // Count non-empty cells in first row to get column count var headers = GetHeadersFromSheet(resultsSheet); headers.Count.ShouldBe(19); } [Fact] public async Task GenerateAsync_SearchResultsSheet_HasCorrectColumnHeaders() { var search = CreateMinimalSearchModel(); search.Results.Add(CreateSampleSearchResult()); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); var headers = GetHeadersFromSheet(resultsSheet); headers.ShouldContain("Work Order Number"); headers.ShouldContain("Work Order Branch Code"); headers.ShouldContain("Lot Number"); headers.ShouldContain("Item Number"); headers.ShouldContain("Planning Family"); headers.ShouldContain("Stocking Type"); headers.ShouldContain("Order Quantity"); headers.ShouldContain("Held Quantity"); headers.ShouldContain("Scrapped Quantity"); headers.ShouldContain("Shipped Quantity"); headers.ShouldContain("Operation Step Branch Code"); headers.ShouldContain("Operation Step"); headers.ShouldContain("Operation Step Description"); headers.ShouldContain("Function Operation Description"); headers.ShouldContain("Operation Step Update Timestamp"); headers.ShouldContain("Status Code"); headers.ShouldContain("Status Description"); headers.ShouldContain("Status Update Timestamp"); headers.ShouldContain("Inclusion Reason"); } #endregion #region Column Header Tests - MIS Info [Fact] public async Task GenerateAsync_MisInfoSheet_Has19ColumnHeaders() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); var headers = GetHeadersFromSheet(misSheet); headers.Count.ShouldBe(19); } [Fact] public async Task GenerateAsync_MisInfoSheet_HasCorrectColumnHeaders() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); var headers = GetHeadersFromSheet(misSheet); headers.ShouldContain("Item Number"); headers.ShouldContain("MIS Job Step Sequence Number"); headers.ShouldContain("MIS Number"); headers.ShouldContain("MIS Revision"); headers.ShouldContain("Item Description"); headers.ShouldContain("MIS Release Status"); headers.ShouldContain("MIS Release Date"); headers.ShouldContain("Branch Code"); headers.ShouldContain("Job Step Sequence Number"); headers.ShouldContain("Matched Sequence Number"); headers.ShouldContain("Matched to F3112Z1?"); headers.ShouldContain("Matched to F3003?"); headers.ShouldContain("Function Operation Description"); headers.ShouldContain("Char Number"); headers.ShouldContain("Test Description"); headers.ShouldContain("Sampling Type"); headers.ShouldContain("Sampling Value"); headers.ShouldContain("Tools & Gauges"); headers.ShouldContain("Work Instructions"); } #endregion #region Column Header Tests - Investigation [Fact] public async Task GenerateAsync_InvestigationSheet_Has12ColumnHeaders() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var investigationSheet = workbook.Worksheet("Investigation"); var headers = GetHeadersFromSheet(investigationSheet); headers.Count.ShouldBe(12); } [Fact] public async Task GenerateAsync_InvestigationSheet_HasCorrectColumnHeaders() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var investigationSheet = workbook.Worksheet("Investigation"); var headers = GetHeadersFromSheet(investigationSheet); headers.ShouldContain("Work Center Code"); headers.ShouldContain("Work Order Number"); headers.ShouldContain("Work Order Start Date"); headers.ShouldContain("Job Step Number"); headers.ShouldContain("Function Operation Description"); headers.ShouldContain("Job Step End Date"); headers.ShouldContain("Function Code"); headers.ShouldContain("Was Job Step Added?"); headers.ShouldContain("Matched Job Step Number"); headers.ShouldContain("Item Number"); headers.ShouldContain("Item Description"); headers.ShouldContain("Routing Type"); } #endregion #region Table Style Tests [Fact] public async Task GenerateAsync_SearchResultsTable_UsesLight18Style() { var search = CreateMinimalSearchModel(); search.Results.Add(CreateSampleSearchResult()); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); var table = resultsSheet.Tables.First(); table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); } [Fact] public async Task GenerateAsync_MisInfoTable_UsesLight18Style() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); var table = misSheet.Tables.First(); table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); } [Fact] public async Task GenerateAsync_InvestigationTable_UsesLight18Style() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var investigationSheet = workbook.Worksheet("Investigation"); var table = investigationSheet.Tables.First(); table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); } #endregion #region Protection Tests [Fact] public async Task GenerateAsync_SearchCriteriaSheet_IsProtected() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var criteriaSheet = workbook.Worksheet("Search Criteria"); criteriaSheet.Protection.IsProtected.ShouldBeTrue(); } [Fact] public async Task GenerateAsync_SearchResultsSheet_IsProtected() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); resultsSheet.Protection.IsProtected.ShouldBeTrue(); } [Fact] public async Task GenerateAsync_MisInfoSheet_IsProtected() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); misSheet.Protection.IsProtected.ShouldBeTrue(); } [Fact] public async Task GenerateAsync_InvestigationSheet_IsProtected() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var investigationSheet = workbook.Worksheet("Investigation"); investigationSheet.Protection.IsProtected.ShouldBeTrue(); } #endregion #region Data Validation Tests [Fact] public async Task GenerateAsync_SearchResults_ContainsCorrectData() { var search = CreateMinimalSearchModel(); var searchResult = CreateSampleSearchResult(); searchResult.WorkOrderNumber = 99999; searchResult.ItemNumber = "TEST-ITEM-001"; searchResult.LotNumber = "LOT-999"; search.Results.Add(searchResult); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); // Data should be in row 2 (after header) resultsSheet.Cell(2, 1).Value.GetNumber().ShouldBe(99999); resultsSheet.Cell(2, 3).Value.GetText().ShouldBe("LOT-999"); resultsSheet.Cell(2, 4).Value.GetText().ShouldBe("TEST-ITEM-001"); } [Fact] public async Task GenerateAsync_MisInfo_ContainsCorrectData() { var search = CreateSearchModelWithMisData(); search.MisResults![0].ItemNumber = "MIS-ITEM-001"; search.MisResults[0].MisNumber = "MIS-12345"; var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); // Data should be in row 2 (after header) misSheet.Cell(2, 1).Value.GetText().ShouldBe("MIS-ITEM-001"); misSheet.Cell(2, 3).Value.GetText().ShouldBe("MIS-12345"); } [Fact] public async Task GenerateAsync_Investigation_ContainsCorrectData() { var search = CreateSearchModelWithMisData(); search.MisNonMatchResults![0].WorkOrderNumber = 77777; search.MisNonMatchResults[0].ItemNumber = "INV-ITEM-001"; var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var investigationSheet = workbook.Worksheet("Investigation"); // Data should be in row 2 (after header) investigationSheet.Cell(2, 2).Value.GetNumber().ShouldBe(77777); investigationSheet.Cell(2, 10).Value.GetText().ShouldBe("INV-ITEM-001"); } #endregion #region Wrapped Column Tests [Fact] public async Task GenerateAsync_MisInfo_TestDescriptionColumn_IsWrapped() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); // Find the Test Description column (should be column 15) var headers = GetHeadersFromSheet(misSheet); var testDescColIndex = headers.IndexOf("Test Description") + 1; misSheet.Column(testDescColIndex).Width.ShouldBe(65); misSheet.Column(testDescColIndex).Style.Alignment.WrapText.ShouldBeTrue(); } [Fact] public async Task GenerateAsync_MisInfo_ToolsGaugesColumn_IsWrapped() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); var headers = GetHeadersFromSheet(misSheet); var toolsGaugesColIndex = headers.IndexOf("Tools & Gauges") + 1; misSheet.Column(toolsGaugesColIndex).Width.ShouldBe(65); misSheet.Column(toolsGaugesColIndex).Style.Alignment.WrapText.ShouldBeTrue(); } [Fact] public async Task GenerateAsync_MisInfo_WorkInstructionsColumn_IsWrapped() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var misSheet = workbook.Worksheet("MIS Info"); var headers = GetHeadersFromSheet(misSheet); var workInstructionsColIndex = headers.IndexOf("Work Instructions") + 1; misSheet.Column(workInstructionsColIndex).Width.ShouldBe(65); misSheet.Column(workInstructionsColIndex).Style.Alignment.WrapText.ShouldBeTrue(); } #endregion #region Large Data Set Tests [Fact] public async Task GenerateAsync_LargeDataSet_GeneratesSuccessfully() { var search = CreateMinimalSearchModel(); // Add 1000 search results for (int i = 0; i < 1000; i++) { search.Results.Add(new SearchResult { WorkOrderNumber = 10000 + i, ItemNumber = $"ITEM-{i:D5}", LotNumber = $"LOT-{i:D5}", Flagged = true }); } var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var resultsSheet = workbook.Worksheet("Search Results"); // Should have 1001 rows (1 header + 1000 data rows) var table = resultsSheet.Tables.First(); table.RowCount().ShouldBe(1001); } #endregion #region Helper Methods private static List GetHeadersFromSheet(IXLWorksheet sheet) { var headers = new List(); var col = 1; while (!sheet.Cell(1, col).IsEmpty()) { headers.Add(sheet.Cell(1, col).Value.GetText()); col++; } return headers; } private static SearchModel CreateMinimalSearchModel() { return new SearchModel { Id = 1, Name = "Integration Test Search", UserName = "testuser", SubmitDt = DateTime.Now.AddHours(-1), StartDt = DateTime.Now.AddMinutes(-30), EndDt = DateTime.Now, ExtractMisData = false, Results = [] }; } private static SearchModel CreateSearchModelWithMisData() { return new SearchModel { Id = 1, Name = "Integration Test Search with MIS", UserName = "testuser", SubmitDt = DateTime.Now.AddHours(-1), StartDt = DateTime.Now.AddMinutes(-30), EndDt = DateTime.Now, ExtractMisData = true, Results = [ CreateSampleSearchResult() ], MisResults = [ new MisSearchResult { ItemNumber = "ITEM-001", MisNumber = "MIS-001", RevId = "A", ItemDescription = "Test Item Description", Status = "Released", BranchCode = "001", JobStepSequenceNumber = 10, TestDescription = "Sample test description", ToolsGauges = "Sample tools and gauges", WorkInstructions = "Sample work instructions" } ], MisNonMatchResults = [ new MisNonMatchSearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001", WorkCenterCode = "WC01", WorkOrderStartDate = DateTime.Now.AddDays(-7), JobStepNumber = 10, JobStepDescription = "Test job step", FunctionCode = "FC01", RoutingType = "M" } ] }; } private static SearchResult CreateSampleSearchResult() { return new SearchResult { WorkOrderNumber = 12345, WorkOrderBranchCode = "001", LotNumber = "LOT-001", ItemNumber = "ITEM-001", PlanningFamily = "PF01", StockingType = "M", OrderQuantity = 100, HeldQuantity = 0, ScrappedQuantity = 0, ShippedQuantity = 50, StepBranchCode = "001", StepNumber = 10, StepDescription = "Assembly", FunctionOperationDescription = "Main assembly operation", StepUpdateDt = DateTime.Now.AddDays(-1), StatusCode = "50", StatusDescription = "In Progress", StatusUpdateDt = DateTime.Now.AddDays(-1), Flagged = true }; } #endregion }