using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Timetables;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Tests;

public sealed class ExperimentsControllerTests
{
    [Fact]
    public async Task BatchProjects_CreateSameDefinitionForSameCourseTasks()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);

        var result = await controller.CreateProjects(
            fixture.BatchProjectRequest(
                ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);

        Assert.IsType<CreatedResult>(result);
        var projects = await fixture.Db.ExperimentProjects
            .OrderBy(x => x.TeachingTaskId)
            .ToListAsync();
        Assert.Equal(2, projects.Count);
        Assert.Equal(2, projects.Select(x => x.TeachingTaskId).Distinct().Count());
        Assert.All(projects, project =>
        {
            Assert.Equal("LAB-BATCH", project.Code);
            Assert.Equal("公共实验任务", project.Name);
            Assert.Equal(ExperimentProjectStatus.Draft, project.Status);
        });
    }

    [Fact]
    public async Task BatchProjects_AssignsPastedNamesAndSequentialNamesInGenerationOrder()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);

        var pasted = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
        {
            Names = ["实验一", "实验二"]
        };
        Assert.IsType<CreatedResult>(await controller.CreateProjects(pasted, CancellationToken.None));
        Assert.Equal(["实验一", "实验二"], await fixture.Db.ExperimentProjects
            .OrderBy(x => x.TeachingTask!.TaskNumber)
            .Select(x => x.Name)
            .ToListAsync());

        fixture.Db.ExperimentProjects.RemoveRange(fixture.Db.ExperimentProjects);
        await fixture.Db.SaveChangesAsync();
        var sequential = fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled) with
        {
            Name = "数据库实验",
            GenerateSequentialNames = true
        };
        Assert.IsType<CreatedResult>(await controller.CreateProjects(sequential, CancellationToken.None));
        Assert.Equal(["数据库实验 01", "数据库实验 02"], await fixture.Db.ExperimentProjects
            .OrderBy(x => x.TeachingTask!.TaskNumber)
            .Select(x => x.Name)
            .ToListAsync());
    }

    [Fact]
    public async Task BatchProjectNames_UpdatesOnlySelectedDraftProjectsInRequestOrder()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);
        await controller.CreateProjects(
            fixture.BatchProjectRequest(ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var ids = await fixture.Db.ExperimentProjects
            .OrderByDescending(x => x.TeachingTask!.TaskNumber)
            .Select(x => x.Id)
            .ToListAsync();

        var result = await controller.UpdateProjectNames(
            new ExperimentProjectBatchNamesRequest(ids, ["第二个", "第一个"]),
            CancellationToken.None);

        Assert.IsType<NoContentResult>(result);
        var projectsById = await fixture.Db.ExperimentProjects.ToDictionaryAsync(x => x.Id, x => x.Name);
        Assert.Equal("第二个", projectsById[ids[0]]);
        Assert.Equal("第一个", projectsById[ids[1]]);
    }

    [Fact]
    public async Task CentralizedBatchProjects_CreatesOneProjectForEveryScheduledExperimentLesson()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        fixture.Db.ScheduleEntries.Add(new ScheduleEntry
        {
            SchedulePlanId = fixture.ScheduleEntry.SchedulePlanId,
            TeachingTaskId = fixture.Task.Id,
            Kind = ScheduleEntryKind.Experiment,
            ClassroomId = fixture.SecondClassroom.Id,
            DayOfWeek = 3,
            StartPeriod = 5,
            PeriodCount = 2,
            StartWeek = 1,
            EndWeek = 8,
            WeekPattern = WeekPattern.All
        });
        await fixture.Db.SaveChangesAsync();

        var result = await fixture.Controller(fixture.ManagerScope).CreateProjects(
            new ExperimentProjectBatchRequest(
                [fixture.Task.Id],
                "LAB-ALL",
                "全部课表实验",
                ExperimentArrangementMode.Centralized,
                null,
                null,
                fixture.Term.StartDate,
                fixture.Term.StartDate.AddDays(14)),
            CancellationToken.None);

        Assert.IsType<CreatedResult>(result);
        var projects = await fixture.Db.ExperimentProjects
            .Where(x => x.Code == "LAB-ALL")
            .OrderBy(x => x.ScheduleEntryId)
            .ToListAsync();
        Assert.Equal(16, projects.Count);
        Assert.All(projects, project =>
        {
            Assert.Equal(fixture.Task.Id, project.TeachingTaskId);
            Assert.Equal(ExperimentArrangementMode.Centralized, project.ArrangementMode);
            Assert.NotNull(project.ScheduleEntryId);
        });
        Assert.Equal(2, projects.Select(x => x.ScheduleEntryId).Distinct().Count());
        Assert.Equal(8, projects.Select(x => x.ScheduleWeek).Distinct().Count());
    }

    [Fact]
    public async Task BatchSessions_RollsBackAllRowsWhenOneConflicts()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);
        await controller.CreateProjects(
            fixture.BatchProjectRequest(
                ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var projectIds = await fixture.Db.ExperimentProjects
            .OrderBy(x => x.TeachingTaskId)
            .Select(x => x.Id)
            .ToListAsync();

        var result = await controller.CreateSessions(
            new ExperimentSessionBatchRequest(
            [
                new ExperimentSessionBatchItem(
                    projectIds[0],
                    fixture.Classroom.Id,
                    fixture.Term.StartDate,
                    1,
                    2,
                    20,
                    null,
                    [fixture.Teacher.Id]),
                new ExperimentSessionBatchItem(
                    projectIds[1],
                    fixture.Classroom.Id,
                    fixture.Term.StartDate,
                    1,
                    2,
                    20,
                    null,
                    [fixture.Teacher.Id])
            ]),
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        fixture.Db.ChangeTracker.Clear();
        Assert.Empty(await fixture.Db.ExperimentSessions.ToListAsync());
    }

    [Fact]
    public async Task BatchSessions_CreatesRowsAtomicallyWhenAllAreValid()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);
        await controller.CreateProjects(
            fixture.BatchProjectRequest(
                ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var projectIds = await fixture.Db.ExperimentProjects
            .OrderBy(x => x.TeachingTaskId)
            .Select(x => x.Id)
            .ToListAsync();

        var result = await controller.CreateSessions(
            new ExperimentSessionBatchRequest(
            [
                new ExperimentSessionBatchItem(
                    projectIds[0],
                    fixture.Classroom.Id,
                    fixture.Term.StartDate,
                    1,
                    2,
                    20,
                    null,
                    [fixture.Teacher.Id]),
                new ExperimentSessionBatchItem(
                    projectIds[1],
                    fixture.SecondClassroom.Id,
                    fixture.Term.StartDate,
                    1,
                    2,
                    20,
                    null,
                    [fixture.Teacher.Id])
            ]),
            CancellationToken.None);

        Assert.IsType<CreatedResult>(result);
        Assert.Equal(2, await fixture.Db.ExperimentSessions.CountAsync());
    }

    [Fact]
    public async Task SelfScheduledProject_AllowsMultipleCandidateSessionsAndRequiresSelectionWindow()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);
        var request = fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled) with
        {
            SelectionStartsAt = DateTime.UtcNow.AddHours(1),
            SelectionEndsAt = DateTime.UtcNow.AddDays(1)
        };
        await manager.CreateProject(request, CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();

        var result = await manager.CreateSessions(
            new ExperimentSessionBatchRequest(
            [
                new ExperimentSessionBatchItem(project.Id, fixture.Classroom.Id,
                    fixture.Term.StartDate, 1, 2, 20, null, [fixture.Teacher.Id]),
                new ExperimentSessionBatchItem(project.Id, fixture.SecondClassroom.Id,
                    fixture.Term.StartDate, 4, 2, 20, null, [fixture.Teacher.Id])
            ]),
            CancellationToken.None);

        Assert.IsType<CreatedResult>(result);
        Assert.Equal(2, await fixture.Db.ExperimentSessions.CountAsync());
        Assert.Equal(2, await fixture.Db.ExperimentSessionInstructors.CountAsync());
        Assert.IsType<NoContentResult>(await manager.PublishProject(project.Id, CancellationToken.None));
        var session = await fixture.Db.ExperimentSessions.OrderBy(x => x.StartPeriod).FirstAsync();
        Assert.IsType<ConflictObjectResult>(
            await fixture.Controller(fixture.StudentScope).Book(session.Id, CancellationToken.None));
    }

    [Fact]
    public async Task BoundInstructor_CanCancelOwnSession()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);
        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        Assert.IsType<CreatedResult>(await manager.CreateSession(
            project.Id, fixture.SessionRequest(1, 2, 20), CancellationToken.None));
        var session = await fixture.Db.ExperimentSessions.SingleAsync();
        Assert.IsType<NoContentResult>(await manager.PublishProject(project.Id, CancellationToken.None));

        var instructorScope = new FixedScope(new CurrentUserScope(
            fixture.Teacher.UserId!.Value,
            fixture.Teacher.Name,
            null,
            DataScope.Self,
            new HashSet<string>([SystemRoles.Teacher])));

        Assert.IsType<NoContentResult>(await fixture.Controller(instructorScope)
            .CancelSession(session.Id, CancellationToken.None));
        Assert.Equal(ExperimentSessionStatus.Cancelled,
            (await fixture.Db.ExperimentSessions.SingleAsync()).Status);
    }

    [Fact]
    public async Task CentralizedProject_PublishesWhenBoundToPublishedExperimentSchedule()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);
        var created = await controller.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
            CancellationToken.None);
        Assert.IsType<CreatedResult>(created);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();

        var sessionResult = await controller.CreateSession(
            project.Id,
            fixture.SessionRequest(1, 2, 10),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(sessionResult);

        var published = await controller.PublishProject(
            project.Id,
            CancellationToken.None);
        Assert.IsType<NoContentResult>(published);
        Assert.Equal(
            ExperimentProjectStatus.Published,
            (await fixture.Db.ExperimentProjects.SingleAsync()).Status);

        Assert.Empty(await fixture.Db.ExperimentSessions.ToListAsync());
    }

    [Fact]
    public async Task PublishedProject_AllowsTeachingDetailCorrectionWithoutChangingSchedule()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ManagerScope);
        await controller.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
            CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        await controller.PublishProject(project.Id, CancellationToken.None);
        fixture.Db.ChangeTracker.Clear();

        Assert.IsType<NoContentResult>(await controller.CorrectPublishedProjectDetails(
            project.Id,
            new PublishedExperimentProjectCorrectionRequest(
                "修正后的实验名称",
                "按本班教学计划调整实验步骤。",
                "请提前完成环境检查。"),
            CancellationToken.None));

        fixture.Db.ChangeTracker.Clear();
        var corrected = await fixture.Db.ExperimentProjects.SingleAsync();
        Assert.Equal("修正后的实验名称", corrected.Name);
        Assert.Equal("按本班教学计划调整实验步骤。", corrected.Description);
        Assert.Equal("请提前完成环境检查。", corrected.Requirements);
        Assert.Equal("LAB-C", corrected.Code);
        Assert.Equal(fixture.ScheduleEntry.Id, corrected.ScheduleEntryId);
    }

    [Fact]
    public async Task SelfScheduledBooking_IsSinglePerProjectAndCanBeChanged()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);
        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        await manager.CreateSession(
            project.Id,
            fixture.SessionRequest(3, 2),
            CancellationToken.None);
        await manager.CreateSession(
            project.Id,
            fixture.SessionRequest(5, 2),
            CancellationToken.None);
        await manager.PublishProject(project.Id, CancellationToken.None);

        var sessions = await fixture.Db.ExperimentSessions
            .OrderBy(x => x.StartPeriod)
            .ToListAsync();
        var student = fixture.Controller(fixture.StudentScope);
        Assert.IsType<NoContentResult>(
            await student.Book(sessions[0].Id, CancellationToken.None));
        Assert.IsType<ConflictObjectResult>(
            await student.Book(sessions[1].Id, CancellationToken.None));

        var booking = await fixture.Db.ExperimentBookings.SingleAsync();
        Assert.IsType<NoContentResult>(
            await student.CancelBooking(booking.Id, CancellationToken.None));
        Assert.IsType<NoContentResult>(
            await student.Book(sessions[1].Id, CancellationToken.None));

        var active = await fixture.Db.ExperimentBookings.SingleAsync();
        Assert.Equal(ExperimentBookingStatus.Booked, active.Status);
        Assert.Equal(sessions[1].Id, active.ExperimentSessionId);
        Assert.Equal(0, (await fixture.Db.ExperimentSessions.FindAsync(
            sessions[0].Id))!.ReservedCount);
        Assert.Equal(1, (await fixture.Db.ExperimentSessions.FindAsync(
            sessions[1].Id))!.ReservedCount);
    }

    [Fact]
    public async Task StudentBooking_RejectsPublishedTimetableConflict()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);
        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        await manager.CreateSession(
            project.Id,
            fixture.SessionRequest(1, 2),
            CancellationToken.None);
        await manager.PublishProject(project.Id, CancellationToken.None);

        fixture.Db.SchedulePlans.Add(new SchedulePlan
        {
            AcademicTermId = fixture.Term.Id,
            Name = "正式课表",
            Version = "V1",
            Status = SchedulePlanStatus.Published,
            Entries =
            [
                new ScheduleEntry
                {
                    TeachingTaskId = fixture.Task.Id,
                    ClassroomId = fixture.SecondClassroom.Id,
                    DayOfWeek = 1,
                    StartPeriod = 1,
                    PeriodCount = 2,
                    StartWeek = 1,
                    EndWeek = 18,
                    WeekPattern = WeekPattern.All
                }
            ]
        });
        await fixture.Db.SaveChangesAsync();

        var session = await fixture.Db.ExperimentSessions.SingleAsync();
        var result = await fixture.Controller(fixture.StudentScope)
            .Book(session.Id, CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        Assert.Empty(fixture.Db.ExperimentBookings);
    }

    [Fact]
    public async Task CentralizedSession_RejectsRelatedPublishedTimetableConflict()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);
        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
            CancellationToken.None);
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        fixture.Db.SchedulePlans.Add(new SchedulePlan
        {
            AcademicTermId = fixture.Term.Id,
            Name = "正式课表",
            Version = "V1",
            Status = SchedulePlanStatus.Published,
            Entries =
            [
                new ScheduleEntry
                {
                    TeachingTaskId = fixture.Task.Id,
                    ClassroomId = fixture.SecondClassroom.Id,
                    DayOfWeek = 1,
                    StartPeriod = 1,
                    PeriodCount = 2,
                    StartWeek = 1,
                    EndWeek = 18,
                    WeekPattern = WeekPattern.All
                }
            ]
        });
        await fixture.Db.SaveChangesAsync();

        var result = await manager.CreateSession(
            project.Id,
            fixture.SessionRequest(1, 2),
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        Assert.Empty(fixture.Db.ExperimentSessions);
    }

    [Fact]
    public async Task StudentTimetable_IncludesCentralizedAndBookedSelfScheduledSessions()
    {
        await using var fixture = await ExperimentFixture.CreateAsync();
        var manager = fixture.Controller(fixture.ManagerScope);

        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.Centralized),
            CancellationToken.None);
        var centralized = await fixture.Db.ExperimentProjects.SingleAsync();
        await manager.CreateSession(
            centralized.Id,
            fixture.SessionRequest(1, 2),
            CancellationToken.None);
        await manager.PublishProject(centralized.Id, CancellationToken.None);

        await manager.CreateProject(
            fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled),
            CancellationToken.None);
        var selfScheduled = await fixture.Db.ExperimentProjects.SingleAsync(
            x => x.ArrangementMode == ExperimentArrangementMode.SelfScheduled);
        await manager.CreateSession(
            selfScheduled.Id,
            fixture.SessionRequest(4, 2),
            CancellationToken.None);
        await manager.PublishProject(selfScheduled.Id, CancellationToken.None);

        var selfSession = await fixture.Db.ExperimentSessions.SingleAsync(
            x => x.ExperimentProjectId == selfScheduled.Id);
        var studentController = fixture.Controller(fixture.StudentScope);
        Assert.IsType<NoContentResult>(
            await studentController.Book(
                selfSession.Id,
                CancellationToken.None));

        var student = await fixture.Db.Students.AsNoTracking().SingleAsync();
        var service = new TimetableDataService(fixture.Db);
        var timetable = await service.BuildAsync(
            TimetableResourceType.Class,
            student.AdministrativeClassId,
            fixture.Term.Id,
            null,
            false,
            student.Id,
            new TimetableStudentDto(student.StudentNumber, student.Name),
            CancellationToken.None);

        Assert.NotNull(timetable);
        Assert.Single(timetable.ExperimentEntries);
        Assert.Equal(
            ExperimentArrangementMode.SelfScheduled,
            timetable.ExperimentEntries[0].ExperimentArrangementMode);
        Assert.Contains(
            timetable.ExperimentEntries,
            x => x.Id == selfSession.Id &&
                 x.ExperimentArrangementMode ==
                 ExperimentArrangementMode.SelfScheduled);
        Assert.All(
            timetable.ExperimentEntries,
            x =>
            {
                Assert.True(x.IsExperiment);
                Assert.Equal(fixture.Term.StartDate, x.ExperimentDate);
                Assert.Equal(1, x.StartWeek);
            });

        var publicClassTimetable = await service.BuildAsync(
            TimetableResourceType.Class,
            student.AdministrativeClassId,
            fixture.Term.Id,
            null,
            false,
            null,
            null,
            CancellationToken.None);
        Assert.NotNull(publicClassTimetable);
        Assert.Empty(publicClassTimetable.ExperimentEntries);

        var booking = await fixture.Db.ExperimentBookings.SingleAsync();
        Assert.IsType<NoContentResult>(
            await studentController.CancelBooking(
                booking.Id,
                CancellationToken.None));
        timetable = await service.BuildAsync(
            TimetableResourceType.Class,
            student.AdministrativeClassId,
            fixture.Term.Id,
            null,
            false,
            student.Id,
            new TimetableStudentDto(student.StudentNumber, student.Name),
            CancellationToken.None);

        Assert.NotNull(timetable);
        Assert.Empty(timetable.ExperimentEntries);
    }

    private sealed class ExperimentFixture : IAsyncDisposable
    {
        private ExperimentFixture(
            SqliteConnection connection,
            AppDbContext db,
            AcademicTerm term,
            TeachingTask task,
            Teacher teacher,
            Classroom classroom,
            Classroom secondClassroom,
            ScheduleEntry scheduleEntry,
            ICurrentUserDataScope managerScope,
            ICurrentUserDataScope studentScope)
        {
            Connection = connection;
            Db = db;
            Term = term;
            Task = task;
            Teacher = teacher;
            Classroom = classroom;
            SecondClassroom = secondClassroom;
            ScheduleEntry = scheduleEntry;
            ManagerScope = managerScope;
            StudentScope = studentScope;
        }

        private SqliteConnection Connection { get; }
        public AppDbContext Db { get; }
        public AcademicTerm Term { get; }
        public TeachingTask Task { get; }
        public Teacher Teacher { get; }
        public Classroom Classroom { get; }
        public Classroom SecondClassroom { get; }
        public ScheduleEntry ScheduleEntry { get; }
        public ICurrentUserDataScope ManagerScope { get; }
        public ICurrentUserDataScope StudentScope { get; }

        public static async Task<ExperimentFixture> CreateAsync()
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var options = new DbContextOptionsBuilder<AppDbContext>()
                .UseSqlite(connection)
                .Options;
            var db = new AppDbContext(options);
            await db.Database.EnsureCreatedAsync();

            var manager = User("manager", "学院实验管理员");
            var studentUser = User("student", "实验学生");
            var campus = new Campus { Code = "MAIN", Name = "主校区" };
            var building = new Building
            {
                Code = "LAB",
                Name = "实验中心",
                CampusId = campus.Id
            };
            var classroom = new Classroom
            {
                Code = "LAB101",
                Name = "实验室 101",
                BuildingId = building.Id,
                Capacity = 40,
                TeachingVenueNature = TeachingVenueNature.Laboratory
            };
            var secondClassroom = new Classroom
            {
                Code = "LAB102",
                Name = "实验室 102",
                BuildingId = building.Id,
                Capacity = 40,
                TeachingVenueNature = TeachingVenueNature.Laboratory
            };
            var college = new College { Code = "CS", Name = "计算机学院" };
            manager.CollegeId = college.Id;
            var major = new Major
            {
                Code = "CS",
                Name = "计算机科学与技术",
                CollegeId = college.Id,
                DegreeType = "工学"
            };
            var administrativeClass = new AdministrativeClass
            {
                Code = "CS2099",
                Name = "计科 2099",
                MajorId = major.Id,
                Grade = 2099
            };
            var student = new Student
            {
                StudentNumber = "20990001",
                Name = "实验学生",
                AdministrativeClassId = administrativeClass.Id,
                EnrollmentYear = 2099,
                EnrollmentDate = new DateOnly(2099, 9, 1),
                UserId = studentUser.Id
            };
            var teacher = new Teacher
            {
                TeacherNumber = "T2099",
                Name = "实验教师",
                CollegeId = college.Id,
                UserId = manager.Id
            };
            var term = new AcademicTerm
            {
                Code = "2099-1",
                Name = "2099—2100 学年第一学期",
                AcademicYear = "2099-2100",
                Season = TermSeason.Autumn,
                StartDate = new DateOnly(2099, 9, 7),
                EndDate = new DateOnly(2100, 1, 17),
                IsCurrent = true
            };
            var course = new Course
            {
                Code = "CSLAB",
                Name = "系统实验",
                CollegeId = college.Id,
                Credits = 2,
                TotalHours = 32,
                LectureHours = 16,
                PracticeHours = 16,
                Nature = CourseNature.Practice,
                AssessmentMethod = AssessmentMethod.Assessment
            };
            var task = new TeachingTask
            {
                TaskNumber = "2099-1-CSLAB-01",
                Name = "系统实验教学班",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 40,
                Status = TeachingTaskStatus.Published,
                Teachers =
                [
                    new TeachingTaskTeacher
                    {
                        TeacherId = teacher.Id,
                        IsPrimary = true
                    }
                ],
                Classes =
                [
                    new TeachingTaskClass
                    {
                        AdministrativeClassId = administrativeClass.Id
                    }
                ]
            };
            var secondTask = new TeachingTask
            {
                TaskNumber = "2099-1-CSLAB-02",
                Name = "系统实验教学班 02",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 40,
                Status = TeachingTaskStatus.Published,
                Teachers =
                [
                    new TeachingTaskTeacher
                    {
                        TeacherId = teacher.Id,
                        IsPrimary = true
                    }
                ]
            };
            db.AddRange(
                manager,
                studentUser,
                campus,
                building,
                classroom,
                secondClassroom,
                college,
                major,
                administrativeClass,
                student,
                teacher,
                term,
                course,
                task,
                secondTask);
            for (var period = 1; period <= 12; period++)
            {
                db.ScheduleTimeSlots.Add(new ScheduleTimeSlot
                {
                    AcademicTermId = term.Id,
                    PeriodNumber = period,
                    Name = $"第 {period} 节",
                    StartsAt = new TimeOnly(8, 0).AddMinutes((period - 1) * 50),
                    EndsAt = new TimeOnly(8, 45).AddMinutes((period - 1) * 50)
                });
            }
            await db.SaveChangesAsync();
            var scheduleEntry = new ScheduleEntry
            {
                SchedulePlan = new SchedulePlan
                {
                    AcademicTermId = term.Id,
                    Name = "已发布实验课表",
                    Version = "LAB-1",
                    Status = SchedulePlanStatus.Published
                },
                TeachingTaskId = task.Id,
                Kind = ScheduleEntryKind.Experiment,
                ClassroomId = classroom.Id,
                DayOfWeek = 1,
                StartPeriod = 10,
                PeriodCount = 2,
                StartWeek = 1,
                EndWeek = 8,
                WeekPattern = WeekPattern.All
            };
            db.ScheduleEntries.Add(scheduleEntry);
            await db.SaveChangesAsync();

            return new ExperimentFixture(
                connection,
                db,
                term,
                task,
                teacher,
                classroom,
                secondClassroom,
                scheduleEntry,
                Scope(
                    manager,
                    SystemRoles.CollegeAdmin,
                    DataScope.College),
                Scope(studentUser, SystemRoles.Student, DataScope.Self));
        }

        public ExperimentsController Controller(
            ICurrentUserDataScope currentScope) =>
            new(
                Db,
                currentScope,
                new ClassroomReservationAvailabilityService(Db));

        public ExperimentProjectRequest ProjectRequest(
            ExperimentArrangementMode mode) =>
            new(
                Task.Id,
                mode == ExperimentArrangementMode.Centralized
                    ? "LAB-C"
                    : "LAB-S",
                mode == ExperimentArrangementMode.Centralized
                    ? "集中上机实验"
                    : "自主上机实验",
                mode,
                "完成规定实验项目。",
                "携带校园卡。",
                Term.StartDate,
                Term.StartDate.AddDays(14),
                mode == ExperimentArrangementMode.Centralized ? ScheduleEntry.Id : null,
                mode == ExperimentArrangementMode.SelfScheduled
                    ? new DateTime(2020, 1, 1, 8, 0, 0)
                    : null,
                mode == ExperimentArrangementMode.SelfScheduled
                    ? new DateTime(2100, 1, 1, 18, 0, 0)
                    : null);

        public ExperimentProjectBatchRequest BatchProjectRequest(
            ExperimentArrangementMode mode) =>
            new(
                Db.TeachingTasks
                    .OrderBy(x => x.TaskNumber)
                    .Select(x => x.Id)
                    .ToList(),
                "LAB-BATCH",
                "公共实验任务",
                mode,
                "多个教学任务共用的实验内容。",
                "携带校园卡。",
                Term.StartDate,
                Term.StartDate.AddDays(14));

        public ExperimentSessionRequest SessionRequest(
            int startPeriod,
            int periodCount,
            int capacity = 1) =>
            new(
                Classroom.Id,
                Term.StartDate,
                startPeriod,
                periodCount,
                capacity,
                null,
                [Teacher.Id]);

        public async ValueTask DisposeAsync()
        {
            await Db.DisposeAsync();
            await Connection.DisposeAsync();
        }

        private static ApplicationUser User(
            string userName,
            string displayName) =>
            new()
            {
                Id = Guid.NewGuid(),
                UserName = userName,
                NormalizedUserName = userName.ToUpperInvariant(),
                DisplayName = displayName,
                IsEnabled = true
            };

        private static ICurrentUserDataScope Scope(
            ApplicationUser user,
            string role,
            DataScope dataScope) =>
            new FixedScope(new CurrentUserScope(
                user.Id,
                user.DisplayName,
                user.CollegeId,
                dataScope,
                new HashSet<string>([role])));
    }

    private sealed class FixedScope(CurrentUserScope current)
        : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = current;
    }
}
