using System.Security.Claims;
using Ikar.Server.Controllers;
using Ikar.Server.Data;
using Ikar.Server.Infrastructure;
using Ikar.Server.Infrastructure.Bots;
using Ikar.Server.Infrastructure.Auth;
using Ikar.Server.Infrastructure.Hubs;
using Ikar.Server.Infrastructure.Push;
using Ikar.Server.Infrastructure.Storage;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;

namespace Ikar.Server.Tests;

internal sealed class SqliteTestDatabase : IAsyncDisposable
{
    private readonly SqliteConnection _connection = new("Data Source=:memory:");

    public async Task<IkarDbContext> CreateContextAsync()
    {
        await _connection.OpenAsync();
        var options = new DbContextOptionsBuilder<IkarDbContext>()
            .UseSqlite(_connection)
            .Options;
        var dbContext = new IkarDbContext(options);
        await dbContext.Database.EnsureCreatedAsync();
        return dbContext;
    }

    public async ValueTask DisposeAsync()
    {
        await _connection.DisposeAsync();
    }
}

internal static class ControllerTestFixture
{
    public static ChatsController CreateChatsController(
        IkarDbContext dbContext,
        Guid userId,
        AttachmentStorageService? attachmentStorageService = null)
    {
        var pushDispatchQueue = new PushDispatchQueue();
        var hubContext = new NoopHubContext<MessengerHub>();
        var botAttachmentStorageService = attachmentStorageService ?? CreateAttachmentStorageService(Path.GetTempPath());
        var controller = new ChatsController(
            dbContext,
            new PresenceTracker(),
            hubContext,
            attachmentStorageService: attachmentStorageService!,
            pushDispatchQueue,
            new BotUpdateService(dbContext),
            new BotCreatorService(
                dbContext,
                new BotTokenService(),
                new BotDeletionService(dbContext, botAttachmentStorageService),
                new PresenceTracker(),
                hubContext,
                pushDispatchQueue));
        AttachUser(controller, userId);
        return controller;
    }

    public static UsersController CreateUsersController(IkarDbContext dbContext, Guid userId)
    {
        var controller = new UsersController(
            dbContext,
            new PresenceTracker(),
            avatarStorageService: null!);
        AttachUser(controller, userId);
        return controller;
    }

    public static BotsController CreateBotsController(IkarDbContext dbContext, Guid userId)
    {
        var attachmentStorageService = CreateAttachmentStorageService(Path.GetTempPath());
        var controller = new BotsController(
            dbContext,
            new BotTokenService(),
            new BotDeletionService(dbContext, attachmentStorageService));
        AttachUser(controller, userId);
        return controller;
    }

    public static StoriesController CreateStoriesController(IkarDbContext dbContext, Guid userId)
    {
        return CreateStoriesController(dbContext, userId, new NoopHubContext<MessengerHub>());
    }

    public static StoriesController CreateStoriesController(
        IkarDbContext dbContext,
        Guid userId,
        IHubContext<MessengerHub> hubContext,
        AttachmentStorageService? attachmentStorageService = null)
    {
        var controller = new StoriesController(
            dbContext,
            new PresenceTracker(),
            hubContext,
            attachmentStorageService ?? new AttachmentStorageService(new TestWebHostEnvironment(Path.GetTempPath())));
        AttachUser(controller, userId);
        return controller;
    }

    public static AttachmentStorageService CreateAttachmentStorageService(string contentRootPath) =>
        new(new TestWebHostEnvironment(contentRootPath));

    public static T ValueOf<T>(ActionResult<T> actionResult)
    {
        var ok = Assert.IsType<OkObjectResult>(actionResult.Result);
        return Assert.IsAssignableFrom<T>(ok.Value);
    }

    private static void AttachUser(ControllerBase controller, Guid userId)
    {
        var identity = new ClaimsIdentity(
            [new Claim(ClaimTypes.NameIdentifier, userId.ToString())],
            authenticationType: "Test");
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(identity)
            }
        };
    }

    private sealed class TestWebHostEnvironment(string contentRootPath) : IWebHostEnvironment
    {
        public string ApplicationName { get; set; } = "Ikar.Server.Tests";
        public IFileProvider WebRootFileProvider { get; set; } = new NullFileProvider();
        public string WebRootPath { get; set; } = contentRootPath;
        public string EnvironmentName { get; set; } = "Development";
        public string ContentRootPath { get; set; } = contentRootPath;
        public IFileProvider ContentRootFileProvider { get; set; } = new PhysicalFileProvider(contentRootPath);
    }
}

internal sealed record RecordedHubSend(
    string TargetKind,
    IReadOnlyList<string> Targets,
    string Method,
    int ArgumentCount);

internal sealed class RecordingHubContext<THub> : IHubContext<THub>
    where THub : Hub
{
    private readonly RecordingHubClients _clients = new();

    public IHubClients Clients => _clients;

    public IGroupManager Groups { get; } = new RecordingGroupManager();

    public IReadOnlyList<RecordedHubSend> Sends => _clients.Sends;

    private sealed class RecordingHubClients : IHubClients
    {
        private readonly List<RecordedHubSend> _sends = [];

        public IReadOnlyList<RecordedHubSend> Sends => _sends;

        public IClientProxy All => Proxy("All", []);

        public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => Proxy("AllExcept", excludedConnectionIds);

        public IClientProxy Client(string connectionId) => Proxy("Client", [connectionId]);

        public IClientProxy Clients(IReadOnlyList<string> connectionIds) => Proxy("Clients", connectionIds);

        public IClientProxy Group(string groupName) => Proxy("Group", [groupName]);

        public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) =>
            Proxy("GroupExcept", [groupName, .. excludedConnectionIds]);

        public IClientProxy Groups(IReadOnlyList<string> groupNames) => Proxy("Groups", groupNames);

        public IClientProxy User(string userId) => Proxy("User", [userId]);

        public IClientProxy Users(IReadOnlyList<string> userIds) => Proxy("Users", userIds);

        private IClientProxy Proxy(string targetKind, IReadOnlyList<string> targets) =>
            new RecordingClientProxy(_sends, targetKind, targets);
    }

    private sealed class RecordingClientProxy(
        List<RecordedHubSend> sends,
        string targetKind,
        IReadOnlyList<string> targets) : IClientProxy
    {
        public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
        {
            sends.Add(new RecordedHubSend(targetKind, targets.ToList(), method, args.Length));
            return Task.CompletedTask;
        }
    }

    private sealed class RecordingGroupManager : IGroupManager
    {
        public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;

        public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }
}

internal sealed class NoopHubContext<THub> : IHubContext<THub>
    where THub : Hub
{
    public IHubClients Clients { get; } = new NoopHubClients();

    public IGroupManager Groups { get; } = new NoopGroupManager();

    private sealed class NoopHubClients : IHubClients
    {
        private static readonly IClientProxy Proxy = new NoopClientProxy();

        public IClientProxy All => Proxy;

        public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => Proxy;

        public IClientProxy Client(string connectionId) => Proxy;

        public IClientProxy Clients(IReadOnlyList<string> connectionIds) => Proxy;

        public IClientProxy Group(string groupName) => Proxy;

        public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => Proxy;

        public IClientProxy Groups(IReadOnlyList<string> groupNames) => Proxy;

        public IClientProxy User(string userId) => Proxy;

        public IClientProxy Users(IReadOnlyList<string> userIds) => Proxy;
    }

    private sealed class NoopClientProxy : IClientProxy
    {
        public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }

    private sealed class NoopGroupManager : IGroupManager
    {
        public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;

        public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }
}
