using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using UniVerse.Application.DTOs.Notifications; using UniVerse.Application.DTOs.Users; using UniVerse.Application.Interfaces; using UniVerse.Domain.Entities; using UniVerse.Domain.Enums; using UniVerse.Domain.Exceptions; using UniVerse.Infrastructure.Data; using UniVerse.Infrastructure.Services; using Xunit; namespace UniVerse.Api.Tests.Users; public class UserServiceTests { [Fact] public async Task GetStatsAsync_ReturnsLevelProgressThresholds() { await using var db = CreateDbContext(); SeedLevelThresholds(db); db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 120 }); await db.SaveChangesAsync(); var service = CreateService(db); var stats = await service.GetStatsAsync(1); Assert.Equal(2, stats.Level); Assert.Equal(100, stats.CurrentLevelXp); Assert.Equal(300, stats.NextLevelXp); } [Fact] public async Task GetStatsAsync_ReturnsNullNextLevelAtMaxConfiguredLevel() { await using var db = CreateDbContext(); SeedLevelThresholds(db); db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 350 }); await db.SaveChangesAsync(); var service = CreateService(db); var stats = await service.GetStatsAsync(1); Assert.Equal(3, stats.Level); Assert.Equal(300, stats.CurrentLevelXp); Assert.Null(stats.NextLevelXp); } [Fact] public async Task GetStatsAsync_ReturnsEnrollmentSlotStateAndRules() { await using var db = CreateDbContext(); SeedLevelThresholds(db); var now = DateTime.UtcNow; db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 350 }); db.Courses.Add(new Course { Id = 1, Name = "Course" }); db.Lectures.AddRange( Lecture(1, now.AddDays(1)), Lecture(2, now.AddDays(2)), Lecture(3, now.AddDays(-1))); db.LectureEnrollments.AddRange( new LectureEnrollment { LectureId = 1, UserId = 1 }, new LectureEnrollment { LectureId = 2, UserId = 1 }, new LectureEnrollment { LectureId = 3, UserId = 1 }); await db.SaveChangesAsync(); var service = CreateService(db); var stats = await service.GetStatsAsync(1); Assert.Equal(3, stats.ActiveEnrollments); Assert.Equal(5, stats.EnrollmentSlotLimit); Assert.Equal(new[] { 1, 3, 4 }, stats.EnrollmentSlotRules.Select(rule => rule.Level)); Assert.Equal(new[] { 3, 5, 7 }, stats.EnrollmentSlotRules.Select(rule => rule.Slots)); } [Fact] public async Task SetRolesAsync_DeduplicatesRolesAndCreatesProfiles() { await using var db = CreateDbContext(); db.Users.Add(new User { Id = 1, Email = "user@test.local", Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }] }); await db.SaveChangesAsync(); var service = CreateService(db); await service.SetRolesAsync(1, [UserRole.Teacher, UserRole.Teacher, UserRole.Student]); var user = await db.Users .Include(u => u.Roles) .FirstAsync(u => u.Id == 1); Assert.Equal(new[] { UserRole.Student, UserRole.Teacher }, user.Roles.Select(role => role.Role).OrderBy(role => role)); Assert.Equal(2, user.Roles.Count); Assert.True(await db.StudentProfiles.AnyAsync(profile => profile.UserId == 1)); Assert.True(await db.TeacherProfiles.AnyAsync(profile => profile.UserId == 1)); } [Fact] public async Task SetRolesAsync_RejectsEmptyRoleSet() { await using var db = CreateDbContext(); db.Users.Add(new User { Id = 1, Email = "user@test.local", Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }] }); await db.SaveChangesAsync(); var service = CreateService(db); await Assert.ThrowsAsync(() => service.SetRolesAsync(1, [])); } [Fact] public async Task SetRolesAsync_PreservesExistingProfiles() { await using var db = CreateDbContext(); db.Users.Add(new User { Id = 1, Email = "user@test.local", Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }] }); db.StudentProfiles.Add(new StudentProfile { Id = 10, UserId = 1, StudentId = "S-1" }); db.TeacherProfiles.Add(new TeacherProfile { Id = 20, UserId = 1, Department = "Math" }); await db.SaveChangesAsync(); var service = CreateService(db); await service.SetRolesAsync(1, [UserRole.Teacher]); Assert.Equal(1, await db.StudentProfiles.CountAsync(profile => profile.UserId == 1)); Assert.Equal(1, await db.TeacherProfiles.CountAsync(profile => profile.UserId == 1)); Assert.Equal("S-1", (await db.StudentProfiles.SingleAsync(profile => profile.UserId == 1)).StudentId); Assert.Equal("Math", (await db.TeacherProfiles.SingleAsync(profile => profile.UserId == 1)).Department); } [Fact] public async Task GetAllAsync_FiltersBySearchActiveAndExactSingleRole() { await using var db = CreateDbContext(); SeedLevelThresholds(db); db.Users.AddRange( User(1, "anna@test.local", "Anna", true, 120, UserRole.Student), User(2, "anna.teacher@test.local", "Anna Teacher", true, 120, UserRole.Teacher), User(3, "anna.admin@test.local", "Anna Admin", true, 120, UserRole.Student, UserRole.Admin), User(4, "inactive@test.local", "Anna Inactive", false, 120, UserRole.Student)); await db.SaveChangesAsync(); var service = CreateService(db); var result = await service.GetAllAsync(new UserFilterRequest( Search: "anna", Role: UserRole.Student, IsActive: true, Page: 1, PageSize: 10)); var user = Assert.Single(result.Items); Assert.Equal(1, user.Id); Assert.Equal(2, user.Level); Assert.Equal(1, result.TotalCount); } [Fact] public async Task GetAllAsync_ReturnsRequestedPageInCreatedAtDescendingOrder() { await using var db = CreateDbContext(); SeedLevelThresholds(db); db.Users.AddRange( User(1, "old@test.local", "Old", true, 0, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), UserRole.Student), User(2, "middle@test.local", "Middle", true, 100, new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), UserRole.Student), User(3, "new@test.local", "New", true, 300, new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc), UserRole.Student)); await db.SaveChangesAsync(); var service = CreateService(db); var result = await service.GetAllAsync(new UserFilterRequest(null, null, null, Page: 2, PageSize: 1)); Assert.Equal(3, result.TotalCount); Assert.Equal(2, result.Page); Assert.Equal(3, result.TotalPages); Assert.Equal(2, Assert.Single(result.Items).Id); } private static AppDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"UserServiceTests_{Guid.NewGuid()}") .Options; return new AppDbContext(options); } private static UserService CreateService(AppDbContext db) { var notifications = Substitute.For(); notifications.CreateUserNotificationAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(callInfo => new UserNotificationDto(1, callInfo.ArgAt(1), callInfo.ArgAt(2), callInfo.ArgAt(3), false, DateTime.UtcNow)); notifications.SendAsync(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); var gamification = new GamificationService(db, notifications, NullLogger.Instance); return new UserService(db, gamification); } private static void SeedLevelThresholds(AppDbContext db) { db.LevelThresholds.AddRange( new LevelThreshold { Level = 1, RequiredXp = 0 }, new LevelThreshold { Level = 2, RequiredXp = 100 }, new LevelThreshold { Level = 3, RequiredXp = 300 }); db.SaveChanges(); } 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 }; private static User User( int id, string email, string displayName, bool isActive, int xp, params UserRole[] roles) => User(id, email, displayName, isActive, xp, DateTime.UtcNow, roles); private static User User( int id, string email, string displayName, bool isActive, int xp, DateTime createdAt, params UserRole[] roles) => new() { Id = id, Email = email, DisplayName = displayName, IsActive = isActive, Xp = xp, CreatedAt = createdAt, Roles = roles.Select(role => new UserRoleAssignment { UserId = id, Role = role }).ToList() }; }