using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Grades;
using Jiaowu.Api.Infrastructure.Persistence;
using ClosedXML.Excel;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Tests;

public sealed class ExperimentGradesControllerTests
{
    [Fact]
    public async Task AssignedTeacher_CanSubmitExperimentGradeSheet()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var project = new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-TEACHER",
            Name = "教师提交实验",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        fixture.Db.ExperimentProjects.Add(project);
        await fixture.Db.SaveChangesAsync();

        var admin = fixture.ExperimentGrades(fixture.AdminScope);
        await admin.CreateSheet(
            new ExperimentGradeSheetRequest(
                project.Id,
                1,
                60,
                [new ExperimentGradeItemRequest(
                    "操作",
                    ExperimentGradeItemKind.Operation,
                    100)]),
            CancellationToken.None);
        var sheet = await fixture.Db.ExperimentGradeSheets
            .Include(x => x.Items)
            .Include(x => x.Records)
            .ThenInclude(x => x.ItemScores)
            .SingleAsync();
        var record = Assert.Single(sheet.Records);
        var item = Assert.Single(sheet.Items);

        var teacher = fixture.ExperimentGrades(fixture.TeacherScope);
        Assert.IsType<NoContentResult>(await teacher.UpdateRecords(
            sheet.Id,
            new ExperimentGradeRecordsRequest(
            [
                new ExperimentGradeRecordRequest(
                    record.Id,
                    ExperimentParticipationStatus.Completed,
                    false,
                    1,
                    null,
                    null,
                    false,
                    null,
                    [new ExperimentGradeItemScoreRequest(item.Id, 85, null)])
            ]),
            CancellationToken.None));
        fixture.Db.ChangeTracker.Clear();
        Assert.IsType<NoContentResult>(await teacher.Submit(
            sheet.Id,
            CancellationToken.None));
    }

    [Fact]
    public async Task Teacher_CanDownloadAndImportExperimentGradeTemplate()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var project = new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-EXCEL",
            Name = "Excel 实验成绩",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        fixture.Db.ExperimentProjects.Add(project);
        await fixture.Db.SaveChangesAsync();

        var admin = fixture.ExperimentGrades(fixture.AdminScope);
        await admin.CreateSheet(new ExperimentGradeSheetRequest(
            project.Id, 1, 60,
            [new ExperimentGradeItemRequest("操作", ExperimentGradeItemKind.Operation, 100)]),
            CancellationToken.None);
        var sheet = await fixture.Db.ExperimentGradeSheets.SingleAsync();
        fixture.Db.ChangeTracker.Clear();

        var teacher = fixture.ExperimentGrades(fixture.TeacherScope);
        var template = Assert.IsType<FileContentResult>(await teacher.DownloadTemplate(
            sheet.Id, CancellationToken.None));
        Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            template.ContentType);
        await using var templateStream = new MemoryStream(template.FileContents);
        await using var stream = new MemoryStream();
        using (var workbook = new XLWorkbook(templateStream))
        {
            var worksheet = workbook.Worksheet("实验成绩导入");
            worksheet.Cell("D2").Value = "已完成";
            worksheet.Cell("E2").Value = 85;
            workbook.SaveAs(stream);
        }
        stream.Position = 0;
        var file = new FormFile(stream, 0, stream.Length, "file", "实验成绩导入模板.xlsx");
        Assert.IsType<OkObjectResult>(await teacher.ImportRecords(
            sheet.Id, file, CancellationToken.None));
        fixture.Db.ChangeTracker.Clear();
        Assert.Equal(85m, await fixture.Db.ExperimentGradeRecords
            .Select(record => record.TotalScore)
            .SingleAsync());
    }

    [Fact]
    public async Task ManagementList_IsPagedFilteredAndCollegeScoped()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var collegeId = await fixture.Db.Courses
            .Where(x => x.Id == fixture.Task.CourseId)
            .Select(x => x.CollegeId)
            .SingleAsync();
        for (var index = 1; index <= 12; index++)
        {
            fixture.Db.ExperimentProjects.Add(new ExperimentProject
            {
                TeachingTaskId = fixture.Task.Id,
                Code = $"LAB-{index:00}",
                Name = $"分页实验 {index:00}",
                ArrangementMode = ExperimentArrangementMode.Centralized,
                StartDate = fixture.Term.StartDate,
                EndDate = fixture.Term.EndDate,
                Status = ExperimentProjectStatus.Published,
                PublishedAt = DateTime.UtcNow
            });
        }

        var otherCollege = new College { Code = "OTHER", Name = "其他学院" };
        var otherCourse = new Course
        {
            CollegeId = otherCollege.Id,
            Code = "OTHER-LAB",
            Name = "其他学院实验",
            Credits = 1,
            TotalHours = 16,
            PracticeHours = 16,
            Nature = CourseNature.Practice,
            AssessmentMethod = AssessmentMethod.Assessment
        };
        var otherTask = new TeachingTask
        {
            AcademicTermId = fixture.Term.Id,
            CourseId = otherCourse.Id,
            TaskNumber = "OTHER-LAB-01",
            Name = "其他学院实验班",
            Capacity = 20,
            Status = TeachingTaskStatus.Published
        };
        var otherProject = new ExperimentProject
        {
            TeachingTaskId = otherTask.Id,
            Code = "FOREIGN-LAB",
            Name = "不可见实验",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        fixture.Db.AddRange(otherCollege, otherCourse, otherTask, otherProject);
        await fixture.Db.SaveChangesAsync();
        fixture.Db.ChangeTracker.Clear();

        var collegeScope = new FixedScope(new CurrentUserScope(
            Guid.NewGuid(),
            "学院管理员",
            collegeId,
            DataScope.College,
            new HashSet<string>([SystemRoles.CollegeAdmin])));
        var controller = fixture.ExperimentGrades(collegeScope);
        var page = Assert.IsType<OkObjectResult>(await controller.GetManagement(
            null,
            null,
            null,
            null,
            1,
            10,
            CancellationToken.None));
        Assert.Equal(1, Property<int>(page.Value, "Total"));
        var course = Assert.Single(Property<System.Collections.IEnumerable>(
            page.Value, "Items").Cast<object>());
        Assert.Equal(12, Property<System.Collections.IEnumerable>(
            course, "Projects").Cast<object>().Count());

        var filtered = Assert.IsType<OkObjectResult>(await controller.GetManagement(
            null,
            null,
            null,
            "LAB-12",
            1,
            10,
            CancellationToken.None));
        Assert.Equal(1, Property<int>(filtered.Value, "Total"));
    }

    [Fact]
    public async Task ManagementList_OrdersProjectsByScheduledWeekAndPeriod()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var plan = new SchedulePlan
        {
            AcademicTermId = fixture.Term.Id,
            Name = "实验课表",
            Version = "v1",
            Status = SchedulePlanStatus.Published
        };
        var laterEntry = new ScheduleEntry
        {
            SchedulePlanId = plan.Id,
            TeachingTaskId = fixture.Task.Id,
            ClassroomId = fixture.Classroom.Id,
            Kind = ScheduleEntryKind.Experiment,
            DayOfWeek = 4,
            StartPeriod = 5,
            PeriodCount = 2,
            StartWeek = 5,
            EndWeek = 5,
            WeekPattern = WeekPattern.All
        };
        var earlierEntry = new ScheduleEntry
        {
            SchedulePlanId = plan.Id,
            TeachingTaskId = fixture.Task.Id,
            ClassroomId = fixture.Classroom.Id,
            Kind = ScheduleEntryKind.Experiment,
            DayOfWeek = 2,
            StartPeriod = 3,
            PeriodCount = 2,
            StartWeek = 2,
            EndWeek = 2,
            WeekPattern = WeekPattern.All
        };
        fixture.Db.AddRange(plan, laterEntry, earlierEntry);
        fixture.Db.AddRange(
            new ExperimentProject
            {
                TeachingTaskId = fixture.Task.Id,
                ScheduleEntryId = laterEntry.Id,
                ScheduleWeek = 5,
                Code = "LAB-A",
                Name = "后面的实验",
                ArrangementMode = ExperimentArrangementMode.Centralized,
                StartDate = fixture.Term.StartDate,
                EndDate = fixture.Term.EndDate,
                Status = ExperimentProjectStatus.Published
            },
            new ExperimentProject
            {
                TeachingTaskId = fixture.Task.Id,
                ScheduleEntryId = earlierEntry.Id,
                ScheduleWeek = 2,
                Code = "LAB-Z",
                Name = "前面的实验",
                ArrangementMode = ExperimentArrangementMode.Centralized,
                StartDate = fixture.Term.StartDate,
                EndDate = fixture.Term.EndDate,
                Status = ExperimentProjectStatus.Published
            });
        await fixture.Db.SaveChangesAsync();
        fixture.Db.ChangeTracker.Clear();

        var result = Assert.IsType<OkObjectResult>(await fixture.ExperimentGrades(
            fixture.AdminScope).GetManagement(
            fixture.Term.Id, null, null, null, 1, 10, CancellationToken.None));
        var course = Assert.Single(Property<System.Collections.IEnumerable>(
            result.Value, "Items").Cast<object>());
        var projects = Property<System.Collections.IEnumerable>(course, "Projects")
            .Cast<object>()
            .ToList();

        Assert.Equal(["前面的实验", "后面的实验"],
            projects.Select(project => Property<string>(project, "Name")));
        var schedule = Property<object>(projects[0], "ScheduleEntry");
        Assert.Equal(2, Property<int>(projects[0], "ScheduleWeek"));
        Assert.Equal(2, Property<int>(schedule, "DayOfWeek"));
        Assert.Equal(3, Property<int>(schedule, "StartPeriod"));
    }

    private static T Property<T>(object? value, string name) =>
        Assert.IsAssignableFrom<T>(
            value!.GetType().GetProperty(name)!.GetValue(value));

    [Fact]
    public async Task IndependentExperimentGrade_CanPublishAndImportAsCourseSnapshot()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        fixture.Db.ExperimentProjects.Add(new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-C",
            Name = "集中实验",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            PublishedAt = DateTime.UtcNow
        });
        await fixture.Db.SaveChangesAsync();
        var project = await fixture.Db.ExperimentProjects.SingleAsync();
        var experimentGrades = fixture.ExperimentGrades(fixture.AdminScope);

        var created = await experimentGrades.CreateSheet(
            new ExperimentGradeSheetRequest(
                project.Id,
                1,
                60,
                [
                    new ExperimentGradeItemRequest(
                        "操作",
                        ExperimentGradeItemKind.Operation,
                        50),
                    new ExperimentGradeItemRequest(
                        "报告",
                        ExperimentGradeItemKind.Report,
                        50)
                ]),
            CancellationToken.None);
        Assert.IsType<CreatedResult>(created);

        var sheet = await fixture.Db.ExperimentGradeSheets
            .Include(x => x.Items)
            .Include(x => x.Records)
            .ThenInclude(x => x.ItemScores)
            .SingleAsync();
        var record = Assert.Single(sheet.Records);
        var operation = sheet.Items.Single(x => x.Name == "操作");
        var report = sheet.Items.Single(x => x.Name == "报告");

        Assert.IsType<NoContentResult>(
            await experimentGrades.UpdateRecords(
                sheet.Id,
                new ExperimentGradeRecordsRequest(
                [
                    new ExperimentGradeRecordRequest(
                        record.Id,
                        ExperimentParticipationStatus.Completed,
                        false,
                        1,
                        "https://example.test/report",
                        DateTime.UtcNow,
                        false,
                        "完成规范",
                        [
                            new ExperimentGradeItemScoreRequest(
                                operation.Id,
                                80,
                                "操作正确"),
                            new ExperimentGradeItemScoreRequest(
                                report.Id,
                                90,
                                "报告完整")
                        ])
                ]),
                CancellationToken.None));
        Assert.IsType<NoContentResult>(
            await experimentGrades.Submit(
                sheet.Id,
                CancellationToken.None));
        Assert.IsType<NoContentResult>(
            await experimentGrades.Approve(
                sheet.Id,
                CancellationToken.None));
        Assert.IsType<NoContentResult>(
            await experimentGrades.Publish(
                sheet.Id,
                CancellationToken.None));

        fixture.Db.ChangeTracker.Clear();
        var publishedRecord = await fixture.Db.ExperimentGradeRecords
            .SingleAsync();
        Assert.Equal(85m, publishedRecord.TotalScore);
        Assert.True(publishedRecord.IsPassed);
        Assert.Equal(
            ExperimentGradeSheetStatus.Published,
            (await fixture.Db.ExperimentGradeSheets.SingleAsync()).Status);

        var studentResult = await fixture
            .ExperimentGrades(fixture.StudentScope)
            .GetMine(CancellationToken.None);
        var studentResultOk = Assert.IsType<OkObjectResult>(studentResult);
        var studentCourse = Assert.Single(
            Assert.IsAssignableFrom<System.Collections.IEnumerable>(
                studentResultOk.Value).Cast<object>());
        Assert.Equal(85m, Property<decimal>(
            studentCourse,
            "ExperimentCourseScore"));
        var persistedExperimentCourseGrade = await fixture.Db
            .ExperimentCourseGrades.SingleAsync();
        Assert.Equal(85m, persistedExperimentCourseGrade.WeightedAverageScore);
        Assert.Equal(1, persistedExperimentCourseGrade.PublishedProjectCount);

        var courseSheet = new GradeSheet
        {
            TeachingTaskId = fixture.Task.Id,
            RegularWeight = 0,
            FinalWeight = 50,
            Items =
            [
                new GradeItem
                {
                    Name = "实验",
                    Weight = 50,
                    SortOrder = 0
                }
            ],
            Records =
            [
                new GradeRecord
                {
                    StudentId = fixture.Student.Id,
                    FinalScore = 80
                }
            ]
        };
        var courseItem = courseSheet.Items.Single();
        courseSheet.Records.Single().ItemScores.Add(new GradeItemScore
        {
            GradeItemId = courseItem.Id
        });
        fixture.Db.GradeSheets.Add(courseSheet);
        await fixture.Db.SaveChangesAsync();

        var grades = new GradesController(fixture.Db, fixture.AdminScope);
        Assert.IsType<OkObjectResult>(
            await grades.ImportExperimentScores(
                courseSheet.Id,
                new ImportExperimentScoresRequest(courseItem.Id),
                CancellationToken.None));

        fixture.Db.ChangeTracker.Clear();
        var importedItem = await fixture.Db.GradeItems.SingleAsync();
        var importedRecord = await fixture.Db.GradeRecords
            .Include(x => x.ItemScores)
            .SingleAsync();
        Assert.Equal(
            GradeItemSourceType.ExperimentSummary,
            importedItem.SourceType);
        Assert.NotNull(importedItem.SourceSnapshotAt);
        Assert.Equal(85m, importedRecord.ItemScores.Single().Score);
        Assert.Equal(82.5m, importedRecord.TotalScore);

        var manualOverride = await grades.UpdateRecords(
            courseSheet.Id,
            new GradeRecordsRequest(
            [
                new GradeRecordRequest(
                    importedRecord.Id,
                    null,
                    80,
                    [new GradeItemScoreRequest(importedItem.Id, 90)],
                    GradeExamStatus.Normal,
                    null)
            ]),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(manualOverride);
    }

    [Fact]
    public async Task ExperimentCourseGrade_IsPersistedAsWeightedAverage()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var firstProject = new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-W1",
            Name = "低权重实验",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            GradeSheet = new ExperimentGradeSheet
            {
                ContributionWeight = 1,
                Status = ExperimentGradeSheetStatus.Published,
                Records =
                [
                    new ExperimentGradeRecord
                    {
                        StudentId = fixture.Student.Id,
                        ParticipationStatus = ExperimentParticipationStatus.Completed,
                        TotalScore = 80,
                        IsPassed = true
                    }
                ]
            }
        };
        var secondProject = new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-W2",
            Name = "高权重实验",
            ArrangementMode = ExperimentArrangementMode.Centralized,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            GradeSheet = new ExperimentGradeSheet
            {
                ContributionWeight = 3,
                Status = ExperimentGradeSheetStatus.Published,
                Records =
                [
                    new ExperimentGradeRecord
                    {
                        StudentId = fixture.Student.Id,
                        ParticipationStatus = ExperimentParticipationStatus.Completed,
                        TotalScore = 90,
                        IsPassed = true
                    }
                ]
            }
        };
        fixture.Db.AddRange(firstProject, secondProject);
        await fixture.Db.SaveChangesAsync();

        await ExperimentGradeAggregationService.RefreshTeachingTaskAsync(
            fixture.Db,
            fixture.Task.Id,
            CancellationToken.None);

        var persisted = await fixture.Db.ExperimentCourseGrades.SingleAsync();
        Assert.Equal(87.5m, persisted.WeightedAverageScore);
        Assert.Equal(4m, persisted.TotalWeight);
        Assert.Equal(2, persisted.PublishedProjectCount);

        firstProject.GradeSheet!.Records.Single().TotalScore = 0;
        await fixture.Db.SaveChangesAsync();
        var referenced = await ExperimentGradeAggregationService.CalculateAsync(
            fixture.Db,
            fixture.Task.Id,
            [fixture.Student.Id],
            CancellationToken.None);
        Assert.Equal(87.5m, referenced.Scores[fixture.Student.Id]);
    }

    [Fact]
    public async Task SelfScheduledSheet_SyncsOnlyActiveBookings()
    {
        await using var fixture = await ExperimentGradeFixture.CreateAsync();
        var project = new ExperimentProject
        {
            TeachingTaskId = fixture.Task.Id,
            Code = "LAB-S",
            Name = "自主实验",
            ArrangementMode = ExperimentArrangementMode.SelfScheduled,
            StartDate = fixture.Term.StartDate,
            EndDate = fixture.Term.EndDate,
            Status = ExperimentProjectStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        var session = new ExperimentSession
        {
            ExperimentProjectId = project.Id,
            ClassroomId = fixture.Classroom.Id,
            SessionDate = fixture.Term.StartDate,
            StartPeriod = 1,
            PeriodCount = 2,
            Capacity = 10,
            ReservedCount = 1
        };
        var booking = new ExperimentBooking
        {
            ExperimentProjectId = project.Id,
            ExperimentSessionId = session.Id,
            StudentId = fixture.Student.Id,
            Status = ExperimentBookingStatus.Booked,
            BookedAt = DateTime.UtcNow
        };
        fixture.Db.AddRange(project, session, booking);
        await fixture.Db.SaveChangesAsync();
        var controller = fixture.ExperimentGrades(fixture.AdminScope);

        await controller.CreateSheet(
            new ExperimentGradeSheetRequest(
                project.Id,
                1,
                60,
                [
                    new ExperimentGradeItemRequest(
                        "操作",
                        ExperimentGradeItemKind.Operation,
                        100)
                ]),
            CancellationToken.None);
        var sheet = await fixture.Db.ExperimentGradeSheets.SingleAsync();
        Assert.Single(fixture.Db.ExperimentGradeRecords);

        booking.Status = ExperimentBookingStatus.Cancelled;
        booking.CancelledAt = DateTime.UtcNow;
        await fixture.Db.SaveChangesAsync();
        Assert.IsType<OkObjectResult>(
            await controller.SyncParticipants(
                sheet.Id,
                CancellationToken.None));
        Assert.Empty(fixture.Db.ExperimentGradeRecords);

        fixture.Db.ChangeTracker.Clear();
        booking = await fixture.Db.ExperimentBookings.SingleAsync();
        controller = fixture.ExperimentGrades(fixture.AdminScope);
        booking.Status = ExperimentBookingStatus.Booked;
        booking.CancelledAt = null;
        await fixture.Db.SaveChangesAsync();
        Assert.IsType<OkObjectResult>(
            await controller.SyncParticipants(
                sheet.Id,
                CancellationToken.None));
        Assert.Single(fixture.Db.ExperimentGradeRecords);
    }

    private sealed class ExperimentGradeFixture : IAsyncDisposable
    {
        private ExperimentGradeFixture(
            SqliteConnection connection,
            AppDbContext db,
            AcademicTerm term,
            TeachingTask task,
            Student student,
            Classroom classroom,
            ICurrentUserDataScope adminScope,
            ICurrentUserDataScope teacherScope,
            ICurrentUserDataScope studentScope)
        {
            Connection = connection;
            Db = db;
            Term = term;
            Task = task;
            Student = student;
            Classroom = classroom;
            AdminScope = adminScope;
            TeacherScope = teacherScope;
            StudentScope = studentScope;
        }

        private SqliteConnection Connection { get; }
        public AppDbContext Db { get; }
        public AcademicTerm Term { get; }
        public TeachingTask Task { get; }
        public Student Student { get; }
        public Classroom Classroom { get; }
        public ICurrentUserDataScope AdminScope { get; }
        public ICurrentUserDataScope TeacherScope { get; }
        public ICurrentUserDataScope StudentScope { get; }

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

            var admin = User("lab-admin", "实验管理员");
            var studentUser = User("lab-student", "实验学生");
            var teacherUser = User("lab-teacher", "实验教师");
            var campus = new Campus { Code = "MAIN", Name = "主校区" };
            var building = new Building
            {
                CampusId = campus.Id,
                Code = "LAB",
                Name = "实验楼"
            };
            var classroom = new Classroom
            {
                BuildingId = building.Id,
                Code = "LAB201",
                Name = "实验室 201",
                Capacity = 30
            };
            var college = new College { Code = "CS", Name = "计算机学院" };
            var major = new Major
            {
                CollegeId = college.Id,
                Code = "CS",
                Name = "计算机科学与技术",
                DegreeType = "工学"
            };
            var administrativeClass = new AdministrativeClass
            {
                MajorId = major.Id,
                Code = "CS2099",
                Name = "计科 2099",
                Grade = 2099
            };
            var student = new Student
            {
                UserId = studentUser.Id,
                AdministrativeClassId = administrativeClass.Id,
                StudentNumber = "20990001",
                Name = "实验学生",
                EnrollmentYear = 2099,
                EnrollmentDate = new DateOnly(2099, 9, 1)
            };
            var teacher = new Teacher
            {
                UserId = teacherUser.Id,
                CollegeId = college.Id,
                TeacherNumber = "T2099",
                Name = "实验教师"
            };
            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
            {
                CollegeId = college.Id,
                Code = "CSLAB",
                Name = "系统实验",
                Credits = 2,
                TotalHours = 32,
                LectureHours = 16,
                PracticeHours = 16,
                Nature = CourseNature.Practice,
                AssessmentMethod = AssessmentMethod.Assessment
            };
            var task = new TeachingTask
            {
                AcademicTermId = term.Id,
                CourseId = course.Id,
                TaskNumber = "2099-1-CSLAB-01",
                Name = "系统实验教学班",
                Capacity = 30,
                Status = TeachingTaskStatus.Published,
                Teachers =
                [
                    new TeachingTaskTeacher
                    {
                        TeacherId = teacher.Id,
                        IsPrimary = true
                    }
                ],
                Classes =
                [
                    new TeachingTaskClass
                    {
                        AdministrativeClassId = administrativeClass.Id
                    }
                ]
            };
            db.AddRange(
                admin,
                studentUser,
                teacherUser,
                campus,
                building,
                classroom,
                college,
                major,
                administrativeClass,
                student,
                teacher,
                term,
                course,
                task);
            await db.SaveChangesAsync();

            return new ExperimentGradeFixture(
                connection,
                db,
                term,
                task,
                student,
                classroom,
                Scope(admin, SystemRoles.SuperAdmin, DataScope.All),
                Scope(teacherUser, SystemRoles.Teacher, DataScope.Self),
                Scope(studentUser, SystemRoles.Student, DataScope.Self));
        }

        public ExperimentGradesController ExperimentGrades(
            ICurrentUserDataScope scope) => new(Db, scope);

        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;
    }
}
