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 Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Tests;

public sealed class CourseSelectionsControllerTests
{
    [Fact]
    public async Task Withdrawal_skips_invalid_waiter_and_promotes_next_student()
    {
        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 data = await SeedFullOfferingAsync(db);

        var firstController = new CourseSelectionsController(
            db,
            new StudentDataScope(data.FirstWaiterUserId));
        var firstResult = await firstController.JoinWaitlist(
            new StudentEnrollmentRequest(data.OfferingId),
            CancellationToken.None);
        Assert.IsType<AcceptedResult>(firstResult);

        var secondController = new CourseSelectionsController(
            db,
            new StudentDataScope(data.SecondWaiterUserId));
        var secondResult = await secondController.JoinWaitlist(
            new StudentEnrollmentRequest(data.OfferingId),
            CancellationToken.None);
        Assert.IsType<AcceptedResult>(secondResult);

        db.ChangeTracker.Clear();
        var firstWaiter = await db.Students.SingleAsync(
            x => x.UserId == data.FirstWaiterUserId);
        firstWaiter.Status = StudentStatus.Suspended;
        await db.SaveChangesAsync();

        var enrolledController = new CourseSelectionsController(
            db,
            new StudentDataScope(data.EnrolledUserId));
        var withdrawResult = await enrolledController.Withdraw(
            data.EnrollmentId,
            CancellationToken.None);
        Assert.IsType<OkObjectResult>(withdrawResult);

        db.ChangeTracker.Clear();
        var enrollments = await db.CourseEnrollments
            .OrderBy(x => x.StudentId)
            .ToListAsync();
        Assert.Equal(
            CourseEnrollmentStatus.Withdrawn,
            enrollments.Single(x => x.StudentId == data.EnrolledStudentId).Status);
        Assert.Equal(
            CourseEnrollmentStatus.Expired,
            enrollments.Single(x => x.StudentId == data.FirstWaiterStudentId).Status);
        Assert.Equal(
            CourseEnrollmentStatus.Enrolled,
            enrollments.Single(x => x.StudentId == data.SecondWaiterStudentId).Status);
        Assert.Equal(
            2,
            await db.Notifications.CountAsync(x =>
                x.Title == "课程候补已失效" ||
                x.Title == "课程候补递补成功"));
    }

    [Fact]
    public async Task Closing_round_expires_remaining_waitlist_and_notifies_student()
    {
        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 data = await SeedFullOfferingAsync(db);

        var studentController = new CourseSelectionsController(
            db,
            new StudentDataScope(data.FirstWaiterUserId));
        Assert.IsType<AcceptedResult>(await studentController.JoinWaitlist(
            new StudentEnrollmentRequest(data.OfferingId),
            CancellationToken.None));

        var adminController = new CourseSelectionsController(
            db,
            new AdminDataScope());
        var closeResult = Assert.IsType<OkObjectResult>(await adminController.CloseRound(
            data.RoundId,
            CancellationToken.None));
        Assert.Equal(2, ReadIntProperty(closeResult.Value, "NotifiedStudentCount"));

        db.ChangeTracker.Clear();
        var waitlist = await db.CourseEnrollments.SingleAsync(
            x => x.StudentId == data.FirstWaiterStudentId);
        Assert.Equal(CourseEnrollmentStatus.Expired, waitlist.Status);
        Assert.NotNull(waitlist.WithdrawnAt);
        var resultNotifications = await db.Notifications
            .Where(x => x.Title == "第一轮选课结果")
            .OrderBy(x => x.UserId)
            .ToListAsync();
        Assert.Equal(2, resultNotifications.Count);
        var selectedNotification = Assert.Single(resultNotifications
            .Where(x => x.UserId == data.EnrolledUserId));
        Assert.Contains("最终选中 1 门", selectedNotification.Content);
        Assert.Contains("《程序设计基础》", selectedNotification.Content);
        var waitlistNotification = Assert.Single(resultNotifications
            .Where(x => x.UserId == data.FirstWaiterUserId));
        Assert.Contains("本轮未选中课程", waitlistNotification.Content);
        Assert.Contains("候补未成功 1 门", waitlistNotification.Content);
        Assert.Equal(NotificationCategory.CourseSelection, waitlistNotification.Category);
        Assert.Equal("/course-selections", waitlistNotification.LinkUrl);
    }

    [Fact]
    public async Task Student_cannot_join_waitlist_outside_the_round_grade_scope()
    {
        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 data = await SeedFullOfferingAsync(db);

        db.CourseSelectionRoundGrades.Add(new CourseSelectionRoundGrade
        {
            CourseSelectionRoundId = data.RoundId,
            Grade = 2025
        });
        await db.SaveChangesAsync();

        var controller = new CourseSelectionsController(
            db,
            new StudentDataScope(data.FirstWaiterUserId));
        var result = await controller.JoinWaitlist(
            new StudentEnrollmentRequest(data.OfferingId),
            CancellationToken.None);

        var conflict = Assert.IsType<ConflictObjectResult>(result);
        var problem = Assert.IsType<ProblemDetails>(conflict.Value);
        Assert.Equal("你所在的年级不属于本轮选课对象。", problem.Detail);
        Assert.False(await db.CourseEnrollments.AnyAsync(x =>
            x.StudentId == data.FirstWaiterStudentId));
    }

    [Fact]
    public async Task Updating_round_preserves_an_unchanged_grade_without_duplicate_rows()
    {
        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 data = await SeedFullOfferingAsync(db);

        var round = await db.CourseSelectionRounds.FindAsync(data.RoundId);
        round!.Status = CourseSelectionRoundStatus.Draft;
        db.CourseSelectionRoundGrades.Add(new CourseSelectionRoundGrade
        {
            CourseSelectionRoundId = data.RoundId,
            Grade = 2026
        });
        await db.SaveChangesAsync();
        db.ChangeTracker.Clear();

        var request = new CourseSelectionRoundRequest(
            round.AcademicTermId,
            "2026 级第一轮选课",
            DateTime.UtcNow.AddHours(-1),
            DateTime.UtcNow.AddDays(7),
            DateTime.UtcNow.AddDays(14),
            30,
            6,
            [2026],
            null);
        var controller = new CourseSelectionsController(db, new AdminDataScope());

        Assert.IsType<NoContentResult>(await controller.UpdateRound(
            data.RoundId,
            request,
            CancellationToken.None));
        db.ChangeTracker.Clear();
        Assert.IsType<NoContentResult>(await controller.UpdateRound(
            data.RoundId,
            request,
            CancellationToken.None));
        Assert.Equal(
            1,
            await db.CourseSelectionRoundGrades.CountAsync(x =>
                x.CourseSelectionRoundId == data.RoundId &&
                x.Grade == 2026));
    }

    [Fact]
    public async Task Student_options_include_an_administrator_assigned_offering_outside_the_students_class_scope()
    {
        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 data = await SeedFullOfferingAsync(db);

        var originalClass = await db.AdministrativeClasses.SingleAsync();
        var otherClass = new AdministrativeClass
        {
            Code = "CS2026-02",
            Name = "计科 2026-2 班",
            MajorId = originalClass.MajorId,
            Grade = 2026
        };
        var taskId = await db.CourseSelectionOfferings
            .Where(x => x.Id == data.OfferingId)
            .Select(x => x.TeachingTaskId)
            .SingleAsync();
        var task = await db.TeachingTasks
            .Include(x => x.Classes)
            .SingleAsync(x => x.Id == taskId);
        task.Classes.Clear();
        task.Classes.Add(new TeachingTaskClass { AdministrativeClassId = otherClass.Id });
        task.SchedulingMode = TeachingTaskSchedulingMode.Standard;
        var termId = await db.CourseSelectionRounds
            .Where(x => x.Id == data.RoundId)
            .Select(x => x.AcademicTermId)
            .SingleAsync();
        var plan = new SchedulePlan
        {
            AcademicTermId = termId,
            Name = "正式课表",
            Version = "V1",
            Status = SchedulePlanStatus.Published,
            PublishedAt = DateTime.UtcNow
        };
        db.AddRange(otherClass, plan);
        await db.SaveChangesAsync();
        db.ScheduleEntries.Add(new ScheduleEntry
        {
            SchedulePlanId = plan.Id,
            TeachingTaskId = task.Id,
            DayOfWeek = 1,
            StartPeriod = 1,
            PeriodCount = 2,
            StartWeek = 1,
            EndWeek = 16,
            WeekPattern = WeekPattern.All
        });
        await db.SaveChangesAsync();
        db.ChangeTracker.Clear();

        var controller = new CourseSelectionsController(
            db,
            new StudentDataScope(data.EnrolledUserId));
        var result = Assert.IsType<OkObjectResult>(await controller.GetStudentOptions(
            data.RoundId,
            CancellationToken.None));
        var offerings = ReadProperty<IEnumerable<StudentOfferingDto>>(
            result.Value, "Offerings");
        var assignedOffering = Assert.Single(offerings);

        Assert.Equal(data.OfferingId, assignedOffering.Id);
        Assert.Equal(CourseEnrollmentStatus.Enrolled, assignedOffering.EnrollmentStatus);
        Assert.Single(assignedOffering.Schedules);
    }

    private static async Task<SeededSelection> SeedFullOfferingAsync(AppDbContext db)
    {
        var college = new College { Code = "CS", Name = "计算机学院" };
        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 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 course = new Course
        {
            Code = "CS101",
            Name = "程序设计基础",
            CollegeId = college.Id,
            Credits = 4,
            TotalHours = 64,
            LectureHours = 48,
            PracticeHours = 16,
            Nature = CourseNature.MajorRequired,
            AssessmentMethod = AssessmentMethod.Examination
        };
        var task = new TeachingTask
        {
            TaskNumber = "2026-1-CS101-01",
            Name = "程序设计基础教学班",
            AcademicTermId = term.Id,
            CourseId = course.Id,
            Capacity = 1,
            SchedulingMode = TeachingTaskSchedulingMode.Flexible,
            Status = TeachingTaskStatus.Published,
            Classes =
            [
                new TeachingTaskClass
                {
                    AdministrativeClassId = administrativeClass.Id
                }
            ]
        };
        var round = new CourseSelectionRound
        {
            AcademicTermId = term.Id,
            Name = "第一轮选课",
            StartsAt = DateTime.UtcNow.AddHours(-1),
            EndsAt = DateTime.UtcNow.AddHours(1),
            WithdrawalEndsAt = DateTime.UtcNow.AddHours(2),
            MaxCredits = 30,
            Status = CourseSelectionRoundStatus.Open
        };
        var offering = new CourseSelectionOffering
        {
            CourseSelectionRoundId = round.Id,
            TeachingTaskId = task.Id,
            Capacity = 1
        };
        var enrolledUser = CreateUser("2026001", "已选学生");
        var firstWaiterUser = CreateUser("2026002", "候补甲");
        var secondWaiterUser = CreateUser("2026003", "候补乙");
        var enrolledStudent = CreateStudent(
            "2026001",
            "已选学生",
            enrolledUser.Id,
            administrativeClass.Id);
        var firstWaiter = CreateStudent(
            "2026002",
            "候补甲",
            firstWaiterUser.Id,
            administrativeClass.Id);
        var secondWaiter = CreateStudent(
            "2026003",
            "候补乙",
            secondWaiterUser.Id,
            administrativeClass.Id);
        var enrollment = new CourseEnrollment
        {
            CourseSelectionOfferingId = offering.Id,
            StudentId = enrolledStudent.Id
        };

        db.Add(college);
        await db.SaveChangesAsync();

        db.AddRange(
            major,
            term,
            course,
            enrolledUser,
            firstWaiterUser,
            secondWaiterUser);
        await db.SaveChangesAsync();

        db.Add(administrativeClass);
        await db.SaveChangesAsync();

        db.AddRange(
            enrolledStudent,
            firstWaiter,
            secondWaiter);
        await db.SaveChangesAsync();

        db.AddRange(task, round);
        await db.SaveChangesAsync();

        db.Add(offering);
        await db.SaveChangesAsync();

        db.Add(enrollment);
        await db.SaveChangesAsync();
        return new SeededSelection(
            round.Id,
            offering.Id,
            enrollment.Id,
            enrolledStudent.Id,
            firstWaiter.Id,
            secondWaiter.Id,
            enrolledUser.Id,
            firstWaiterUser.Id,
            secondWaiterUser.Id);
    }

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

    private static int ReadIntProperty(object? value, string propertyName)
    {
        Assert.NotNull(value);
        var property = value.GetType().GetProperty(propertyName);
        Assert.NotNull(property);
        return Assert.IsType<int>(property.GetValue(value));
    }

    private static T ReadProperty<T>(object? value, string propertyName)
    {
        Assert.NotNull(value);
        var property = value.GetType().GetProperty(propertyName);
        Assert.NotNull(property);
        return Assert.IsAssignableFrom<T>(property.GetValue(value));
    }

    private static Student CreateStudent(
        string number,
        string name,
        Guid userId,
        Guid classId) =>
        new()
        {
            StudentNumber = number,
            Name = name,
            UserId = userId,
            AdministrativeClassId = classId,
            EnrollmentYear = 2026,
            EnrollmentDate = new DateOnly(2026, 9, 1),
            Status = StudentStatus.Active
        };

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

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

    private sealed record SeededSelection(
        Guid RoundId,
        Guid OfferingId,
        Guid EnrollmentId,
        Guid EnrolledStudentId,
        Guid FirstWaiterStudentId,
        Guid SecondWaiterStudentId,
        Guid EnrolledUserId,
        Guid FirstWaiterUserId,
        Guid SecondWaiterUserId);
}
