using ClosedXML.Excel;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;

namespace Jiaowu.Api.Tests;

public sealed class TeachingWorkflowRosterTests
{
    [Fact]
    public async Task ClassAssignedStudents_AppearAcrossTeachingAndExamWorkflows()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var teacherUserId = Guid.NewGuid();
        var studentUserId = Guid.NewGuid();
        var teacherUser = new ApplicationUser
        {
            Id = teacherUserId,
            UserName = "T001",
            NormalizedUserName = "T001",
            DisplayName = "张老师"
        };
        var studentUser = new ApplicationUser
        {
            Id = studentUserId,
            UserName = "202601001",
            NormalizedUserName = "202601001",
            DisplayName = "周同学"
        };
        var campus = new Campus { Code = "MAIN", Name = "主校区" };
        var college = new College
        {
            Code = "CS",
            Name = "计算机学院",
            CampusId = campus.Id
        };
        var building = new Building
        {
            Code = "B01",
            Name = "第一教学楼",
            CampusId = campus.Id
        };
        var classroom = new Classroom
        {
            Code = "B01-101",
            Name = "101",
            BuildingId = building.Id,
            Capacity = 60
        };
        var major = new Major
        {
            Code = "080901",
            Name = "计算机科学与技术",
            CollegeId = college.Id,
            DegreeType = "工学学士"
        };
        var administrativeClass = new AdministrativeClass
        {
            Code = "CS2026-01",
            Name = "计科 2026-1 班",
            MajorId = major.Id,
            Grade = 2026
        };
        var student = new Student
        {
            StudentNumber = "202601001",
            Name = "周同学",
            AdministrativeClassId = administrativeClass.Id,
            EnrollmentYear = 2026,
            EnrollmentDate = new DateOnly(2026, 9, 1),
            UserId = studentUserId
        };
        var teacher = new Teacher
        {
            TeacherNumber = "T001",
            Name = "张老师",
            CollegeId = college.Id,
            UserId = teacherUserId
        };
        var course = new Course
        {
            Code = "CS101",
            Name = "程序设计基础",
            CollegeId = college.Id,
            Credits = 4,
            TotalHours = 64,
            LectureHours = 48,
            PracticeHours = 16,
            Nature = CourseNature.MajorRequired,
            AssessmentMethod = AssessmentMethod.Examination
        };
        var term = new AcademicTerm
        {
            Code = "2026-1",
            Name = "2026—2027 学年第一学期",
            AcademicYear = "2026-2027",
            Season = TermSeason.Autumn,
            StartDate = new DateOnly(2026, 9, 1),
            EndDate = new DateOnly(2027, 1, 20)
        };
        var task = new TeachingTask
        {
            TaskNumber = "2026-1-CS101-01",
            Name = "程序设计基础教学班",
            AcademicTermId = term.Id,
            CourseId = course.Id,
            Capacity = 60,
            Status = TeachingTaskStatus.Published,
            Teachers =
            [
                new TeachingTaskTeacher
                {
                    TeacherId = teacher.Id,
                    IsPrimary = true
                }
            ],
            Classes =
            [
                new TeachingTaskClass
                {
                    AdministrativeClassId = administrativeClass.Id
                }
            ]
        };
        db.AddRange(
            teacherUser,
            studentUser,
            campus,
            college,
            building,
            classroom,
            major,
            administrativeClass,
            student,
            teacher,
            course,
            term,
            task);
        await db.SaveChangesAsync();

        var examSession = new ExamSession
        {
            TeachingTaskId = task.Id,
            ExamDate = new DateOnly(2027, 1, 8),
            StartPeriod = 1,
            PeriodCount = 2,
            StartsAt = new DateTime(2027, 1, 8, 8, 0, 0, DateTimeKind.Utc),
            EndsAt = new DateTime(2027, 1, 8, 9, 50, 0, DateTimeKind.Utc),
            RequiredInvigilatorCount = 1
        };
        var examPlan = new ExamPlan
        {
            AcademicTermId = term.Id,
            Name = "期末考试",
            Status = ExamPlanStatus.Draft,
            Sessions = [examSession],
            Rooms =
            [
                new ExamRoomAssignment
                {
                    CourseId = course.Id,
                    ClassroomId = classroom.Id,
                    ExamDate = new DateOnly(2027, 1, 8),
                    StartPeriod = 1,
                    PeriodCount = 2,
                    StartsAt = new DateTime(2027, 1, 8, 8, 0, 0, DateTimeKind.Utc),
                    EndsAt = new DateTime(2027, 1, 8, 9, 50, 0, DateTimeKind.Utc),
                    RequiredInvigilatorCount = 1,
                    SessionLinks =
                    [
                        new ExamRoomSession
                        {
                            ExamSessionId = examSession.Id
                        }
                    ],
                    Seats =
                    [
                        new ExamSeatAssignment
                        {
                            ExamSessionId = examSession.Id,
                            StudentId = student.Id,
                            SeatNumber = 1
                        }
                    ],
                    Invigilators =
                    [
                        new ExamRoomInvigilator
                        {
                            TeacherId = teacher.Id
                        }
                    ]
                }
            ]
        };
        db.ExamPlans.Add(examPlan);
        await db.SaveChangesAsync();

        var scope = new TeacherDataScope(teacherUserId);
        var attendance = new AttendanceController(db, scope);
        var attendanceResult = await attendance.CreateSheet(
            new AttendanceSheetRequest(
                task.Id,
                "第1周点名",
                new DateTime(2026, 9, 3),
                null,
                AttendanceCheckInMethod.Manual,
                null,
                null,
                null,
                null),
            CancellationToken.None);
        Assert.IsType<CreatedResult>(attendanceResult);
        Assert.Equal(1, await db.AttendanceRecords.CountAsync());

        db.ChangeTracker.Clear();
        var grades = new GradesController(db, scope);
        var gradeResult = await grades.CreateSheet(
            new GradeSheetRequest(task.Id, 40, 60, []),
            CancellationToken.None);
        Assert.IsType<CreatedResult>(gradeResult);
        Assert.Equal(1, await db.GradeRecords.CountAsync());

        var makeupPlan = new MakeupExamPlan
        {
            AcademicTermId = term.Id,
            Name = "补考成绩录入测试",
            Status = MakeupExamPlanStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        var makeupSession = new MakeupExamSession
        {
            MakeupExamPlanId = makeupPlan.Id,
            TeachingTaskId = task.Id,
            ExamDate = new DateOnly(2027, 2, 20),
            StartPeriod = 1,
            PeriodCount = 2,
            StartsAt = new DateTime(2027, 2, 20, 8, 0, 0, DateTimeKind.Utc),
            EndsAt = new DateTime(2027, 2, 20, 9, 50, 0, DateTimeKind.Utc),
            Enrollments =
            [
                new MakeupExamEnrollment
                {
                    StudentId = student.Id,
                    Reason = MakeupReason.Failed
                }
            ]
        };
        makeupPlan.Sessions.Add(makeupSession);
        db.MakeupExamPlans.Add(makeupPlan);
        await db.SaveChangesAsync();
        db.ChangeTracker.Clear();

        var makeup = new MakeupExamsController(
            db,
            scope,
            new MakeupExamEligibilityService(db));
        Assert.IsType<NoContentResult>(await makeup.RecordScores(
            makeupSession.Id,
            [new RecordMakeupScoreRequest(student.Id, 75)],
            CancellationToken.None));
        Assert.Equal(
            75,
            await db.MakeupExamEnrollments
                .Where(x => x.MakeupExamSessionId == makeupSession.Id)
                .Select(x => x.MakeupScore)
                .SingleAsync());

        var selections = new CourseSelectionsController(db, scope);
        Assert.Single(ReadItems(await selections.GetMyTeachingTasks(
            term.Id,
            CancellationToken.None)));
        var roster = Assert.IsType<OkObjectResult>(
            await selections.GetTeachingTaskRoster(task.Id, CancellationToken.None));
        Assert.Equal(1, ReadIntProperty(roster.Value!, "StudentCount"));

        var adjustments = new CourseAdjustmentsController(db, scope);
        Assert.Single(ReadItems(await adjustments.GetTaskOptions(
            term.Id,
            CancellationToken.None)));

        var managerScope = new ManagerDataScope();
        var exams = new ExamsController(
            db,
            managerScope,
            NoOpAppCache.Instance);
        var publishResult = await exams.Publish(examPlan.Id, CancellationToken.None);
        var acceptedPublish = Assert.IsType<AcceptedAtActionResult>(publishResult);
        var publishJobId = (Guid)acceptedPublish.Value!
            .GetType().GetProperty("jobId")!.GetValue(acceptedPublish.Value)!;
        var publishProcessor = new ExamPublishJobProcessor(
            db,
            NoOpAppCache.Instance,
            NullLogger<ExamPublishJobProcessor>.Instance);
        await publishProcessor.ProcessAsync(publishJobId, CancellationToken.None);

        var planResult = Assert.IsType<OkObjectResult>(
            await exams.GetPlan(
                examPlan.Id,
                cancellationToken: CancellationToken.None));
        var planSessions = ReadEnumerableProperty(planResult.Value!, "Sessions");
        Assert.Equal(1, ReadIntProperty(planSessions.Single(), "StudentCount"));
        var returnedRoom = Assert.Single(
            ReadEnumerableProperty(planSessions.Single(), "ExamRooms"));
        Assert.Equal(
            classroom.Id,
            returnedRoom.GetType().GetProperty("ClassroomId")!.GetValue(returnedRoom));
        Assert.Equal(1, ReadIntProperty(returnedRoom, "SeatCount"));
        Assert.Equal(1, ReadIntProperty(returnedRoom, "TotalSeatCount"));
        Assert.False(
            (bool)returnedRoom.GetType().GetProperty("IsMixed")!
                .GetValue(returnedRoom)!);
        Assert.Equal(
            [teacher.Id],
            ReadEnumerableProperty(returnedRoom, "InvigilatorIds").Cast<Guid>());
        Assert.Equal(
            [teacher.Name],
            ReadEnumerableProperty(returnedRoom, "InvigilatorNames").Cast<string>());

        var examSessionId = examPlan.Sessions.Single().Id;
        var rosterResult = Assert.IsType<OkObjectResult>(
            await exams.GetRoster(examSessionId, CancellationToken.None));
        Assert.Single(ReadEnumerableProperty(rosterResult.Value!, "Students"));

        var studentExams = new ExamsController(
            db,
            new StudentDataScope(studentUserId),
            NoOpAppCache.Instance);
        Assert.Single(ReadItems(
            await studentExams.GetMySchedule(CancellationToken.None)));
        var teacherExams = new ExamsController(
            db,
            scope,
            NoOpAppCache.Instance);
        Assert.Single(ReadItems(
            await teacherExams.GetMySchedule(CancellationToken.None)));

        var exportResult = Assert.IsType<FileContentResult>(
            await exams.ExportSignInSheets(examPlan.Id, CancellationToken.None));
        using var workbookStream = new MemoryStream(exportResult.FileContents);
        using var workbook = new XLWorkbook(workbookStream);
        Assert.Equal(2, workbook.Worksheets.Count);
        var signInSheet = workbook.Worksheets.Single(x => x.Name != "考场汇总");
        Assert.Equal("202601001", signInSheet.Cell("C8").GetString());
        Assert.Equal("周同学", signInSheet.Cell("D8").GetString());
    }

    private static List<object> ReadItems(ActionResult result)
    {
        var ok = Assert.IsType<OkObjectResult>(result);
        return Assert.IsAssignableFrom<System.Collections.IEnumerable>(ok.Value)
            .Cast<object>()
            .ToList();
    }

    private static int ReadIntProperty(object value, string name) =>
        (int)value.GetType().GetProperty(name)!.GetValue(value)!;

    private static List<object> ReadEnumerableProperty(object value, string name) =>
        Assert.IsAssignableFrom<System.Collections.IEnumerable>(
                value.GetType().GetProperty(name)!.GetValue(value))
            .Cast<object>()
            .ToList();

    private sealed class TeacherDataScope(Guid userId) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            userId,
            "张老师",
            null,
            DataScope.Self,
            new HashSet<string>([SystemRoles.Teacher]));
    }

    private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            userId,
            "周同学",
            null,
            DataScope.Self,
            new HashSet<string>([SystemRoles.Student]));
    }

    private sealed class ManagerDataScope : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            Guid.NewGuid(),
            "考试管理员",
            null,
            DataScope.All,
            new HashSet<string>([SystemRoles.AcademicAdmin]));
    }
}
