using ClosedXML.Excel; using WorkTracker.Domain; using WorkTracker.Services.Exports; using Xunit; namespace WorkTracker.Tests; public sealed class MonthlyTimesheetExcelExporterTests { [Fact] public void Export_ForApril2026EmptyTimesheet_MatchesExpectedWorkbook() { var exporter = new MonthlyTimesheetExcelExporter(); var timesheet = CreateTimesheet(new DateOnly(2026, 4, 1), new HashSet { new(2026, 4, 6), new(2026, 4, 25) }); var templatePath = GetTemplatePath(); using var templateStream = File.OpenRead(templatePath); var workbookBytes = exporter.Export(timesheet, templateStream); WorkbookAssert.Equivalent(GetExpectedWorkbookPath(), workbookBytes); } [Fact] public void Export_ForThirtyOneDayMonth_ShiftsTotalColumnAfterLastDay() { var exporter = new MonthlyTimesheetExcelExporter(); var timesheet = CreateTimesheet(new DateOnly(2026, 5, 1), new HashSet()); var templatePath = GetTemplatePath(); using var templateStream = File.OpenRead(templatePath); var workbookBytes = exporter.Export(timesheet, templateStream); using var workbook = new XLWorkbook(new MemoryStream(workbookBytes)); var worksheet = workbook.Worksheet(1); Assert.Equal(31d, worksheet.Cell("AG1").GetDouble()); Assert.Equal("SUM(C2:AG2)", worksheet.Cell("AH2").FormulaA1); Assert.Equal("TOTALE", worksheet.Cell("AH1").GetString()); } [Fact] public void Export_ForMonthWhereSecondDayIsWeekend_OnlyHighlightsWeekendAndHolidayDays() { var exporter = new MonthlyTimesheetExcelExporter(); var timesheet = CreateTimesheet(new DateOnly(2026, 5, 1), new HashSet { new(2026, 5, 4) }); var templatePath = GetTemplatePath(); using var templateStream = File.OpenRead(templatePath); var workbookBytes = exporter.Export(timesheet, templateStream); using var workbook = new XLWorkbook(new MemoryStream(workbookBytes)); var worksheet = workbook.Worksheet(1); var headerRegularFill = GetFillSignature(worksheet.Cell("C1")); var headerSaturdayFill = GetFillSignature(worksheet.Cell("D1")); var headerSundayFill = GetFillSignature(worksheet.Cell("E1")); var headerHolidayFill = GetFillSignature(worksheet.Cell("F1")); var headerNextWeekdayFill = GetFillSignature(worksheet.Cell("G1")); Assert.NotEqual(headerRegularFill, headerSaturdayFill); Assert.NotEqual(headerRegularFill, headerSundayFill); Assert.Equal(headerSundayFill, headerHolidayFill); Assert.Equal(headerRegularFill, headerNextWeekdayFill); var bodyRegularFill = GetFillSignature(worksheet.Cell("C4")); var bodySaturdayFill = GetFillSignature(worksheet.Cell("D4")); var bodyHolidayFill = GetFillSignature(worksheet.Cell("F4")); var bodyNextWeekdayFill = GetFillSignature(worksheet.Cell("G4")); Assert.NotEqual(bodyRegularFill, bodySaturdayFill); Assert.Equal(bodySaturdayFill, bodyHolidayFill); Assert.Equal(bodyRegularFill, bodyNextWeekdayFill); } private static MonthlyTimesheetModel CreateTimesheet(DateOnly monthStart, ISet holidays) { var lastDay = monthStart.AddMonths(1).AddDays(-1); var days = new List(); for (var date = monthStart; date <= lastDay; date = date.AddDays(1)) { days.Add(new MonthlyTimesheetDayModel { Date = date, IsWeekend = date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday, IsHoliday = holidays.Contains(date) }); } return new MonthlyTimesheetModel { Year = monthStart.Year, Month = monthStart.Month, Days = days, Rows = [ CreateRow("office", days.Count), CreateRow("home", days.Count), CreateRow("overtime", days.Count), CreateRow("weekend", days.Count), CreateRow("night", days.Count), CreateRow("vacation", days.Count), CreateRow("permit", days.Count), CreateRow("compensatory-rest", days.Count), CreateRow("sick", days.Count), CreateRow("holiday", days.Count) ] }; } private static MonthlyTimesheetRowModel CreateRow(string key, int dayCount) { return new MonthlyTimesheetRowModel { Key = key, DailyValues = Enumerable.Repeat(null, dayCount).ToList() }; } private static string GetExpectedWorkbookPath() { var repositoryRoot = FindRepositoryRoot(); var candidate = Path.Combine(repositoryRoot, "tests", "WorkTracker.Tests", "Expected", "monthly-timesheet-2026-04-empty.expected.xlsx"); return File.Exists(candidate) ? candidate : GetTemplatePath(); } private static string GetTemplatePath() { var repositoryRoot = FindRepositoryRoot(); return Path.Combine(repositoryRoot, "Templates", "monthly-timesheet-template.xlsx"); } private static string FindRepositoryRoot() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "WorkTracker.sln"))) { return directory.FullName; } directory = directory.Parent; } throw new DirectoryNotFoundException("Unable to locate the WorkTracker repository root."); } private static string GetFillSignature(IXLCell cell) { var fill = cell.Style.Fill; return string.Join( "|", fill.PatternType, DescribeColor(fill.BackgroundColor), DescribeColor(fill.PatternColor)); } private static string DescribeColor(XLColor color) { return color.ColorType switch { XLColorType.Color => $"rgb:{color.Color.ToArgb()}", XLColorType.Indexed => $"indexed:{color.Indexed}", XLColorType.Theme => $"theme:{color.ThemeColor}:{Math.Round(color.ThemeTint, 12)}", _ => color.ColorType.ToString() }; } }