using Jiaowu.Api.Controllers; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.System; using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Scheduling; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class SchedulesControllerTests { [Fact] public async Task Manual_entry_rejects_hours_reserved_for_experiments() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var term = new AcademicTerm { Code = "2026-LAB", Name = "2026 实验学时测试", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 7), EndDate = new DateOnly(2027, 1, 17) }; var college = new College { Code = "LAB", Name = "实验学院" }; var course = new Course { Code = "LAB-01", Name = "实验混合课程", College = college, Credits = 2, TotalHours = 32, LectureHours = 16, PracticeHours = 16 }; var task = new TeachingTask { TaskNumber = "TASK-LAB", Name = "实验混合教学班", AcademicTerm = term, Course = course, Capacity = 30, StartWeek = 1, EndWeek = 16, WeeklyHours = 2, Status = TeachingTaskStatus.Published }; var plan = new SchedulePlan { AcademicTerm = term, Name = "排课草稿", Version = "V1" }; plan.Entries.Add(new ScheduleEntry { TeachingTask = task, DayOfWeek = 1, StartPeriod = 1, PeriodCount = 1, StartWeek = 1, EndWeek = 16, WeekPattern = WeekPattern.All }); db.AddRange(term, college, course, task, plan); db.ScheduleTimeSlots.AddRange( new ScheduleTimeSlot { AcademicTerm = term, PeriodNumber = 1, Name = "第 1 节", StartsAt = new TimeOnly(8, 0), EndsAt = new TimeOnly(8, 45) }, new ScheduleTimeSlot { AcademicTerm = term, PeriodNumber = 2, Name = "第 2 节", StartsAt = new TimeOnly(8, 55), EndsAt = new TimeOnly(9, 40) }); db.TeachingTaskScheduleConstraints.Add( new TeachingTaskScheduleConstraint { TeachingTask = task, RequiresClassroom = false }); await db.SaveChangesAsync(); var controller = new SchedulesController(db); var result = await controller.CreateEntry( plan.Id, new ScheduleEntryRequest( task.Id, null, 2, 2, 1, 1, 16, WeekPattern.All, null), CancellationToken.None); var problem = Assert.IsType(result); var details = Assert.IsType(problem.Value); Assert.Contains( "实践学时请在实验管理中安排", details.Detail); Assert.Single(await db.ScheduleEntries.ToListAsync()); } [Fact] public async Task Publish_returns_accepted_before_background_validation_runs() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var term = new AcademicTerm { Code = "2026-P", Name = "2026 发布测试", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 7), EndDate = new DateOnly(2027, 1, 17) }; var college = new College { Code = "P", Name = "发布学院" }; var course = new Course { Code = "P101", Name = "发布课程", College = college, Credits = 1, TotalHours = 16, LectureHours = 16 }; var task = new TeachingTask { TaskNumber = "TASK-P", Name = "发布教学班", AcademicTerm = term, Course = course, Capacity = 30, WeeklyHours = 1, Status = TeachingTaskStatus.Published }; var plan = new SchedulePlan { AcademicTerm = term, Name = "发布草稿", Version = "V1" }; plan.Entries.Add(new ScheduleEntry { TeachingTask = task, DayOfWeek = 1, StartPeriod = 1, PeriodCount = 1, StartWeek = 1, EndWeek = 16, WeekPattern = WeekPattern.All }); db.AddRange(term, college, course, task, plan); await db.SaveChangesAsync(); var controller = new SchedulesController(db) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; var result = await controller.PublishPlan(plan.Id, CancellationToken.None); var accepted = Assert.IsType(result.Result); var response = Assert.IsType(accepted.Value); Assert.Equal(SchedulePublishJobStatus.Queued, response.Status); Assert.Equal(plan.Id, response.SchedulePlanId); Assert.Equal( SchedulePlanStatus.Draft, (await db.SchedulePlans.SingleAsync()).Status); Assert.Equal(1, await db.SchedulePublishJobs.CountAsync()); var outbox = await db.BackgroundJobOutboxMessages.SingleAsync(); Assert.Equal(BackgroundJobKind.SchedulePublish, outbox.JobKind); Assert.Equal(response.Id, outbox.JobId); Assert.Equal(BackgroundJobOutboxState.Pending, outbox.State); } [Fact] public async Task Latest_auto_schedule_job_remains_available_after_completion() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var term = new AcademicTerm { Code = "2026-F", Name = "2026 秋季", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 7), EndDate = new DateOnly(2027, 1, 17) }; var plan = new SchedulePlan { AcademicTerm = term, Name = "排课草稿", Version = "V1" }; var completed = new AutomaticScheduleJob { SchedulePlan = plan, Status = AutomaticScheduleJobStatus.Succeeded, CreatedEntries = 12, CompletedTasks = 5, TotalTasks = 6, ProcessedTasks = 6, MessagesJson = "[\"TASK-06 仍有 2 学时无法安排\"]", CompletedAt = DateTime.UtcNow }; db.AddRange(term, plan, completed); await db.SaveChangesAsync(); var controller = new SchedulesController(db); var result = await controller.GetLatestAutomaticScheduleJob( plan.Id, CancellationToken.None); var ok = Assert.IsType(result.Result); var response = Assert.IsType(ok.Value); Assert.Equal(completed.Id, response.Id); Assert.Equal(AutomaticScheduleJobStatus.Succeeded, response.Status); Assert.Equal( "TASK-06 仍有 2 学时无法安排", Assert.Single(response.Messages)); } }