using QuestionsApi.Services.Flashcards; using Xunit; namespace QuestionsApi.Tests; public sealed class FlashcardSchedulingTests { [Fact] public void NextStudyDayStart_IsMidnightOfTheFollowingDay() { var studyDayStart = new DateTime(2026, 3, 5, 0, 0, 0, DateTimeKind.Utc); var next = FlashcardScheduling.NextStudyDayStart(studyDayStart); Assert.Equal(new DateTime(2026, 3, 6, 0, 0, 0, DateTimeKind.Utc), next); } [Fact] public void NextStudyDayStart_TruncatesTimeOfDayBeforeAdvancing() { // A session that started mid-day still buries siblings until the *next calendar day*, // not exactly 24h later. var studyDayStart = new DateTime(2026, 3, 5, 14, 30, 0, DateTimeKind.Utc); var next = FlashcardScheduling.NextStudyDayStart(studyDayStart); Assert.Equal(new DateTime(2026, 3, 6, 0, 0, 0, DateTimeKind.Utc), next); } [Theory] [InlineData(20, null, 0, 20)] [InlineData(20, null, 15, 5)] [InlineData(20, null, 25, 0)] [InlineData(20, 5, 0, 5)] [InlineData(20, 0, 0, 0)] public void ResolveDailyBudget_DeckOverrideWinsOverGlobal_ClampedAtZero( int globalLimit, int? deckOverride, int consumedToday, int expected) { Assert.Equal(expected, FlashcardScheduling.ResolveDailyBudget(globalLimit, deckOverride, consumedToday)); } private static readonly DateTime Today = new(2026, 3, 10, 0, 0, 0, DateTimeKind.Utc); [Fact] public void ComputeStreakDays_NoReviews_IsZero() { Assert.Equal(0, FlashcardScheduling.ComputeStreakDays([], Today)); } [Fact] public void ComputeStreakDays_ReviewedTodayOnly_IsOne() { var timestamps = new List { Today.AddHours(9) }; Assert.Equal(1, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } [Fact] public void ComputeStreakDays_ThreeConsecutiveDaysEndingToday_IsThree() { var timestamps = new List { Today.AddDays(-2).AddHours(8), Today.AddDays(-1).AddHours(20), Today.AddHours(7) }; Assert.Equal(3, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } [Fact] public void ComputeStreakDays_NotYetReviewedToday_StillCountsThroughYesterday() { // The streak shouldn't visibly reset to 0 the instant a new calendar day begins, before // the user has had a chance to study today. var timestamps = new List { Today.AddDays(-2).AddHours(8), Today.AddDays(-1).AddHours(20) }; Assert.Equal(2, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } [Fact] public void ComputeStreakDays_GapBeforeYesterday_BreaksTheStreak() { var timestamps = new List { Today.AddDays(-10).AddHours(8), Today.AddDays(-1).AddHours(20), Today.AddHours(7) }; Assert.Equal(2, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } [Fact] public void ComputeStreakDays_MissedYesterdayAndToday_IsZero() { var timestamps = new List { Today.AddDays(-3).AddHours(8) }; Assert.Equal(0, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } [Fact] public void ComputeStreakDays_MultipleReviewsSameDay_CountOnceTowardTheStreak() { var timestamps = new List { Today.AddHours(7), Today.AddHours(8), Today.AddHours(9) }; Assert.Equal(1, FlashcardScheduling.ComputeStreakDays(timestamps, Today)); } }