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 CentralizedProject_PublishesAndUsesTeachingTaskRoster()
    {
        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<CreatedResult>(sessionResult);
        Assert.Equal(
            1,
            (await fixture.Db.ExperimentSessions.SingleAsync()).Capacity);

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

        var session = await fixture.Db.ExperimentSessions.SingleAsync();
        var participants = await controller.GetParticipants(
            session.Id,
            CancellationToken.None);
        var ok = Assert.IsType<OkObjectResult>(participants);
        var rows = Assert.IsAssignableFrom<IEnumerable<object>>(ok.Value);
        Assert.Single(rows);
    }

    [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.Equal(2, timetable.ExperimentEntries.Count);
        Assert.Contains(
            timetable.ExperimentEntries,
            x => x.ExperimentArrangementMode ==
                 ExperimentArrangementMode.Centralized);
        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.Single(timetable.ExperimentEntries);
        Assert.Equal(
            ExperimentArrangementMode.Centralized,
            timetable.ExperimentEntries[0].ExperimentArrangementMode);
    }

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

        private SqliteConnection Connection { get; }
        public AppDbContext Db { get; }
        public AcademicTerm Term { get; }
        public TeachingTask Task { get; }
        public Classroom Classroom { get; }
        public Classroom SecondClassroom { 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
            };
            var secondClassroom = new Classroom
            {
                Code = "LAB102",
                Name = "实验室 102",
                BuildingId = building.Id,
                Capacity = 40
            };
            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
            };
            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
                    }
                ]
            };
            db.AddRange(
                manager,
                studentUser,
                campus,
                building,
                classroom,
                secondClassroom,
                college,
                major,
                administrativeClass,
                student,
                teacher,
                term,
                course,
                task);
            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();

            return new ExperimentFixture(
                connection,
                db,
                term,
                task,
                classroom,
                secondClassroom,
                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));

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

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