using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using NSubstitute;
using UniVerse.Application.DTOs.Achievements;
using UniVerse.Application.DTOs.Auth;
using UniVerse.Application.DTOs.Common;
using UniVerse.Application.DTOs.Courses;
using UniVerse.Application.DTOs.Gamification;
using UniVerse.Application.DTOs.Lectures;
using UniVerse.Application.DTOs.Locations;
using UniVerse.Application.DTOs.Notifications;
using UniVerse.Application.DTOs.Reviews;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.DTOs.Tags;
using UniVerse.Application.DTOs.Users;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Enums;
using UniVerse.Domain.Exceptions;
using UniVerse.Infrastructure.Data;

namespace UniVerse.Api.Tests.Helpers;

/// <summary>
/// WebApplicationFactory для интеграционных тестов.
/// Заменяет Npgsql DbContext на InMemory, создает заглушки для всех интерфейсов внешних сервисов
/// и отключает фоновую службу LLM, чтобы тестам не требовалась реальная инфраструктура.
/// </summary>
public class ApiWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        // Используем Development, чтобы были включены Swagger и конечная точка DevLogin
        builder.UseEnvironment("Development");

        builder.ConfigureAppConfiguration((_, config) =>
        {
            // Внедряем настройки тестового JWT — должны совпадать с константами TestJwtFactory
            var testSettings = new Dictionary<string, string?>
            {
                ["Jwt:Secret"]   = TestJwtFactory.Secret,
                ["Jwt:Issuer"]   = TestJwtFactory.Issuer,
                ["Jwt:Audience"] = TestJwtFactory.Audience,
                // Отключаем оркестрацию Aspire
                ["Aspire:Enabled"] = "false",
                // Фиктивные значения Azure AD (маршруты имеют атрибут [AllowAnonymous] или тестируются отдельно)
                ["AzureAd:TenantId"] = "test-tenant",
                ["AzureAd:ClientId"] = "test-client",
                // Фиктивные значения LLM / Modeus (клиенты заменяются ниже)
                ["Llm:BaseUrl"]      = "http://localhost:9999/",
                ["ModeusApi:BaseUrl"] = "http://localhost:9998/",
            };
            config.AddInMemoryCollection(testSettings);
        });

        builder.ConfigureServices(services =>
        {
            // ── 1. Заменяем Npgsql DbContext на InMemory ──────────────────────────
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.RemoveAll<AppDbContext>();

            // Удаляем все регистрации, связанные с DbContext, которые добавил хост
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null) services.Remove(descriptor);

            // Находим и удаляем все дескрипторы настроек DbContext
            var dbContextDescriptors = services
                .Where(d => d.ServiceType == typeof(DbContextOptions<AppDbContext>)
                         || d.ImplementationType == typeof(AppDbContext))
                .ToList();
            foreach (var d in dbContextDescriptors) services.Remove(d);

            services.AddDbContext<AppDbContext>(options =>
                options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));

            // ── 2. Отключаем фоновые службы ────────────────────────────────────
            // Удаляем все регистрации IHostedService, чтобы предотвратить запуск фоновой задачи LLM
            var hostedServices = services
                .Where(d => d.ServiceType == typeof(IHostedService))
                .ToList();
            foreach (var d in hostedServices) services.Remove(d);

            // ── 3. Создаем заглушки для всех интерфейсов Application сервисов ─────────
            ReplaceWithSubstitute<IAuthService>(services, CreateAuthServiceStub());
            ReplaceWithSubstitute<IUserService>(services, CreateUserServiceStub());
            ReplaceWithSubstitute<ILectureService>(services, CreateLectureServiceStub());
            ReplaceWithSubstitute<IReviewService>(services, CreateReviewServiceStub());
            ReplaceWithSubstitute<IReviewPromptService>(services, CreateReviewPromptServiceStub());
            ReplaceWithSubstitute<ICourseService>(services, CreateCourseServiceStub());
            ReplaceWithSubstitute<ITagService>(services, CreateTagServiceStub());
            ReplaceWithSubstitute<ILocationService>(services, CreateLocationServiceStub());
            ReplaceWithSubstitute<IAchievementService>(services, CreateAchievementServiceStub());
            ReplaceWithSubstitute<IGamificationService>(services, CreateGamificationServiceStub());
            ReplaceWithSubstitute<IScheduleSyncService>(services, CreateSyncServiceStub());
            ReplaceWithSubstitute<ILlmAnalysisService>(services, Substitute.For<ILlmAnalysisService>());
            ReplaceWithSubstitute<ILlmClient>(services, Substitute.For<ILlmClient>());
            ReplaceWithSubstitute<IMicrosoftAuthClient>(services, Substitute.For<IMicrosoftAuthClient>());
            ReplaceWithSubstitute<INotificationService>(services, CreateNotificationServiceStub());
        });
    }

    private static void ReplaceWithSubstitute<TService>(IServiceCollection services, TService instance)
        where TService : class
    {
        services.RemoveAll<TService>();
        services.AddScoped<TService>(_ => instance);
    }

    // ── Фабрики заглушек ────────────────────────────────────────────────────────────

    private static IAuthService CreateAuthServiceStub()
    {
        var stub = Substitute.For<IAuthService>();
        var authResult = new AuthResult(
            new AuthResponse("access_token", DateTime.UtcNow.AddHours(1),
                new UserAuthDto(1, "test@test.com", "Test User", [UserRole.Student])),
            "refresh_token");
        stub.LoginWithMicrosoftAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<string?>())
            .Returns(authResult);
        stub.DevLoginAsync(Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<IReadOnlyCollection<UserRole>>(), Arg.Any<string?>())
            .Returns(authResult);
        stub.RefreshTokenAsync(Arg.Any<string>()).Returns(authResult);
        stub.GetCurrentUserAsync(Arg.Any<int>())
            .Returns(new CurrentUserDto(1, "test@test.com", "Test", null, [UserRole.Student], 0, 0, 1, DateTime.UtcNow));
        return stub;
    }

    private static INotificationService CreateNotificationServiceStub()
    {
        var stub = Substitute.For<INotificationService>();
        stub.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        stub.ScheduleAsync(Arg.Any<ScheduleNotificationRequest>(), Arg.Any<CancellationToken>())
            .Returns(new ScheduledNotificationResponse("test-job", DateTimeOffset.UtcNow.AddMinutes(5)));
        stub.GetUserNotificationsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>(), Arg.Any<CancellationToken>())
            .Returns(PagedResult<UserNotificationDto>.Create([], 0, 1, 20));
        stub.MarkAllReadAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
            .Returns(Task.CompletedTask);
        stub.CreateUserNotificationAsync(
                Arg.Any<int>(),
                Arg.Any<string>(),
                Arg.Any<string>(),
                Arg.Any<string>(),
                Arg.Any<CancellationToken>())
            .Returns(new UserNotificationDto(1, "achievement", "Title", "Body", false, DateTime.UtcNow));
        return stub;
    }

    private static IUserService CreateUserServiceStub()
    {
        var stub = Substitute.For<IUserService>();
        var userDto = new UserDto(1, "test@test.com", "Test", null, [UserRole.Student], true, 0, 0, 1, DateTime.UtcNow);
        var pagedUsers = PagedResult<UserDto>.Create([userDto], 1, 1, 20);
        var lectureDto = new LectureDto(1, 1, "Course", null, null, null, null,
            "Title", null, LectureFormat.Offline,
            DateTime.UtcNow, DateTime.UtcNow.AddHours(2),
            true, 30, 0, null, DateTime.UtcNow, true);
        var pagedLectures = PagedResult<LectureDto>.Create([lectureDto], 1, 1, 20);

        stub.GetByIdAsync(Arg.Any<int>()).Returns(userDto);
        stub.UpdateProfileAsync(Arg.Any<int>(), Arg.Any<UpdateUserRequest>()).Returns(userDto);
        stub.GetStatsAsync(Arg.Any<int>()).Returns(new UserStatsDto(
            0,
            0,
            0,
            0,
            0,
            1,
            0,
            0,
            100,
            0,
            3,
            [new EnrollmentSlotRuleDto(1, 3), new EnrollmentSlotRuleDto(3, 5), new EnrollmentSlotRuleDto(4, 7)]));
        stub.GetEnrollmentsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedLectures);
        stub.GetMyEnrollmentsIcsAsync(Arg.Any<int>()).Returns("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n");
        stub.GetEnrollmentIcsAsync(Arg.Any<int>(), Arg.Any<int>()).Returns("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n");
        stub.GetCalendarSubscriptionTokenAsync(Arg.Any<int>()).Returns("test-token");
        stub.GetEnrollmentsIcsBySubscriptionTokenAsync("bad-token")
            .Returns(Task.FromException<string>(new ForbiddenException("Invalid calendar subscription token.")));
        stub.GetEnrollmentsIcsBySubscriptionTokenAsync(Arg.Is<string>(token => token != "bad-token"))
            .Returns(Task.FromResult("BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n"));
        stub.GetAllAsync(Arg.Any<UserFilterRequest>()).Returns(pagedUsers);
        stub.SetRolesAsync(Arg.Any<int>(), Arg.Any<IReadOnlyCollection<UserRole>>()).Returns(Task.CompletedTask);
        stub.SetActiveAsync(Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
        return stub;
    }

    private static ILectureService CreateLectureServiceStub()
    {
        var stub = Substitute.For<ILectureService>();
        var lectureDto = new LectureDto(1, 1, "Course", null, null, null, null,
            "Title", null, LectureFormat.Offline,
            DateTime.UtcNow, DateTime.UtcNow.AddHours(2),
            true, 30, 0, null, DateTime.UtcNow);
        var detailDto = new LectureDetailDto(1, 1, "Course", null, null, null, null,
            "Title", null, LectureFormat.Offline,
            DateTime.UtcNow, DateTime.UtcNow.AddHours(2),
            true, 30, 0, null, DateTime.UtcNow, false);
        var pagedLectures = PagedResult<LectureDto>.Create([lectureDto], 1, 1, 20);
        var pagedEnrollments = PagedResult<EnrollmentDto>.Create([], 0, 1, 20);

        stub.GetAllAsync(Arg.Any<LectureFilterRequest>(), Arg.Any<int?>()).Returns(pagedLectures);
        stub.GetByIdAsync(Arg.Any<int>(), Arg.Any<int?>()).Returns(detailDto);
        stub.CreateAsync(Arg.Any<CreateLectureRequest>()).Returns(lectureDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLectureRequest>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(lectureDto);
        stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.EnrollAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.UnenrollAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.MarkAttendanceAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
        stub.GetEnrollmentsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(pagedEnrollments);
        return stub;
    }

    private static IReviewService CreateReviewServiceStub()
    {
        var stub = Substitute.For<IReviewService>();
        var reviewDto = new ReviewDto(1, 1, "Lecture", 1, "User",
            ReviewRating.Like, "Great!", ReviewLlmStatus.Pending,
            null, null, null, null, null, DateTime.UtcNow);
        var pagedReviews = PagedResult<ReviewDto>.Create([reviewDto], 1, 1, 20);

        stub.CreateAsync(Arg.Any<int>(), Arg.Any<CreateReviewRequest>()).Returns(reviewDto);
        stub.GetByIdAsync(Arg.Any<int>()).Returns(reviewDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<UpdateReviewRequest>()).Returns(reviewDto);
        stub.DeleteAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<bool>()).Returns(Task.CompletedTask);
        stub.GetByLectureAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>(), Arg.Any<int?>(), Arg.Any<bool>()).Returns(pagedReviews);
        stub.GetByUserAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(pagedReviews);
        stub.GetAllAsync(Arg.Any<ReviewFilterRequest>()).Returns(pagedReviews);
        stub.ReanalyzeAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        return stub;
    }

    private static IReviewPromptService CreateReviewPromptServiceStub()
    {
        var stub = Substitute.For<IReviewPromptService>();
        var promptDto = new ReviewPromptDto(
            "Analyze {lectureContext}. Review: {reviewText}",
            DateTime.UtcNow);

        stub.GetAsync().Returns(promptDto);
        stub.UpdateAsync(Arg.Any<UpdateReviewPromptRequest>()).Returns(callInfo =>
            new ReviewPromptDto(callInfo.Arg<UpdateReviewPromptRequest>().Prompt, DateTime.UtcNow));
        return stub;
    }

    private static ICourseService CreateCourseServiceStub()
    {
        var stub = Substitute.For<ICourseService>();
        var courseDto = new CourseDto(1, "Course", null, false, [], DateTime.UtcNow);
        var paged = PagedResult<CourseDto>.Create([courseDto], 1, 1, 20);

        stub.GetAllAsync(Arg.Any<CourseFilterRequest>()).Returns(paged);
        stub.GetByIdAsync(Arg.Any<int>()).Returns(courseDto);
        stub.CreateAsync(Arg.Any<CreateCourseRequest>()).Returns(courseDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateCourseRequest>()).Returns(courseDto);
        stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.AddTagAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.RemoveTagAsync(Arg.Any<int>(), Arg.Any<int>()).Returns(Task.CompletedTask);
        return stub;
    }

    private static ITagService CreateTagServiceStub()
    {
        var stub = Substitute.For<ITagService>();
        var tagDto = new TagDto(1, "Tag", TagType.Topic, null, DateTime.UtcNow);

        stub.GetAllAsync(Arg.Any<TagType?>(), Arg.Any<int?>()).Returns([tagDto]);
        stub.GetByIdAsync(Arg.Any<int>()).Returns(tagDto);
        stub.CreateAsync(Arg.Any<CreateTagRequest>()).Returns(tagDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateTagRequest>()).Returns(tagDto);
        stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.GetTreeAsync().Returns(new List<TagTreeDto>());
        return stub;
    }

    private static ILocationService CreateLocationServiceStub()
    {
        var stub = Substitute.For<ILocationService>();
        var locationDto = new LocationDto(1, "Room 101", null, null, null, DateTime.UtcNow);

        stub.GetAllAsync().Returns([locationDto]);
        stub.GetByIdAsync(Arg.Any<int>()).Returns(locationDto);
        stub.CreateAsync(Arg.Any<CreateLocationRequest>()).Returns(locationDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateLocationRequest>()).Returns(locationDto);
        stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        return stub;
    }

    private static IAchievementService CreateAchievementServiceStub()
    {
        var stub = Substitute.For<IAchievementService>();
        var achievementDto = new AchievementDto(1, "First Review", null, null, 10, 5, null, DateTime.UtcNow);

        stub.GetAllAsync().Returns([achievementDto]);
        stub.GetByIdAsync(Arg.Any<int>()).Returns(achievementDto);
        stub.CreateAsync(Arg.Any<CreateAchievementRequest>()).Returns(achievementDto);
        stub.UpdateAsync(Arg.Any<int>(), Arg.Any<UpdateAchievementRequest>()).Returns(achievementDto);
        stub.DeleteAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        return stub;
    }

    private static IGamificationService CreateGamificationServiceStub()
    {
        var stub = Substitute.For<IGamificationService>();
        var paged = PagedResult<CoinTransactionDto>.Create([], 0, 1, 20);

        stub.GetUserAchievementsAsync(Arg.Any<int>()).Returns(new List<UserAchievementDto>());
        stub.GetTransactionsAsync(Arg.Any<int>(), Arg.Any<PaginationRequest>()).Returns(paged);
        stub.AwardCoinsAsync(Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CoinTransactionType>(),
            Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>()).Returns(Task.CompletedTask);
        stub.CheckAndAwardAchievementsAsync(Arg.Any<int>()).Returns(Task.CompletedTask);
        stub.CalculateLevelAsync(Arg.Any<int>()).Returns(Task.FromResult(1));
        stub.GetLevelProgressAsync(Arg.Any<int>()).Returns(Task.FromResult(new LevelProgressDto(0, 100)));
        return stub;
    }

    private static IScheduleSyncService CreateSyncServiceStub()
    {
        var stub = Substitute.For<IScheduleSyncService>();
        var syncResult = new SyncResultDto(0, 0, 0, null);
        var syncStatus = new SyncStatusDto(null, "idle", null);

        stub.SyncScheduleAsync(Arg.Any<SyncScheduleRequest>()).Returns(syncResult);
        stub.SyncRoomsAsync().Returns(syncResult);
        stub.SearchEmployeesAsync(Arg.Any<string>()).Returns(new List<EmployeeDto>());
        stub.GetLastSyncStatusAsync().Returns(syncStatus);
        return stub;
    }
}
