using ClosedXML.Excel; using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Formatting; 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; /// /// Tests comparing generated output against legacy format specifications. /// These tests verify column order, format strings, and protection settings /// match the legacy ExcelWriter.cs implementation. /// public class LegacyComparisonTests { private readonly ExcelExportService _service; public LegacyComparisonTests() { var logger = Substitute.For>(); var options = Microsoft.Extensions.Options.Options.Create(new ExcelExportOptions { CriteriaSheetPassword = "JDE_SCOPING_TOOL_PASS", DataSheetPassword = "JDESCOPINGTOOL" }); var cache = new OutputColumnCache(); var tableWriter = new AttributeTableWriter(cache); var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter); _service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter); } #region Search Results Column Order Tests /// /// Verifies Search Results columns match legacy order per ExcelWriter.cs lines 197-218. /// Legacy order: Work Order Number, Work Order Branch Code, Lot Number, Item Number, /// Planning Family, Order Quantity, Held Quantity, Scrapped Quantity, Shipped Quantity, /// Operation Step Branch Code, Operation Step, Operation Step Description, /// Function Operation Description, Operation Step Update Timestamp, Status Code, /// Status Description, Status Update Timestamp, Inclusion Reason /// /// Note: The new implementation adds "Stocking Type" after "Planning Family" per spec. /// [Fact] public async Task SearchResults_ColumnOrder_MatchesLegacyWithEnhancements() { var search = CreateSearchModelWithResults(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("Search Results"); // Verify key column positions (0-indexed from GetHeadersFromSheet) var headers = GetHeadersFromSheet(sheet); headers[0].ShouldBe("Work Order Number"); headers[1].ShouldBe("Work Order Branch Code"); headers[2].ShouldBe("Lot Number"); headers[3].ShouldBe("Item Number"); headers[4].ShouldBe("Planning Family"); // New column: Stocking Type (not in legacy) headers[5].ShouldBe("Stocking Type"); headers[6].ShouldBe("Order Quantity"); headers[7].ShouldBe("Held Quantity"); headers[8].ShouldBe("Scrapped Quantity"); headers[9].ShouldBe("Shipped Quantity"); headers[10].ShouldBe("Operation Step Branch Code"); headers[11].ShouldBe("Operation Step"); headers[12].ShouldBe("Operation Step Description"); headers[13].ShouldBe("Function Operation Description"); headers[14].ShouldBe("Operation Step Update Timestamp"); headers[15].ShouldBe("Status Code"); headers[16].ShouldBe("Status Description"); headers[17].ShouldBe("Status Update Timestamp"); headers[18].ShouldBe("Inclusion Reason"); } #endregion #region MIS Info Column Order Tests /// /// Verifies MIS Info columns match expected order per spec. /// Legacy order per ExcelWriter.cs lines 299-330: /// Item Number, Item Description, MIS Job Step Sequence Number, MIS Number, MIS Revision, /// MIS Release Status, MIS Release Date, Branch Code, Job Step Sequence Number, /// Matched Sequence Number, Matched to F3112Z1?, Matched to F3003?, /// Function Operation Description, Char Number, Test Description, /// Sampling Type, Sampling Value, Tools & Gauges, Work Instructions /// /// New implementation reorders to match attribute Order values. /// [Fact] public async Task MisInfo_ColumnOrder_MatchesSpec() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("MIS Info"); var headers = GetHeadersFromSheet(sheet); // Verify column order matches OutputColumn Order attributes headers[0].ShouldBe("Item Number"); headers[1].ShouldBe("MIS Job Step Sequence Number"); headers[2].ShouldBe("MIS Number"); headers[3].ShouldBe("MIS Revision"); headers[4].ShouldBe("Item Description"); headers[5].ShouldBe("MIS Release Status"); headers[6].ShouldBe("MIS Release Date"); headers[7].ShouldBe("Branch Code"); headers[8].ShouldBe("Job Step Sequence Number"); headers[9].ShouldBe("Matched Sequence Number"); headers[10].ShouldBe("Matched to F3112Z1?"); headers[11].ShouldBe("Matched to F3003?"); headers[12].ShouldBe("Function Operation Description"); headers[13].ShouldBe("Char Number"); headers[14].ShouldBe("Test Description"); headers[15].ShouldBe("Sampling Type"); headers[16].ShouldBe("Sampling Value"); headers[17].ShouldBe("Tools & Gauges"); headers[18].ShouldBe("Work Instructions"); } #endregion #region Investigation Column Order Tests /// /// Verifies Investigation columns match expected order per spec. /// Legacy order per ExcelWriter.cs lines 403-418: /// Work Center Code, Work Order Number, Work Order Start Date, Job Step Number, /// Function Operation Description, Job Step End Date, Function Code, /// Item Number, Item Description, Routing Type /// /// New implementation adds: Was Job Step Added?, Matched Job Step Number /// [Fact] public async Task Investigation_ColumnOrder_MatchesSpecWithEnhancements() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("Investigation"); var headers = GetHeadersFromSheet(sheet); headers[0].ShouldBe("Work Center Code"); headers[1].ShouldBe("Work Order Number"); headers[2].ShouldBe("Work Order Start Date"); headers[3].ShouldBe("Job Step Number"); headers[4].ShouldBe("Function Operation Description"); headers[5].ShouldBe("Job Step End Date"); headers[6].ShouldBe("Function Code"); // New columns per spec headers[7].ShouldBe("Was Job Step Added?"); headers[8].ShouldBe("Matched Job Step Number"); headers[9].ShouldBe("Item Number"); headers[10].ShouldBe("Item Description"); headers[11].ShouldBe("Routing Type"); } #endregion #region Format String Tests /// /// Verifies timestamp format matches legacy TIMESTAMP_FORMAT = "[$-409]m/d/yy h:mm AM/PM;@" /// per ExcelWriter.cs line 26. /// [Fact] public void TimestampFormat_MatchesLegacy() { ExcelFormats.TimestampFormat.ShouldBe("[$-409]m/d/yy h:mm AM/PM;@"); } /// /// Verifies date format uses locale-aware format. /// Legacy used "m/d/yyyy" for Investigation sheet dates. /// [Fact] public void DateFormat_MatchesLegacyPattern() { // Legacy used "m/d/yyyy", new implementation uses "[$-409]MM/dd/yyyy;@" // Both produce similar output, the new format includes locale specifier ExcelFormats.DateFormat.ShouldContain("MM/dd/yyyy"); } /// /// Verifies wrapped column width matches legacy WRAPPED_CELL_WIDTH = 65 /// per ExcelWriter.cs line 31. /// [Fact] public void WrappedColumnWidth_MatchesLegacy() { ExcelFormats.WrappedColumnWidth.ShouldBe(65); } /// /// Verifies criteria sheet padding factor matches legacy 1.15 (15%) /// per ExcelWriter.cs line 175. /// [Fact] public void CriteriaPaddingFactor_MatchesLegacy() { ExcelFormats.CriteriaPaddingFactor.ShouldBe(1.15); } /// /// Verifies data sheet padding factor matches legacy 1.3 (30%) /// per ExcelWriter.cs lines 251, 367, 442. /// [Fact] public void DataPaddingFactor_MatchesLegacy() { ExcelFormats.DataPaddingFactor.ShouldBe(1.30); } #endregion #region Protection Settings Tests /// /// Verifies criteria sheet uses correct password per ExcelWriter.cs line 179. /// Legacy password: "JDE_SCOPING_TOOL_PASS" /// [Fact] public async Task CriteriaSheet_Protection_IsEnabled() { var search = CreateSearchModelWithResults(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("Search Criteria"); sheet.Protection.IsProtected.ShouldBeTrue(); } /// /// Verifies data sheets use correct password per ExcelWriter.cs line 277, 496. /// Legacy password: "JDESCOPINGTOOL" /// [Fact] public async Task DataSheets_Protection_IsEnabled() { var search = CreateSearchModelWithMisData(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheet("Search Results").Protection.IsProtected.ShouldBeTrue(); workbook.Worksheet("MIS Info").Protection.IsProtected.ShouldBeTrue(); workbook.Worksheet("Investigation").Protection.IsProtected.ShouldBeTrue(); } /// /// Verifies protection allows filtering per legacy settings. /// Legacy: AllowAutoFilter = true (line 268) /// [Fact] public async Task DataSheets_Protection_AllowsFiltering() { var search = CreateSearchModelWithMisData(); 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.AllowedElements.HasFlag(XLSheetProtectionElements.AutoFilter).ShouldBeTrue(); } /// /// Verifies protection allows sorting per legacy settings. /// Legacy: AllowSort = true (line 276) /// [Fact] public async Task DataSheets_Protection_AllowsSorting() { var search = CreateSearchModelWithMisData(); 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.AllowedElements.HasFlag(XLSheetProtectionElements.Sort).ShouldBeTrue(); } /// /// Verifies protection allows formatting per legacy settings. /// Legacy: AllowFormatCells, AllowFormatColumns, AllowFormatRows = true (lines 270-272) /// [Fact] public async Task DataSheets_Protection_AllowsFormatting() { var search = CreateSearchModelWithMisData(); 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.AllowedElements.HasFlag(XLSheetProtectionElements.FormatCells).ShouldBeTrue(); resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatColumns).ShouldBeTrue(); resultsSheet.Protection.AllowedElements.HasFlag(XLSheetProtectionElements.FormatRows).ShouldBeTrue(); } #endregion #region Table Style Tests /// /// Note: Legacy used TableStyles.Medium1 per ExcelWriter.cs line 261. /// New implementation uses Light18 per spec requirement. /// This is an intentional change documented in the spec. /// [Fact] public async Task DataSheets_UseLight18TableStyle_PerSpec() { var search = CreateSearchModelWithMisData(); 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(); // Spec specifies Light18, legacy used Medium1 table.Theme.ShouldBe(XLTableTheme.TableStyleLight18); } #endregion #region Timestamp Formatting Tests /// /// Verifies criteria sheet timestamp format matches legacy. /// Legacy format per line 98: "{searchModel.SubmitDT:MMM dd, yyyy hh:mm:ss tt} EST" /// [Fact] public async Task CriteriaSheet_TimestampFormat_MatchesLegacy() { var search = CreateSearchModelWithResults(); search.SubmitDt = new DateTime(2024, 1, 15, 14, 30, 45); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("Search Criteria"); var submitTimestamp = sheet.Cell(4, 2).Value.GetText(); submitTimestamp.ShouldContain("Jan 15, 2024"); submitTimestamp.ShouldContain("02:30:45"); submitTimestamp.ShouldContain("EST"); } #endregion #region Header Formatting Tests /// /// Verifies header cell formatting matches legacy ApplyHeaderFormat. /// Legacy per lines 467-476: Bold, centered, Gainsboro background. /// [Fact] public async Task Headers_Formatting_MatchesLegacy() { var search = CreateSearchModelWithResults(); var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); var sheet = workbook.Worksheet("Search Criteria"); // Check "Search Name" header cell var headerCell = sheet.Cell(1, 1); headerCell.Style.Font.Bold.ShouldBeTrue(); headerCell.Style.Fill.BackgroundColor.ShouldBe(XLColor.Gainsboro); headerCell.Style.Alignment.Horizontal.ShouldBe(XLAlignmentHorizontalValues.Center); } #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 CreateSearchModelWithResults() { return new SearchModel { Id = 1, Name = "Legacy Comparison Test", UserName = "testuser", SubmitDt = DateTime.Now.AddHours(-1), StartDt = DateTime.Now.AddMinutes(-30), EndDt = DateTime.Now, ExtractMisData = false, Results = [ 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", StepUpdateDt = DateTime.Now, StatusCode = "50", StatusDescription = "In Progress", Flagged = true } ] }; } private static SearchModel CreateSearchModelWithMisData() { var model = CreateSearchModelWithResults(); model.ExtractMisData = true; model.MisResults = [ new MisSearchResult { ItemNumber = "ITEM-001", SequenceNumber = "010", MisNumber = "MIS-001", RevId = "A", ItemDescription = "Test Item", Status = "Released", ReleaseDate = DateTime.Now.AddDays(-30), BranchCode = "001", JobStepSequenceNumber = 10, MatchedSequenceNumber = 10, RoutingMatch = true, MasterMatch = true, FunctionOperationDescription = "Assembly operation", CharNumber = "001", TestDescription = "Sample test description", SamplingType = "100%", SamplingValue = "1", ToolsGauges = "Gauge A, Gauge B", WorkInstructions = "Step 1: Do this. Step 2: Do that." } ]; model.MisNonMatchResults = [ new MisNonMatchSearchResult { WorkCenterCode = "WC01", WorkOrderNumber = 12345, WorkOrderStartDate = DateTime.Now.AddDays(-7), JobStepNumber = 10, JobStepDescription = "Test operation", JobStepEndDate = DateTime.Now.AddDays(-5), FunctionCode = "FC01", WasJobStepAdded = false, MatchedJobStepNumber = 10, ItemNumber = "ITEM-001", ItemDescription = "Test Item Description", RoutingType = "M" } ]; return model; } #endregion }