using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
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<ForbiddenException>(() => 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);
    }

    [Fact]
    public async Task CalendarSubscriptionToken_Roundtrip_ReturnsUserEnrollmentsIcs()
    {
        await using var db = CreateDbContext();
        var startsAt = new DateTime(2026, 1, 10, 9, 0, 0, DateTimeKind.Utc);
        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();
        var service = CreateService(db);

        var token = await service.GetCalendarSubscriptionTokenAsync(1);
        var ics = await service.GetEnrollmentsIcsBySubscriptionTokenAsync(token);

        Assert.Contains("BEGIN:VCALENDAR", ics);
        Assert.Contains("Lecture 1", ics);
    }

    [Fact]
    public async Task CalendarSubscriptionToken_RejectsTamperedToken()
    {
        await using var db = CreateDbContext();
        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        await db.SaveChangesAsync();
        var service = CreateService(db);
        var token = await service.GetCalendarSubscriptionTokenAsync(1);
        var tampered = token[..^1] + (token[^1] == 'A' ? 'B' : 'A');

        await Assert.ThrowsAsync<ForbiddenException>(() =>
            service.GetEnrollmentsIcsBySubscriptionTokenAsync(tampered));
    }

    [Fact]
    public async Task GetEnrollmentIcsAsync_ReturnsLectureIcsWithoutEnrollment()
    {
        await using var db = CreateDbContext();
        var startsAt = new DateTime(2026, 2, 10, 9, 0, 0, DateTimeKind.Utc);
        db.Users.Add(new User { Id = 1, Email = "student@test.local" });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(Lecture(1853, startsAt));
        await db.SaveChangesAsync();
        var service = CreateService(db);

        var ics = await service.GetEnrollmentIcsAsync(1, 1853);

        Assert.Contains("BEGIN:VCALENDAR", ics);
        Assert.Contains("Lecture 1853", ics);
    }

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

    private static UserService CreateService(AppDbContext db)
    {
        var notifications = Substitute.For<INotificationService>();
        notifications.CreateUserNotificationAsync(
                Arg.Any<int>(),
                Arg.Any<string>(),
                Arg.Any<string>(),
                Arg.Any<string>(),
                Arg.Any<CancellationToken>())
            .Returns(callInfo => new UserNotificationDto(1, callInfo.ArgAt<string>(1), callInfo.ArgAt<string>(2), callInfo.ArgAt<string>(3), false, DateTime.UtcNow));
        notifications.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);

        var gamification = new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Jwt:Secret"] = "test-calendar-subscription-secret-32chars"
            })
            .Build();
        return new UserService(db, gamification, config);
    }

    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()
        };
}
