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

namespace Jiaowu.Api.Tests;

public sealed class AttendanceControllerTests
{
    [Fact]
    public async Task TaskStatistics_UsesSubmittedSheetsAndExportsAllDetails()
    {
        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 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 studentUserId = Guid.NewGuid();
        var studentUser = new ApplicationUser
        {
            Id = studentUserId,
            UserName = "202601001",
            NormalizedUserName = "202601001",
            DisplayName = "周同学"
        };
        var firstStudent = new Student
        {
            StudentNumber = "202601001",
            Name = "周同学",
            UserId = studentUserId,
            AdministrativeClassId = administrativeClass.Id,
            EnrollmentYear = 2026,
            EnrollmentDate = new DateOnly(2026, 9, 1)
        };
        var secondStudentUserId = Guid.NewGuid();
        var secondStudentUser = new ApplicationUser
        {
            Id = secondStudentUserId,
            UserName = "202601002",
            NormalizedUserName = "202601002",
            DisplayName = "吴同学"
        };
        var secondStudent = new Student
        {
            StudentNumber = "202601002",
            Name = "吴同学",
            UserId = secondStudentUserId,
            AdministrativeClassId = administrativeClass.Id,
            EnrollmentYear = 2026,
            EnrollmentDate = new DateOnly(2026, 9, 1)
        };
        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
        };
        db.AddRange(
            studentUser,
            secondStudentUser,
            college,
            major,
            administrativeClass,
            firstStudent,
            secondStudent,
            course,
            term,
            task);
        await db.SaveChangesAsync();

        db.AttendanceSheets.AddRange(
            new AttendanceSheet
            {
                TeachingTaskId = task.Id,
                Name = "第1周点名",
                AttendanceDate = new DateTime(2026, 9, 3),
                Status = AttendanceSheetStatus.Submitted,
                Records =
                [
                    new AttendanceRecord
                    {
                        StudentId = firstStudent.Id,
                        Status = AttendanceStatus.Present
                    },
                    new AttendanceRecord
                    {
                        StudentId = secondStudent.Id,
                        Status = AttendanceStatus.Absent
                    }
                ]
            },
            new AttendanceSheet
            {
                TeachingTaskId = task.Id,
                Name = "第2周点名",
                AttendanceDate = new DateTime(2026, 9, 10),
                Status = AttendanceSheetStatus.Submitted,
                Records =
                [
                    new AttendanceRecord
                    {
                        StudentId = firstStudent.Id,
                        Status = AttendanceStatus.Late
                    },
                    new AttendanceRecord
                    {
                        StudentId = secondStudent.Id,
                        Status = AttendanceStatus.Excused
                    }
                ]
            },
            new AttendanceSheet
            {
                TeachingTaskId = task.Id,
                Name = "未提交点名",
                AttendanceDate = new DateTime(2026, 9, 17),
                Status = AttendanceSheetStatus.Draft,
                Records =
                [
                    new AttendanceRecord
                    {
                        StudentId = firstStudent.Id,
                        Status = AttendanceStatus.Absent
                    }
                ]
            });
        await db.SaveChangesAsync();

        var controller = new AttendanceController(db, new AllDataScope());
        var result = await controller.GetTaskStatistics(
            task.Id,
            CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result);
        var statistics = Assert.IsType<AttendanceCourseStatistics>(ok.Value);
        Assert.Equal(2, statistics.Summary.SubmittedSheetCount);
        Assert.Equal(4, statistics.Summary.RecordCount);
        Assert.Equal(3, statistics.Summary.RequiredCount);
        Assert.Equal(66.7m, statistics.Summary.OverallAttendanceRate);
        Assert.Equal(0, statistics.Summary.PerfectAttendanceCount);
        Assert.Equal(2, statistics.Students.Count);
        Assert.Equal(0m, statistics.Students[0].AttendanceRate);
        Assert.Equal(100m, statistics.Students[1].AttendanceRate);

        var exportResult = await controller.ExportTaskStatistics(
            task.Id,
            CancellationToken.None);
        var file = Assert.IsType<FileContentResult>(exportResult);
        using var workbook = new XLWorkbook(new MemoryStream(file.FileContents));
        Assert.NotNull(workbook.Worksheet("课程概览"));
        Assert.Equal(
            3,
            workbook.Worksheet("学生出勤明细").LastRowUsed()!.RowNumber());
        Assert.Equal(
            3,
            workbook.Worksheet("历次点名趋势").LastRowUsed()!.RowNumber());

        var now = DateTime.UtcNow;
        var qrRecord = new AttendanceRecord
        {
            StudentId = firstStudent.Id,
            Status = AttendanceStatus.Absent
        };
        var secondQrRecord = new AttendanceRecord
        {
            StudentId = secondStudent.Id,
            Status = AttendanceStatus.Absent
        };
        var qrSheet = new AttendanceSheet
        {
            TeachingTaskId = task.Id,
            Name = "课堂扫码签到",
            AttendanceDate = now,
            Status = AttendanceSheetStatus.Draft,
            CheckInMethod = AttendanceCheckInMethod.QrCode,
            CheckInToken = "TEST-QR-TOKEN",
            CheckInStartsAt = now.AddMinutes(-1),
            CheckInEndsAt = now.AddMinutes(10),
            Records = [qrRecord, secondQrRecord]
        };
        db.AttendanceSheets.Add(qrSheet);
        await db.SaveChangesAsync();
        var qrChallengeResult = await controller.GetQrChallenge(
            qrSheet.Id,
            CancellationToken.None);
        var qrChallengeOk = Assert.IsType<OkObjectResult>(qrChallengeResult);
        var qrToken = Assert.IsType<string>(
            qrChallengeOk.Value!.GetType().GetProperty("Token")!.GetValue(
                qrChallengeOk.Value));

        var studentController = new AttendanceController(
            db,
            new StudentDataScope(studentUserId));
        var myRecordsResult = await studentController.GetMyRecords(
            null,
            CancellationToken.None);
        var myRecordsOk = Assert.IsType<OkObjectResult>(myRecordsResult);
        var myRecords = Assert
            .IsAssignableFrom<System.Collections.IEnumerable>(myRecordsOk.Value)
            .Cast<object>()
            .ToList();
        Assert.Equal(2, myRecords.Count);
        Assert.All(myRecords, item =>
            Assert.Equal(
                task.Id,
                item.GetType().GetProperty("TeachingTaskId")!.GetValue(item)));

        var infoResult = await studentController.GetCheckInInfo(
            qrToken,
            CancellationToken.None);
        Assert.IsType<OkObjectResult>(infoResult);
        Assert.IsType<NotFoundResult>(
            await studentController.GetCheckInInfo(
                qrSheet.CheckInToken,
                CancellationToken.None));

        var qrCheckInResult = await studentController.CheckIn(
            new AttendanceCheckInRequest(
                null,
                qrToken,
                null,
                null,
                null,
                "test-device-1",
                "android"),
            CancellationToken.None);
        Assert.IsType<OkObjectResult>(qrCheckInResult);
        Assert.Equal(AttendanceStatus.Present, qrRecord.Status);
        Assert.Equal(AttendanceCheckInMethod.QrCode, qrRecord.CheckedInMethod);
        Assert.NotNull(qrRecord.CheckInAt);
        Assert.Null(qrRecord.CheckInLatitude);
        var qrAttempt = await db.AttendanceCheckInAttempts.SingleAsync(
            x => x.AttendanceSheetId == qrSheet.Id);
        Assert.True(qrAttempt.IsSuccessful);
        Assert.Equal("android", qrAttempt.DevicePlatform);
        Assert.NotNull(qrAttempt.DeviceIdentifierHash);

        var secondStudentController = new AttendanceController(
            db,
            new StudentDataScope(secondStudentUserId));
        Assert.IsType<OkObjectResult>(
            await secondStudentController.CheckIn(
                new AttendanceCheckInRequest(
                    null,
                    qrToken,
                    null,
                    null,
                    null,
                    "test-device-1",
                    "android"),
                CancellationToken.None));
        var sharedDeviceAttempt = await db.AttendanceCheckInAttempts.SingleAsync(
            x => x.AttendanceSheetId == qrSheet.Id &&
                 x.StudentId == secondStudent.Id);
        Assert.Contains(
            "SharedDevice",
            sharedDeviceAttempt.RiskFlags ?? string.Empty);
        var sheetDetailResult = await controller.GetSheet(
            qrSheet.Id,
            CancellationToken.None);
        var sheetDetailOk = Assert.IsType<OkObjectResult>(sheetDetailResult);
        var sheetDetail = sheetDetailOk.Value!.GetType()
            .GetProperty("Sheet")!
            .GetValue(sheetDetailOk.Value)!;
        var riskSummary = sheetDetail.GetType()
            .GetProperty("RiskSummary")!
            .GetValue(sheetDetail)!;
        Assert.Equal(
            2,
            riskSummary.GetType()
                .GetProperty("SharedDeviceStudentCount")!
                .GetValue(riskSummary));

        var locationRecord = new AttendanceRecord
        {
            StudentId = firstStudent.Id,
            Status = AttendanceStatus.Absent
        };
        var locationSheet = new AttendanceSheet
        {
            TeachingTaskId = task.Id,
            Name = "课堂定位签到",
            AttendanceDate = now,
            Status = AttendanceSheetStatus.Draft,
            CheckInMethod = AttendanceCheckInMethod.Location,
            CheckInStartsAt = now.AddMinutes(-1),
            CheckInEndsAt = now.AddMinutes(10),
            TargetLatitude = 39.9m,
            TargetLongitude = 116.4m,
            LocationRadiusMeters = 100,
            Records = [locationRecord]
        };
        db.AttendanceSheets.Add(locationSheet);
        await db.SaveChangesAsync();

        var outsideResult = await studentController.CheckIn(
            new AttendanceCheckInRequest(
                locationSheet.Id,
                null,
                39.91m,
                116.4m,
                8),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(outsideResult);
        Assert.Null(locationRecord.CheckInAt);

        var inaccurateResult = await studentController.CheckIn(
            new AttendanceCheckInRequest(
                locationSheet.Id,
                null,
                39.9001m,
                116.4m,
                150),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(inaccurateResult);
        Assert.Null(locationRecord.CheckInAt);

        var missingAccuracyResult = await studentController.CheckIn(
            new AttendanceCheckInRequest(
                locationSheet.Id,
                null,
                39.9001m,
                116.4m,
                null),
            CancellationToken.None);
        Assert.IsType<ConflictObjectResult>(missingAccuracyResult);
        Assert.Null(locationRecord.CheckInAt);
        var locationFailures = await db.AttendanceCheckInAttempts
            .Where(x =>
                x.AttendanceSheetId == locationSheet.Id &&
                !x.IsSuccessful)
            .OrderBy(x => x.CreatedAt)
            .ToListAsync();
        Assert.Equal(3, locationFailures.Count);
        Assert.Contains(
            locationFailures,
            x => (x.RiskFlags ?? string.Empty).Contains(
                "RepeatedFailures",
                StringComparison.Ordinal));

        var nearbyResult = await studentController.CheckIn(
            new AttendanceCheckInRequest(
                locationSheet.Id,
                null,
                39.9001m,
                116.4m,
                8),
            CancellationToken.None);
        Assert.IsType<OkObjectResult>(nearbyResult);
        Assert.Equal(AttendanceStatus.Present, locationRecord.Status);
        Assert.Equal(
            AttendanceCheckInMethod.Location,
            locationRecord.CheckedInMethod);
        Assert.InRange(locationRecord.CheckInDistanceMeters!.Value, 1, 100);
    }

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

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