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.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Jiaowu.Api.Tests;

public sealed class NotificationsControllerTests
{
    [Fact]
    public void Send_message_request_validation_metadata_is_compatible_with_mvc_record_binding()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddControllers()
            .AddApplicationPart(typeof(NotificationsController).Assembly);

        using var serviceProvider = services.BuildServiceProvider();
        var objectValidator = serviceProvider.GetRequiredService<IObjectModelValidator>();
        var httpContext = new DefaultHttpContext
        {
            RequestServices = serviceProvider
        };
        var actionContext = new ActionContext(
            httpContext,
            new RouteData(),
            new ActionDescriptor(),
            new ModelStateDictionary());
        var request = new SendMessageRequest(
            "校内通知",
            "<p>这是一条用于验证请求模型绑定的通知。</p>");

        var exception = Record.Exception(() =>
            objectValidator.Validate(actionContext, null, string.Empty, request));

        Assert.Null(exception);
        Assert.True(actionContext.ModelState.IsValid);
    }

    [Fact]
    public async Task College_administrator_can_only_send_to_enabled_users_in_own_college()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.CollegeAdmin));

        var result = await controller.Send(
            new SendMessageRequest(
                "学院通知",
                "请按时完成教学材料提交。"),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        var notifications = await fixture.Db.Notifications
            .Include(x => x.MessageDispatch)
            .ToListAsync();
        Assert.Equal(
            new[]
            {
                fixture.ClassStudentUser.Id,
                fixture.CollegeRecipient.Id,
                fixture.TeacherUser.Id
            }.Order(),
            notifications.Select(x => x.UserId).Order());
        var dispatch = Assert.Single(
            notifications.Select(x => x.MessageDispatch).Distinct());
        Assert.Equal("第一学院全院成员", dispatch!.AudienceName);
        Assert.Equal(3, dispatch.RecipientCount);
    }

    [Fact]
    public async Task Teacher_can_send_notice_only_to_own_teaching_class()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.TeacherUser.Id,
                fixture.FirstCollege.Id,
                SystemRoles.Teacher));

        var result = await controller.Send(
            new SendMessageRequest(
                "课程资料提醒",
                "请在规定时间内完成课程资料提交。",
                fixture.OwnTask.Id),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        var notification = Assert.Single(await fixture.Db.Notifications
            .Include(x => x.MessageDispatch)
            .ToListAsync());
        Assert.Equal(fixture.ClassStudentUser.Id, notification.UserId);
        Assert.Equal(NotificationCategory.General, notification.Category);
        Assert.Null(notification.LinkUrl);
        Assert.Equal(
            MessageAudienceType.TeachingTask,
            notification.MessageDispatch!.AudienceType);

        fixture.Db.Notifications.Remove(notification);
        fixture.Db.MessageDispatches.Remove(notification.MessageDispatch);
        await fixture.Db.SaveChangesAsync();

        var forbiddenTask = await controller.Send(
            new SendMessageRequest(
                "越权消息",
                "不应发送。",
                fixture.OtherTask.Id),
            CancellationToken.None);

        Assert.IsType<ConflictObjectResult>(forbiddenTask);
        Assert.Empty(await fixture.Db.Notifications.ToListAsync());
    }

    [Fact]
    public async Task School_administrator_reaches_all_enabled_accounts_except_self()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.AcademicAdmin));

        var result = await controller.Send(
            new SendMessageRequest(
                "全校通知",
                "本周五进行系统维护。"),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        var recipientIds = await fixture.Db.Notifications
            .Select(x => x.UserId)
            .ToListAsync();
        Assert.DoesNotContain(fixture.Sender.Id, recipientIds);
        Assert.DoesNotContain(fixture.DisabledRecipient.Id, recipientIds);
        Assert.Contains(fixture.CollegeRecipient.Id, recipientIds);
        Assert.Contains(fixture.OutsideRecipient.Id, recipientIds);
        Assert.Contains(fixture.TeacherUser.Id, recipientIds);
        Assert.Contains(fixture.ClassStudentUser.Id, recipientIds);
    }

    [Fact]
    public async Task School_administrator_can_filter_recipients_by_college()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.AcademicAdmin));

        var result = await controller.Send(
            new SendMessageRequest(
                "第二学院通知",
                "<p><strong>仅发送</strong>给第二学院。</p>",
                RecipientMode: MessageRecipientMode.Filtered,
                RecipientFilter: new MessageRecipientFilter(
                    CollegeId: fixture.SecondCollege.Id)),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        var notification = Assert.Single(
            await fixture.Db.Notifications.ToListAsync());
        Assert.Equal(fixture.OutsideRecipient.Id, notification.UserId);
        var dispatch = await fixture.Db.MessageDispatches.SingleAsync();
        Assert.Equal(MessageAudienceType.Custom, dispatch.AudienceType);
        Assert.Equal("指定学院", dispatch.AudienceName);
    }

    [Fact]
    public async Task College_administrator_cannot_select_recipient_outside_college()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.CollegeAdmin));

        var result = await controller.Send(
            new SendMessageRequest(
                "越权消息",
                "<p>不应发送。</p>",
                RecipientMode: MessageRecipientMode.Selected,
                RecipientUserIds: [fixture.OutsideRecipient.Id]),
            CancellationToken.None);

        Assert.IsType<BadRequestObjectResult>(result);
        Assert.Empty(await fixture.Db.Notifications.ToListAsync());
        Assert.Empty(await fixture.Db.MessageDispatches.ToListAsync());
    }

    [Fact]
    public async Task Administrator_can_send_rich_content_larger_than_legacy_limit()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.AcademicAdmin));
        var content = $"<h2>教学安排</h2><p>{new string('内', 1500)}</p>";

        var result = await controller.Send(
            new SendMessageRequest(
                "富文本通知",
                content,
                RecipientMode: MessageRecipientMode.Selected,
                RecipientUserIds: [fixture.ClassStudentUser.Id]),
            CancellationToken.None);

        Assert.IsType<OkObjectResult>(result);
        var notification = await fixture.Db.Notifications.SingleAsync();
        Assert.Equal(content, notification.Content);
        Assert.Equal(fixture.ClassStudentUser.Id, notification.UserId);
    }

    [Fact]
    public async Task Mark_all_read_only_updates_the_current_users_unread_notifications()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        fixture.Db.Notifications.AddRange(
            new Notification
            {
                UserId = fixture.Sender.Id,
                Title = "当前用户未读",
                Content = "应被标为已读。"
            },
            new Notification
            {
                UserId = fixture.Sender.Id,
                Title = "当前用户已读",
                Content = "应保持已读。",
                IsRead = true
            },
            new Notification
            {
                UserId = fixture.CollegeRecipient.Id,
                Title = "其他用户未读",
                Content = "不应被修改。"
            });
        await fixture.Db.SaveChangesAsync();

        var controller = new NotificationsController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.AcademicAdmin));

        var result = await controller.MarkAllRead(CancellationToken.None);

        Assert.IsType<NoContentResult>(result);
        var notifications = await fixture.Db.Notifications.AsNoTracking()
            .OrderBy(x => x.Title)
            .ToListAsync();
        Assert.All(
            notifications.Where(x => x.UserId == fixture.Sender.Id),
            notification => Assert.True(notification.IsRead));
        Assert.False(Assert.Single(
            notifications,
            x => x.UserId == fixture.CollegeRecipient.Id).IsRead);
    }

    [Fact]
    public async Task Publishing_course_grades_automatically_notifies_roster_students()
    {
        await using var fixture = await NotificationFixture.CreateAsync();
        var studentId = await fixture.Db.Students
            .Where(x => x.UserId == fixture.ClassStudentUser.Id)
            .Select(x => x.Id)
            .SingleAsync();
        var sheet = new GradeSheet
        {
            TeachingTaskId = fixture.OwnTask.Id,
            Status = GradeSheetStatus.Approved,
            Records =
            [
                new GradeRecord
                {
                    StudentId = studentId,
                    RegularScore = 88,
                    FinalScore = 92,
                    TotalScore = 90.8m,
                    GradePoint = 4
                }
            ]
        };
        fixture.Db.GradeSheets.Add(sheet);
        await fixture.Db.SaveChangesAsync();
        fixture.Db.ChangeTracker.Clear();

        var controller = new GradesController(
            fixture.Db,
            new TestDataScope(
                fixture.Sender.Id,
                fixture.FirstCollege.Id,
                SystemRoles.AcademicAdmin));

        Assert.IsType<NoContentResult>(await controller.Publish(
            sheet.Id,
            CancellationToken.None));

        var notification = Assert.Single(await fixture.Db.Notifications
            .Where(x => x.UserId == fixture.ClassStudentUser.Id)
            .ToListAsync());
        Assert.Equal("成绩已发布", notification.Title);
        Assert.Contains("《测试课程》", notification.Content);
        Assert.DoesNotContain("90.8", notification.Content);
        Assert.Equal(NotificationCategory.Grade, notification.Category);
        Assert.Equal("/grades", notification.LinkUrl);
    }

    private sealed class TestDataScope(
        Guid userId,
        Guid? collegeId,
        params string[] roles) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            userId,
            "测试发送人",
            collegeId,
            EffectiveDataScopeResolver.Resolve(roles),
            roles.ToHashSet(StringComparer.OrdinalIgnoreCase));
    }

    private sealed class NotificationFixture : IAsyncDisposable
    {
        private readonly SqliteConnection connection;

        private NotificationFixture(
            SqliteConnection connection,
            AppDbContext db,
            College firstCollege,
            College secondCollege,
            ApplicationUser sender,
            ApplicationUser collegeRecipient,
            ApplicationUser outsideRecipient,
            ApplicationUser disabledRecipient,
            ApplicationUser teacherUser,
            ApplicationUser classStudentUser,
            TeachingTask ownTask,
            TeachingTask otherTask)
        {
            this.connection = connection;
            Db = db;
            FirstCollege = firstCollege;
            SecondCollege = secondCollege;
            Sender = sender;
            CollegeRecipient = collegeRecipient;
            OutsideRecipient = outsideRecipient;
            DisabledRecipient = disabledRecipient;
            TeacherUser = teacherUser;
            ClassStudentUser = classStudentUser;
            OwnTask = ownTask;
            OtherTask = otherTask;
        }

        public AppDbContext Db { get; }
        public College FirstCollege { get; }
        public College SecondCollege { get; }
        public ApplicationUser Sender { get; }
        public ApplicationUser CollegeRecipient { get; }
        public ApplicationUser OutsideRecipient { get; }
        public ApplicationUser DisabledRecipient { get; }
        public ApplicationUser TeacherUser { get; }
        public ApplicationUser ClassStudentUser { get; }
        public TeachingTask OwnTask { get; }
        public TeachingTask OtherTask { get; }

        public static async Task<NotificationFixture> 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 firstCollege = new College
            {
                Code = "C01",
                Name = "第一学院"
            };
            var secondCollege = new College
            {
                Code = "C02",
                Name = "第二学院"
            };
            var users = Enumerable.Range(1, 6)
                .Select(index => new ApplicationUser
                {
                    Id = Guid.NewGuid(),
                    UserName = $"user{index}",
                    NormalizedUserName = $"USER{index}",
                    DisplayName = $"用户{index}",
                    CollegeId = index == 3 ? secondCollege.Id : firstCollege.Id,
                    IsEnabled = index != 4
                })
                .ToArray();
            var sender = users[0];
            var collegeRecipient = users[1];
            var outsideRecipient = users[2];
            var disabledRecipient = users[3];
            var teacherUser = users[4];
            var classStudentUser = users[5];

            var major = new Major
            {
                Code = "M01",
                Name = "测试专业",
                CollegeId = firstCollege.Id,
                DegreeType = "本科",
                SchoolingYears = 4
            };
            var administrativeClass = new AdministrativeClass
            {
                Code = "CL01",
                Name = "测试行政班",
                MajorId = major.Id,
                Grade = 2025
            };
            var student = new Student
            {
                StudentNumber = "20250001",
                Name = "测试学生",
                AdministrativeClassId = administrativeClass.Id,
                EnrollmentYear = 2025,
                EnrollmentDate = new DateOnly(2025, 9, 1),
                UserId = classStudentUser.Id
            };
            var teacher = new Teacher
            {
                TeacherNumber = "T001",
                Name = "测试教师",
                CollegeId = firstCollege.Id,
                UserId = teacherUser.Id
            };
            var term = new AcademicTerm
            {
                Code = "2025-A",
                Name = "2025 秋季学期",
                AcademicYear = "2025-2026",
                Season = TermSeason.Autumn,
                StartDate = new DateOnly(2025, 9, 1),
                EndDate = new DateOnly(2026, 1, 20),
                IsCurrent = true
            };
            var course = new Course
            {
                Code = "COURSE-1",
                Name = "测试课程",
                CollegeId = firstCollege.Id,
                Credits = 2,
                TotalHours = 32,
                LectureHours = 32,
                PracticeHours = 0,
                Nature = CourseNature.MajorRequired,
                AssessmentMethod = AssessmentMethod.Examination
            };
            var ownTask = new TeachingTask
            {
                TaskNumber = "TASK-1",
                Name = "测试课程教学班",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 30,
                Status = TeachingTaskStatus.Published
            };
            ownTask.Teachers.Add(new TeachingTaskTeacher
            {
                TeachingTaskId = ownTask.Id,
                TeacherId = teacher.Id,
                IsPrimary = true
            });
            ownTask.Classes.Add(new TeachingTaskClass
            {
                TeachingTaskId = ownTask.Id,
                AdministrativeClassId = administrativeClass.Id
            });
            var otherTask = new TeachingTask
            {
                TaskNumber = "TASK-2",
                Name = "其他教学班",
                AcademicTermId = term.Id,
                CourseId = course.Id,
                Capacity = 30,
                Status = TeachingTaskStatus.Published
            };

            db.AddRange(firstCollege, secondCollege, term);
            db.Users.AddRange(users);
            await db.SaveChangesAsync();
            db.Add(major);
            await db.SaveChangesAsync();
            db.Add(course);
            await db.SaveChangesAsync();
            db.Add(teacher);
            await db.SaveChangesAsync();
            db.Add(administrativeClass);
            await db.SaveChangesAsync();
            db.AddRange(student, ownTask, otherTask);
            await db.SaveChangesAsync();
            db.ChangeTracker.Clear();

            return new NotificationFixture(
                connection,
                db,
                firstCollege,
                secondCollege,
                sender,
                collegeRecipient,
                outsideRecipient,
                disabledRecipient,
                teacherUser,
                classStudentUser,
                ownTask,
                otherTask);
        }

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