using ClosedXML.Excel; using JdeScoping.ExcelIO.Options; 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; public class ExcelExportServiceTests { private readonly ExcelExportService _service; private readonly ILogger _logger; private readonly IOptions _options; public ExcelExportServiceTests() { _logger = Substitute.For>(); _options = Microsoft.Extensions.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); } [Fact] public async Task GenerateAsync_ReturnsValidExcelBytes() { var search = CreateMinimalSearchModel(); var result = await _service.GenerateAsync(search); result.ShouldNotBeNull(); result.Length.ShouldBeGreaterThan(0); // Verify it's a valid Excel file using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.Count.ShouldBeGreaterThanOrEqualTo(2); } [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 var criteriaSheet).ShouldBeTrue(); criteriaSheet.ShouldNotBeNull(); } [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 var resultsSheet).ShouldBeTrue(); resultsSheet.ShouldNotBeNull(); } [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.Count.ShouldBe(4); // Criteria, Results, MIS Info, Investigation workbook.Worksheets.TryGetWorksheet("MIS Info", out var misSheet).ShouldBeTrue(); misSheet.ShouldNotBeNull(); } [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 var investigationSheet).ShouldBeTrue(); investigationSheet.ShouldNotBeNull(); } [Fact] public async Task GenerateAsync_WithoutMisData_DoesNotCreateMisSheets() { var search = CreateMinimalSearchModel(); search.ExtractMisData = false; var result = await _service.GenerateAsync(search); using var stream = new MemoryStream(result); using var workbook = new XLWorkbook(stream); workbook.Worksheets.Count.ShouldBe(2); // Only Criteria and Results } [Fact] public async Task GenerateAsync_CancellationRequested_ThrowsOperationCanceled() { var search = CreateMinimalSearchModel(); var cts = new CancellationTokenSource(); cts.Cancel(); await Should.ThrowAsync( () => _service.GenerateAsync(search, cts.Token)); } [Fact] public async Task GenerateAsync_CriteriaSheet_ContainsSearchName() { var search = CreateMinimalSearchModel(); search.Name = "Test Search Name"; 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.Cell(1, 2).Value.GetText().ShouldBe("Test Search Name"); } [Fact] public async Task GenerateAsync_CriteriaSheet_ContainsUserName() { var search = CreateMinimalSearchModel(); search.UserName = "testuser"; 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.Cell(2, 2).Value.GetText().ShouldBe("testuser"); } [Fact] public async Task GenerateAsync_ResultsSheet_ContainsResultData() { var search = CreateMinimalSearchModel(); search.Results.Add(new SearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001", LotNumber = "LOT-001", 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"); // Check header row resultsSheet.Cell(1, 1).Value.GetText().ShouldBe("Work Order Number"); // Check data row resultsSheet.Cell(2, 1).Value.GetNumber().ShouldBe(12345); } private static SearchModel CreateMinimalSearchModel() { return new SearchModel { Id = 1, Name = "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 = "Test Search with MIS", UserName = "testuser", SubmitDt = DateTime.Now.AddHours(-1), StartDt = DateTime.Now.AddMinutes(-30), EndDt = DateTime.Now, ExtractMisData = true, Results = [ new SearchResult { WorkOrderNumber = 12345, Flagged = true } ], MisResults = [ new MisSearchResult { ItemNumber = "ITEM-001", MisNumber = "MIS-001" } ], MisNonMatchResults = [ new MisNonMatchSearchResult { WorkOrderNumber = 12345, ItemNumber = "ITEM-001" } ] }; } }