using System.Globalization; using FluentAssertions; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Calendar; using Xunit; namespace Wino.Core.Tests; public class CalendarRangeResolverTests { [Fact] public void Resolve_Day_ReturnsAnchorDateOnly() { var settings = CreateSettings(); var today = new DateOnly(2026, 3, 20); var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Day, today), settings, today); range.StartDate.Should().Be(today); range.EndDate.Should().Be(today); range.DayCount.Should().Be(1); range.ContainsToday.Should().BeTrue(); range.Dates.Should().ContainSingle().Which.Should().Be(today); } [Fact] public void Resolve_Week_HonorsConfiguredFirstDayOfWeek() { var settings = CreateSettings(firstDayOfWeek: DayOfWeek.Sunday); var anchor = new DateOnly(2026, 3, 18); var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Week, anchor), settings, today: anchor); range.StartDate.Should().Be(new DateOnly(2026, 3, 15)); range.EndDate.Should().Be(new DateOnly(2026, 3, 21)); range.DayCount.Should().Be(7); } [Fact] public void Resolve_WorkWeek_UsesConfiguredBounds() { var settings = CreateSettings( firstDayOfWeek: DayOfWeek.Sunday, workWeekStart: DayOfWeek.Monday, workWeekEnd: DayOfWeek.Thursday); var anchor = new DateOnly(2026, 3, 18); var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.WorkWeek, anchor), settings, today: anchor); range.StartDate.Should().Be(new DateOnly(2026, 3, 16)); range.EndDate.Should().Be(new DateOnly(2026, 3, 19)); range.DayCount.Should().Be(4); } [Fact] public void Resolve_Month_CoversEntireAnchorMonth() { var settings = CreateSettings(); var anchor = new DateOnly(2026, 2, 14); var range = CalendarRangeResolver.Resolve(new CalendarDisplayRequest(CalendarDisplayType.Month, anchor), settings, today: anchor); range.StartDate.Should().Be(new DateOnly(2026, 2, 1)); range.EndDate.Should().Be(new DateOnly(2026, 2, 28)); range.DayCount.Should().Be(28); range.SpansSingleMonth.Should().BeTrue(); } [Theory] [InlineData(CalendarDisplayType.Day, 2026, 3, 18, 2026, 3, 19, 2026, 3, 17)] [InlineData(CalendarDisplayType.Week, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)] [InlineData(CalendarDisplayType.WorkWeek, 2026, 3, 18, 2026, 3, 25, 2026, 3, 11)] [InlineData(CalendarDisplayType.Month, 2026, 3, 18, 2026, 4, 18, 2026, 2, 18)] public void Navigate_MovesExactlyOnePeriod( CalendarDisplayType displayType, int year, int month, int day, int nextYear, int nextMonth, int nextDay, int previousYear, int previousMonth, int previousDay) { var settings = CreateSettings(); var today = new DateOnly(2026, 3, 20); var current = CalendarRangeResolver.Resolve( new CalendarDisplayRequest(displayType, new DateOnly(year, month, day)), settings, today); var next = CalendarRangeResolver.Navigate(current, 1, settings, today); var previous = CalendarRangeResolver.Navigate(current, -1, settings, today); next.AnchorDate.Should().Be(new DateOnly(nextYear, nextMonth, nextDay)); previous.AnchorDate.Should().Be(new DateOnly(previousYear, previousMonth, previousDay)); } [Fact] public void ChangeDisplayType_FromMonth_UsesTodayWhenTodayIsInsideCurrentMonth() { var settings = CreateSettings(); var today = new DateOnly(2026, 3, 20); var monthRange = CalendarRangeResolver.Resolve( new CalendarDisplayRequest(CalendarDisplayType.Month, new DateOnly(2026, 3, 5)), settings, today); var dayRange = CalendarRangeResolver.ChangeDisplayType(monthRange, CalendarDisplayType.Day, settings, today); dayRange.AnchorDate.Should().Be(today); dayRange.StartDate.Should().Be(today); dayRange.EndDate.Should().Be(today); } [Fact] public void Formatter_Day_UsesMonthDayPattern() { var formatter = new CalendarRangeTextFormatter(); var range = new VisibleDateRange( CalendarDisplayType.Day, new DateOnly(2026, 3, 20), new DateOnly(2026, 3, 20), new DateOnly(2026, 3, 20), new DateOnly(2026, 3, 20), 1, true, true, [new DateOnly(2026, 3, 20)]); var text = formatter.Format(range, new TestDateContextProvider("en-US", today: new DateOnly(2026, 3, 20))); text.Should().Be("March 20"); } [Fact] public void Formatter_Range_UsesCultureMonthDayPattern() { var formatter = new CalendarRangeTextFormatter(); var range = new VisibleDateRange( CalendarDisplayType.Week, new DateOnly(2026, 3, 20), new DateOnly(2026, 3, 16), new DateOnly(2026, 3, 22), new DateOnly(2026, 3, 20), 7, true, true, [ new DateOnly(2026, 3, 16), new DateOnly(2026, 3, 17), new DateOnly(2026, 3, 18), new DateOnly(2026, 3, 19), new DateOnly(2026, 3, 20), new DateOnly(2026, 3, 21), new DateOnly(2026, 3, 22) ]); var text = formatter.Format(range, new TestDateContextProvider("de-DE", today: new DateOnly(2026, 3, 20))); text.Should().Be("16. März - 22"); } private static CalendarSettings CreateSettings( DayOfWeek firstDayOfWeek = DayOfWeek.Monday, DayOfWeek workWeekStart = DayOfWeek.Monday, DayOfWeek workWeekEnd = DayOfWeek.Friday, string cultureName = "en-US") { return new CalendarSettings( firstDayOfWeek, [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday], true, workWeekStart, workWeekEnd, TimeSpan.FromHours(9), TimeSpan.FromHours(18), 64, DayHeaderDisplayType.TwentyFourHour, CultureInfo.GetCultureInfo(cultureName)); } private sealed class TestDateContextProvider(string cultureName, DateOnly today) : IDateContextProvider { public CultureInfo Culture => CultureInfo.GetCultureInfo(cultureName); public TimeZoneInfo TimeZone => TimeZoneInfo.Utc; public DateOnly GetToday() => today; } }