using Microsoft.EntityFrameworkCore;
using NSubstitute;
using UniVerse.Application.DTOs.Lectures;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Exceptions;
using UniVerse.Infrastructure.Data;
using UniVerse.Infrastructure.Services;
using Xunit;

namespace UniVerse.Api.Tests.Lectures;

public class LectureServiceTests
{
    [Fact]
    public async Task GetAllAsync_MarksLecturesEnrolledByCurrentUser()
    {
        await using var db = CreateDbContext();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
        var startsAt = DateTime.UtcNow.AddDays(1);

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.AddRange(
            Lecture(1, startsAt),
            Lecture(2, startsAt.AddDays(1)));
        db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 });
        await db.SaveChangesAsync();

        var result = await service.GetAllAsync(new LectureFilterRequest(null, null, null, null, null, null, null, null), 1);

        Assert.True(result.Items.Single(item => item.Id == 1).IsEnrolled);
        Assert.False(result.Items.Single(item => item.Id == 2).IsEnrolled);
    }

    [Fact]
    public async Task EnrollAsync_SchedulesLectureReminders()
    {
        await using var db = CreateDbContext();
        var scheduler = Substitute.For<INotificationScheduler>();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
        var startsAt = DateTime.UtcNow.AddHours(4);

        db.Users.Add(new User { Id = 1, Email = "student@test.local", DisplayName = "Student" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(1, startsAt));
        await db.SaveChangesAsync();

        await service.EnrollAsync(1, 1);

        await scheduler.Received(1).ScheduleAsync(
            Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("через 3 часа")),
            Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(-3))),
            "lecture-1-user-1-starts-in-3-hours",
            Arg.Any<CancellationToken>());
        await scheduler.Received(1).ScheduleAsync(
            Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("через 1 час")),
            Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(-1))),
            "lecture-1-user-1-starts-in-1-hour",
            Arg.Any<CancellationToken>());
        await scheduler.Received(1).ScheduleAsync(
            Arg.Is<NotificationMessage>(m => m.Recipient == "student@test.local" && m.Subject.Contains("Оцените")),
            Arg.Is<DateTimeOffset>(d => d == new DateTimeOffset(startsAt.AddHours(2))),
            "lecture-1-user-1-ended",
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task EnrollAsync_SkipsPastLectureReminders()
    {
        await using var db = CreateDbContext();
        var scheduler = Substitute.For<INotificationScheduler>();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
        var startsAt = DateTime.UtcNow.AddMinutes(90);

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(1, startsAt));
        await db.SaveChangesAsync();

        await service.EnrollAsync(1, 1);

        await scheduler.DidNotReceive().ScheduleAsync(
            Arg.Any<NotificationMessage>(),
            Arg.Any<DateTimeOffset>(),
            "lecture-1-user-1-starts-in-3-hours",
            Arg.Any<CancellationToken>());
        await scheduler.Received(2).ScheduleAsync(
            Arg.Any<NotificationMessage>(),
            Arg.Any<DateTimeOffset>(),
            Arg.Any<string>(),
            Arg.Any<CancellationToken>());
    }

    [Theory]
    [InlineData(1, 3)]
    [InlineData(2, 3)]
    [InlineData(3, 5)]
    [InlineData(4, 7)]
    [InlineData(5, 7)]
    public async Task EnrollAsync_ThrowsWhenActiveEnrollmentLimitReached(int level, int activeEnrollments)
    {
        await using var db = CreateDbContext();
        var gamification = Substitute.For<IGamificationService>();
        gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(level);
        var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
        var startsAt = DateTime.UtcNow.AddDays(1);

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(100, startsAt.AddDays(100)));
        for (var i = 1; i <= activeEnrollments; i++)
        {
            db.Lectures.Add(Lecture(i, startsAt.AddDays(i)));
            db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1 });
        }
        await db.SaveChangesAsync();

        await Assert.ThrowsAsync<ConflictException>(() => service.EnrollAsync(100, 1));
    }

    [Fact]
    public async Task EnrollAsync_ThrowsWhenPastUnattendedEnrollmentsReachLimit()
    {
        await using var db = CreateDbContext();
        var gamification = Substitute.For<IGamificationService>();
        gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(1);
        var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
        var now = DateTime.UtcNow;

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(100, now.AddDays(1)));
        for (var i = 1; i <= 3; i++)
        {
            db.Lectures.Add(Lecture(i, now.AddDays(-i)));
            db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1 });
        }
        await db.SaveChangesAsync();

        await Assert.ThrowsAsync<ConflictException>(() => service.EnrollAsync(100, 1));
    }

    [Fact]
    public async Task EnrollAsync_DoesNotCountAttendedEnrollmentsTowardLimit()
    {
        await using var db = CreateDbContext();
        var gamification = Substitute.For<IGamificationService>();
        gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(1);
        var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
        var now = DateTime.UtcNow;

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(100, now.AddDays(1)));
        for (var i = 1; i <= 3; i++)
        {
            db.Lectures.Add(Lecture(i, now.AddDays(-i)));
            db.LectureEnrollments.Add(new LectureEnrollment { LectureId = i, UserId = 1, Attended = true });
        }
        await db.SaveChangesAsync();

        await service.EnrollAsync(100, 1);

        Assert.True(await db.LectureEnrollments.AnyAsync(e => e.LectureId == 100 && e.UserId == 1));
    }

    [Fact]
    public async Task EnrollAsync_CountsMandatoryAttendeesTowardLectureCapacity()
    {
        await using var db = CreateDbContext();
        var gamification = Substitute.For<IGamificationService>();
        gamification.CalculateLevelAsync(Arg.Any<int>()).Returns(1);
        var service = new LectureService(db, gamification, Substitute.For<INotificationScheduler>());
        var lecture = Lecture(1, DateTime.UtcNow.AddDays(1));
        lecture.MaxEnrollments = 31;
        lecture.MandatoryAttendeesCount = 30;

        db.Users.AddRange(
            new User { Id = 1, Email = "first@test.local" },
            new User { Id = 2, Email = "second@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(lecture);
        await db.SaveChangesAsync();

        await service.EnrollAsync(1, 1);

        await Assert.ThrowsAsync<ConflictException>(() => service.EnrollAsync(1, 2));
    }

    [Fact]
    public async Task UnenrollAsync_CancelsLectureReminders()
    {
        await using var db = CreateDbContext();
        var scheduler = Substitute.For<INotificationScheduler>();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), scheduler);
        var startsAt = DateTime.UtcNow.AddHours(4);

        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(1, startsAt));
        db.LectureEnrollments.Add(new LectureEnrollment { LectureId = 1, UserId = 1 });
        await db.SaveChangesAsync();

        await service.UnenrollAsync(1, 1);

        await scheduler.Received(1).CancelAsync("lecture-1-user-1-starts-in-3-hours", Arg.Any<CancellationToken>());
        await scheduler.Received(1).CancelAsync("lecture-1-user-1-starts-in-1-hour", Arg.Any<CancellationToken>());
        await scheduler.Received(1).CancelAsync("lecture-1-user-1-ended", Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task UpdateAsync_TeacherCannotUpdateAnotherTeachersLecture()
    {
        await using var db = CreateDbContext();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        var lecture = Lecture(1, DateTime.UtcNow.AddDays(1));
        lecture.TeacherId = 2;
        db.Lectures.Add(lecture);
        await db.SaveChangesAsync();

        var request = new UpdateLectureRequest(null, null, "Updated", null, Domain.Enums.LectureFormat.Offline,
            DateTime.UtcNow.AddDays(1), DateTime.UtcNow.AddDays(1).AddHours(2), true, 30, null);

        await Assert.ThrowsAsync<ForbiddenException>(() => service.UpdateAsync(1, request, currentUserId: 1));
    }

    [Fact]
    public async Task GetEnrollmentsAsync_AdminCanReadAnyLecture()
    {
        await using var db = CreateDbContext();
        var service = new LectureService(db, Substitute.For<IGamificationService>(), Substitute.For<INotificationScheduler>());
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        var lecture = Lecture(1, DateTime.UtcNow.AddDays(1));
        lecture.TeacherId = 2;
        db.Lectures.Add(lecture);
        await db.SaveChangesAsync();

        var result = await service.GetEnrollmentsAsync(1, new UniVerse.Application.DTOs.Common.PaginationRequest(), currentUserId: 1, isAdmin: true);

        Assert.Empty(result.Items);
    }

    private static AppDbContext CreateDbContext()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase($"LectureServiceTests_{Guid.NewGuid()}")
            .Options;
        return new AppDbContext(options);
    }

    private static Lecture Lecture(int id, DateTime startsAt) => new()
    {
        Id = id,
        CourseId = 1,
        Title = $"Lecture {id}",
        StartsAt = startsAt,
        EndsAt = startsAt.AddHours(2),
        IsOpen = true,
        MaxEnrollments = 30
    };
}
