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 ClassroomReservationsControllerTests
{
    [Fact]
    public async Task Create_UsesApplicantsCollegeAndRejectsPublishedClassConflict()
    {
        await using var fixture = await ReservationFixture.CreateAsync();
        var controller = fixture.Controller(fixture.ApplicantScope);
        var conflictedRequest = fixture.Request(
            fixture.ScheduledClassroom.Id,
            fixture.ReservationDate,
            1,
            2);

        var conflicted = await controller.Create(
            conflictedRequest,
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(conflicted);
        Assert.Empty(fixture.Db.ClassroomReservations);

        var created = await controller.Create(
            fixture.Request(
                fixture.AvailableClassroom.Id,
                fixture.ReservationDate,
                1,
                2),
            CancellationToken.None);

        Assert.IsType<CreatedResult>(created);
        var reservation = Assert.Single(fixture.Db.ClassroomReservations);
        Assert.Equal(fixture.Applicant.Id, reservation.ApplicantUserId);
        Assert.Equal(fixture.CollegeA.Id, reservation.ApplicantCollegeId);
        Assert.Equal(ClassroomReservationStatus.Submitted, reservation.Status);
    }

    [Fact]
    public async Task Review_OnlyAllowsApplicantCollegeAndApprovesAvailableRoom()
    {
        await using var fixture = await ReservationFixture.CreateAsync();
        var reservation = fixture.AddSubmittedReservation(
            fixture.AvailableClassroom.Id,
            fixture.ReservationDate,
            3,
            2);
        await fixture.Db.SaveChangesAsync();

        var wrongCollege = await fixture.Controller(fixture.CollegeBReviewerScope)
            .Approve(
                reservation.Id,
                new ReviewClassroomReservationRequest(""),
                CancellationToken.None);

        Assert.IsType<ForbidResult>(wrongCollege);
        Assert.Equal(
            ClassroomReservationStatus.Submitted,
            reservation.Status);

        var approved = await fixture.Controller(fixture.CollegeAReviewerScope)
            .Approve(
                reservation.Id,
                new ReviewClassroomReservationRequest("同意"),
                CancellationToken.None);

        Assert.IsType<NoContentResult>(approved);
        var stored = await fixture.Db.ClassroomReservations
            .AsNoTracking()
            .SingleAsync(item => item.Id == reservation.Id);
        Assert.Equal(ClassroomReservationStatus.Approved, stored.Status);
        Assert.Equal(fixture.CollegeAReviewer.Id, stored.ReviewedByUserId);
    }

    [Fact]
    public async Task Approve_RechecksApprovedReservationConflict()
    {
        await using var fixture = await ReservationFixture.CreateAsync();
        fixture.Db.ClassroomReservations.Add(new ClassroomReservation
        {
            ApplicantUserId = fixture.Applicant.Id,
            ApplicantName = fixture.Applicant.DisplayName,
            ApplicantCollegeId = fixture.CollegeA.Id,
            AcademicTermId = fixture.Term.Id,
            ClassroomId = fixture.AvailableClassroom.Id,
            ReservationDate = fixture.ReservationDate,
            StartPeriod = 5,
            PeriodCount = 2,
            AttendeeCount = 20,
            Purpose = "已批准活动",
            ContactPhone = "13800000000",
            Status = ClassroomReservationStatus.Approved
        });
        var competing = fixture.AddSubmittedReservation(
            fixture.AvailableClassroom.Id,
            fixture.ReservationDate,
            6,
            2);
        await fixture.Db.SaveChangesAsync();

        var result = await fixture.Controller(fixture.CollegeAReviewerScope)
            .Approve(
                competing.Id,
                new ReviewClassroomReservationRequest(null),
                CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(result);
        var stored = await fixture.Db.ClassroomReservations
            .AsNoTracking()
            .SingleAsync(item => item.Id == competing.Id);
        Assert.Equal(ClassroomReservationStatus.Submitted, stored.Status);
    }

    private sealed class ReservationFixture : IAsyncDisposable
    {
        private ReservationFixture(
            SqliteConnection connection,
            AppDbContext db,
            College collegeA,
            ApplicationUser applicant,
            ApplicationUser collegeAReviewer,
            AcademicTerm term,
            Classroom scheduledClassroom,
            Classroom availableClassroom,
            DateOnly reservationDate,
            ICurrentUserDataScope applicantScope,
            ICurrentUserDataScope collegeAReviewerScope,
            ICurrentUserDataScope collegeBReviewerScope)
        {
            Connection = connection;
            Db = db;
            CollegeA = collegeA;
            Applicant = applicant;
            CollegeAReviewer = collegeAReviewer;
            Term = term;
            ScheduledClassroom = scheduledClassroom;
            AvailableClassroom = availableClassroom;
            ReservationDate = reservationDate;
            ApplicantScope = applicantScope;
            CollegeAReviewerScope = collegeAReviewerScope;
            CollegeBReviewerScope = collegeBReviewerScope;
        }

        private SqliteConnection Connection { get; }
        public AppDbContext Db { get; }
        public College CollegeA { get; }
        public ApplicationUser Applicant { get; }
        public ApplicationUser CollegeAReviewer { get; }
        public AcademicTerm Term { get; }
        public Classroom ScheduledClassroom { get; }
        public Classroom AvailableClassroom { get; }
        public DateOnly ReservationDate { get; }
        public ICurrentUserDataScope ApplicantScope { get; }
        public ICurrentUserDataScope CollegeAReviewerScope { get; }
        public ICurrentUserDataScope CollegeBReviewerScope { get; }

        public static async Task<ReservationFixture> 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 campus = new Campus { Code = "MAIN", Name = "主校区" };
            var building = new Building
            {
                Code = "A",
                Name = "明德楼",
                CampusId = campus.Id
            };
            var scheduledClassroom = new Classroom
            {
                Code = "A101",
                Name = "A101",
                BuildingId = building.Id,
                Capacity = 60
            };
            var availableClassroom = new Classroom
            {
                Code = "A102",
                Name = "A102",
                BuildingId = building.Id,
                Capacity = 80
            };
            var collegeA = new College
            {
                Code = "CS",
                Name = "计算机学院"
            };
            var collegeB = new College
            {
                Code = "EE",
                Name = "电子工程学院"
            };
            var applicant = User("student", "测试申请人", collegeA.Id);
            var collegeAReviewer = User(
                "college-a",
                "计算机学院审核人",
                collegeA.Id);
            var collegeBReviewer = User(
                "college-b",
                "电子工程学院审核人",
                collegeB.Id);
            var term = new AcademicTerm
            {
                Code = "2099-1",
                Name = "2099—2100 学年第一学期",
                AcademicYear = "2099-2100",
                Season = TermSeason.Autumn,
                StartDate = new DateOnly(2099, 9, 1),
                EndDate = new DateOnly(2100, 1, 31),
                IsCurrent = true
            };
            var course = new Course
            {
                Code = "CS101",
                Name = "程序设计基础",
                CollegeId = collegeA.Id,
                Credits = 3,
                TotalHours = 48,
                LectureHours = 32,
                PracticeHours = 16,
                Nature = CourseNature.MajorRequired,
                AssessmentMethod = AssessmentMethod.Examination
            };
            var teachingTask = new TeachingTask
            {
                TaskNumber = "2099-1-CS101-01",
                Name = "程序设计基础教学班",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 60,
                Status = TeachingTaskStatus.Published
            };
            var reservationDate = term.StartDate;
            var (_, dayOfWeek) =
                ClassroomReservationAvailabilityService.ResolveTeachingWeek(
                    term,
                    reservationDate);
            var plan = new SchedulePlan
            {
                AcademicTermId = term.Id,
                Name = "正式课表",
                Version = "v1",
                Status = SchedulePlanStatus.Published,
                Entries =
                [
                    new ScheduleEntry
                    {
                        TeachingTaskId = teachingTask.Id,
                        ClassroomId = scheduledClassroom.Id,
                        DayOfWeek = dayOfWeek,
                        StartPeriod = 1,
                        PeriodCount = 2,
                        StartWeek = 1,
                        EndWeek = 18,
                        WeekPattern = WeekPattern.All
                    }
                ]
            };
            db.AddRange(
                campus,
                building,
                scheduledClassroom,
                availableClassroom,
                collegeA,
                collegeB,
                applicant,
                collegeAReviewer,
                collegeBReviewer,
                term,
                course,
                teachingTask,
                plan);
            await db.SaveChangesAsync();

            return new ReservationFixture(
                connection,
                db,
                collegeA,
                applicant,
                collegeAReviewer,
                term,
                scheduledClassroom,
                availableClassroom,
                reservationDate,
                Scope(applicant, SystemRoles.Student),
                Scope(collegeAReviewer, SystemRoles.CollegeAdmin),
                Scope(collegeBReviewer, SystemRoles.CollegeAdmin));
        }

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

        public CreateClassroomReservationRequest Request(
            Guid classroomId,
            DateOnly date,
            int startPeriod,
            int periodCount) =>
            new(
                Term.Id,
                classroomId,
                date,
                startPeriod,
                periodCount,
                30,
                "学院学术活动",
                "13800000000",
                null);

        public ClassroomReservation AddSubmittedReservation(
            Guid classroomId,
            DateOnly date,
            int startPeriod,
            int periodCount)
        {
            var reservation = new ClassroomReservation
            {
                ApplicantUserId = Applicant.Id,
                ApplicantName = Applicant.DisplayName,
                ApplicantCollegeId = CollegeA.Id,
                AcademicTermId = Term.Id,
                ClassroomId = classroomId,
                ReservationDate = date,
                StartPeriod = startPeriod,
                PeriodCount = periodCount,
                AttendeeCount = 20,
                Purpose = "学院交流活动",
                ContactPhone = "13800000000"
            };
            Db.ClassroomReservations.Add(reservation);
            return reservation;
        }

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

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

        private static ICurrentUserDataScope Scope(
            ApplicationUser user,
            string role) =>
            new FixedScope(new CurrentUserScope(
                user.Id,
                user.DisplayName,
                user.CollegeId,
                role == SystemRoles.CollegeAdmin
                    ? DataScope.College
                    : DataScope.Self,
                new HashSet<string>([role])));
    }

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