using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
using UniVerse.Domain.Enums;
using UniVerse.Infrastructure.Data;
using UniVerse.Infrastructure.Services;
using Xunit;

namespace UniVerse.Api.Tests.Gamification;

public class GamificationServiceTests
{
    [Fact]
    public async Task CheckAndAwardAchievementsAsync_AwardsModernConditionsOnce()
    {
        await using var db = CreateDbContext();
        SeedLevelThresholds(db);
        var service = CreateService(db);

        db.Users.Add(new User
        {
            Id = 1,
            Email = "student@test.local",
            DisplayName = "Student",
            AvatarUrl = "avatar.png",
            Xp = 100,
            Coins = 510
        });
        db.Courses.Add(new Course { Id = 1, Name = "Course" });
        db.Lectures.Add(new Lecture
        {
            Id = 1,
            CourseId = 1,
            Title = "Future lecture",
            StartsAt = DateTime.UtcNow.AddDays(1),
            EndsAt = DateTime.UtcNow.AddDays(1).AddHours(2),
            IsOpen = true
        });
        db.LectureEnrollments.Add(new LectureEnrollment { UserId = 1, LectureId = 1 });
        db.Reviews.AddRange(
            new Review { Id = 1, UserId = 1, LectureId = 1, Rating = ReviewRating.Like },
            new Review { Id = 2, UserId = 1, LectureId = 1, Rating = ReviewRating.Neutral },
            new Review { Id = 3, UserId = 1, LectureId = 1, Rating = ReviewRating.Dislike });
        db.CoinTransactions.Add(new CoinTransaction
        {
            UserId = 1,
            Amount = 510,
            Type = CoinTransactionType.AdminAdjustment,
            Description = "Initial coins"
        });
        db.Achievements.AddRange(
            Achievement(1001, "First activity", "first_activity:1", 10),
            Achievement(1002, "Reviews", "reviews_written:3", 20),
            Achievement(1003, "Active registrations", "active_registrations:1", 30),
            Achievement(1004, "Coins earned", "coins_earned:500", 40),
            Achievement(1005, "Level reached", "level_reached:2", 50),
            Achievement(1006, "Profile completed", "profile_completed:1", 60),
            Achievement(1007, "Old condition", "reviews_1", 100));
        await db.SaveChangesAsync();

        await service.CheckAndAwardAchievementsAsync(1);
        await service.CheckAndAwardAchievementsAsync(1);

        var user = await db.Users.FindAsync(1);
        Assert.NotNull(user);
        Assert.Equal(720, user!.Coins);
        Assert.Equal(310, user.Xp);
        Assert.Equal(6, await db.UserAchievements.CountAsync(ua => ua.UserId == 1));
        Assert.False(await db.UserAchievements.AnyAsync(ua => ua.AchievementId == 1007));
        Assert.Equal(6, await db.CoinTransactions.CountAsync(ct =>
            ct.UserId == 1 && ct.Type == CoinTransactionType.AchievementReward));
    }

    [Fact]
    public async Task CheckAndAwardAchievementsAsync_CountsConsecutiveIsoWeeksAcrossYears()
    {
        await using var db = CreateDbContext();
        SeedLevelThresholds(db);
        var service = CreateService(db);

        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, new DateTime(2025, 12, 29, 10, 0, 0, DateTimeKind.Utc)),
            Lecture(2, new DateTime(2026, 1, 5, 10, 0, 0, DateTimeKind.Utc)),
            Lecture(3, new DateTime(2026, 1, 12, 10, 0, 0, DateTimeKind.Utc)));
        db.LectureEnrollments.AddRange(
            new LectureEnrollment { UserId = 1, LectureId = 1, Attended = true },
            new LectureEnrollment { UserId = 1, LectureId = 2, Attended = true },
            new LectureEnrollment { UserId = 1, LectureId = 3, Attended = true });
        db.Achievements.Add(Achievement(1001, "Streak", "attendance_streak_weeks:3", 10));
        await db.SaveChangesAsync();

        await service.CheckAndAwardAchievementsAsync(1);

        Assert.True(await db.UserAchievements.AnyAsync(ua => ua.UserId == 1 && ua.AchievementId == 1001));
    }

    [Theory]
    [InlineData(0, 1)]
    [InlineData(99, 1)]
    [InlineData(100, 2)]
    [InlineData(299, 2)]
    [InlineData(300, 3)]
    public async Task CalculateLevelAsync_UsesDatabaseThresholds(int xp, int expectedLevel)
    {
        await using var db = CreateDbContext();
        SeedLevelThresholds(db);
        var service = CreateService(db);

        var level = await service.CalculateLevelAsync(xp);

        Assert.Equal(expectedLevel, level);
    }

    [Theory]
    [InlineData(120, 100, 300)]
    [InlineData(350, 300, null)]
    public async Task GetLevelProgressAsync_ReturnsCurrentAndNextThresholds(int xp, int currentLevelXp, int? nextLevelXp)
    {
        await using var db = CreateDbContext();
        SeedLevelThresholds(db);
        var service = CreateService(db);

        var progress = await service.GetLevelProgressAsync(xp);

        Assert.Equal(currentLevelXp, progress.CurrentLevelXp);
        Assert.Equal(nextLevelXp, progress.NextLevelXp);
    }

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

    private static GamificationService 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);

        return new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
    }

    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 Achievement Achievement(int id, string name, string condition, int coinReward) => new()
    {
        Id = id,
        Name = name,
        Condition = condition,
        CoinReward = coinReward
    };

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