using Microsoft.Extensions.Time.Testing; using SeerrAutoApprove.Options; using SeerrAutoApprove.Quota; namespace SeerrAutoApprove.Tests; public sealed class QuotaRulesTests { // Thursday private static readonly DateTimeOffset Now = new(2026, 10, 15, 12, 30, 0, TimeSpan.Zero); private static FakeTimeProvider Time(TimeZoneInfo? zone = null) { var time = new FakeTimeProvider(Now); time.SetLocalTimeZone(zone ?? TimeZoneInfo.Utc); return time; } [Fact] public void Rolling_period_starts_n_days_ago() { var start = QuotaRules.GetPeriodStart(new AutoApproveOptions { PeriodType = PeriodType.Rolling, RollingDays = 7 }, Time()); Assert.Equal(Now.AddDays(-7), start); } [Theory] [InlineData(CalendarPeriod.Day, 2026, 10, 15)] [InlineData(CalendarPeriod.Week, 2026, 10, 12)] [InlineData(CalendarPeriod.Month, 2026, 10, 1)] [InlineData(CalendarPeriod.Year, 2026, 1, 1)] public void Calendar_period_starts_at_beginning_of_period(CalendarPeriod period, int year, int month, int day) { var options = new AutoApproveOptions { PeriodType = PeriodType.Calendar, CalendarPeriod = period }; var start = QuotaRules.GetPeriodStart(options, Time()); Assert.Equal(new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero), start); } [Fact] public void Calendar_period_respects_local_time_zone() { var zone = TimeZoneInfo.CreateCustomTimeZone("UTC+2", TimeSpan.FromHours(2), "UTC+2", "UTC+2"); var options = new AutoApproveOptions { PeriodType = PeriodType.Calendar, CalendarPeriod = CalendarPeriod.Month }; var start = QuotaRules.GetPeriodStart(options, Time(zone)); Assert.Equal(new DateTimeOffset(2026, 10, 1, 0, 0, 0, TimeSpan.FromHours(2)), start); } [Fact] public void Week_starts_on_monday_when_today_is_sunday() { var time = new FakeTimeProvider(new DateTimeOffset(2026, 10, 18, 10, 0, 0, TimeSpan.Zero)); time.SetLocalTimeZone(TimeZoneInfo.Utc); var options = new AutoApproveOptions { PeriodType = PeriodType.Calendar, CalendarPeriod = CalendarPeriod.Week }; Assert.Equal(new DateTimeOffset(2026, 10, 12, 0, 0, 0, TimeSpan.Zero), QuotaRules.GetPeriodStart(options, time)); } [Fact] public void Categorizes_requests() { Assert.Equal(QuotaCategory.Movie, QuotaRules.GetCategory(Requests.Movie(1))); Assert.Equal(QuotaCategory.Movie4k, QuotaRules.GetCategory(Requests.Movie(1, is4k: true))); Assert.Equal(QuotaCategory.Series, QuotaRules.GetCategory(Requests.Series(1, 1))); Assert.Equal(QuotaCategory.Series4k, QuotaRules.GetCategory(Requests.Series(1, 1, is4k: true))); } [Fact] public void Falls_back_to_media_type_when_request_type_missing() { var request = Requests.Movie(1) with { Type = null, Media = new Seerr.SeerrMedia { MediaType = "tv" } }; Assert.Equal(QuotaCategory.Series, QuotaRules.GetCategory(request)); } [Theory] [InlineData(SeriesCountMode.PerSeason, 3, 3)] [InlineData(SeriesCountMode.PerSeason, 0, 1)] [InlineData(SeriesCountMode.PerRequest, 3, 1)] public void Series_units_follow_count_mode(SeriesCountMode mode, int seasons, int expected) { var units = QuotaRules.GetUnits(Requests.Series(1, seasons), new AutoApproveOptions { SeriesCountMode = mode }); Assert.Equal(expected, units); } [Fact] public void Movies_always_count_as_one() { Assert.Equal(1, QuotaRules.GetUnits(Requests.Movie(1), new AutoApproveOptions())); } }