using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Infrastructure.CourseSelection; namespace Jiaowu.Api.Tests; public sealed class CourseSelectionRulesTests { [Fact] public void Selection_requires_open_status_and_active_window() { var now = new DateTime(2026, 7, 24, 8, 0, 0, DateTimeKind.Utc); var round = CreateRound(now.AddHours(-1), now.AddHours(1)); Assert.True(CourseSelectionRules.IsSelectionOpen(round, now)); round.Status = CourseSelectionRoundStatus.Closed; Assert.False(CourseSelectionRules.IsSelectionOpen(round, now)); round.Status = CourseSelectionRoundStatus.Open; Assert.False(CourseSelectionRules.IsSelectionOpen(round, now.AddHours(2))); } [Fact] public void Withdrawal_respects_deadline() { var now = new DateTime(2026, 7, 24, 8, 0, 0, DateTimeKind.Utc); var round = CreateRound(now.AddDays(-1), now.AddDays(1)); round.WithdrawalEndsAt = now.AddMinutes(30); Assert.True(CourseSelectionRules.CanWithdraw(round, now)); Assert.False(CourseSelectionRules.CanWithdraw(round, now.AddHours(1))); } [Fact] public void Schedule_conflict_detects_overlapping_weeks_and_periods() { var selected = CreateEntry(1, 1, 2, 1, 16, WeekPattern.All); var overlapping = CreateEntry(1, 2, 2, 1, 16, WeekPattern.All); Assert.True(CourseSelectionRules.HasScheduleConflict( [overlapping], [selected])); } [Fact] public void Odd_and_even_week_entries_do_not_conflict() { var selected = CreateEntry(3, 5, 2, 1, 16, WeekPattern.Odd); var candidate = CreateEntry(3, 5, 2, 1, 16, WeekPattern.Even); Assert.False(CourseSelectionRules.HasScheduleConflict( [candidate], [selected])); } [Theory] [InlineData(CourseNature.GeneralRequired, true)] [InlineData(CourseNature.GeneralElective, false)] [InlineData(CourseNature.MajorRequired, false)] [InlineData(CourseNature.MajorElective, false)] [InlineData(CourseNature.Practice, false)] public void Proxy_enrollment_is_limited_to_general_required_courses( CourseNature nature, bool expected) { Assert.Equal(expected, CourseSelectionRules.SupportsProxyEnrollment(nature)); } private static CourseSelectionRound CreateRound(DateTime startsAt, DateTime endsAt) => new() { Name = "第一轮选课", StartsAt = startsAt, EndsAt = endsAt, WithdrawalEndsAt = endsAt.AddDays(1), Status = CourseSelectionRoundStatus.Open }; private static ScheduleEntry CreateEntry( int dayOfWeek, int startPeriod, int periodCount, int startWeek, int endWeek, WeekPattern weekPattern) => new() { DayOfWeek = dayOfWeek, StartPeriod = startPeriod, PeriodCount = periodCount, StartWeek = startWeek, EndWeek = endWeek, WeekPattern = weekPattern }; }