using System.Collections.Concurrent;
using System.Text.Json;
using DorkNet.Server.Hubs;
using Microsoft.AspNetCore.SignalR;
namespace DorkNet.Server.Tests;
///
/// Stand-in for the notify hub that records every push instead of sending it,
/// so tests can assert on the exact frames the game client would receive.
/// A frame is the hub method plus its single string argument, parsed back into
/// the {Id, Msg} envelope.
///
public sealed class CapturingHubContext : IHubContext
{
private readonly ConcurrentQueue _sent = new();
public IHubClients Clients => new CapturingClients(this);
public IGroupManager Groups { get; } = new NoopGroups();
public IReadOnlyList Sent => _sent.ToArray();
/// Frames sent to a target such as group:player:12, in
/// order, optionally filtered by envelope Id.
public IReadOnlyList To(string target, string? id = null) =>
_sent.Where(f => f.Target == target && (id is null || f.Id == id)).ToList();
public static string PlayerGroup(long playerId) => $"group:{NotifyHub.GroupForPlayer(playerId)}";
internal void Record(string target, IReadOnlyList? excluded, string method, object?[] args) =>
_sent.Enqueue(new SentFrame(target, excluded ?? Array.Empty(), method,
args.Length == 1 && args[0] is string s ? s : JsonSerializer.Serialize(args)));
public sealed record SentFrame(string Target, IReadOnlyList Excluded, string Method, string Payload)
{
public JsonElement Envelope => JsonDocument.Parse(Payload).RootElement;
public string? Id => Envelope.TryGetProperty("Id", out var id) ? id.GetString() : null;
public JsonElement Msg => Envelope.GetProperty("Msg");
}
private sealed class CapturingClients(CapturingHubContext owner) : IHubClients
{
private IClientProxy Proxy(string target, IReadOnlyList? excluded = null) =>
new CapturingProxy(owner, target, excluded);
public IClientProxy All => Proxy("all");
public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) => Proxy("all", excludedConnectionIds);
public IClientProxy Client(string connectionId) => Proxy($"conn:{connectionId}");
public IClientProxy Clients(IReadOnlyList connectionIds) => Proxy($"conns:{string.Join(",", connectionIds)}");
public IClientProxy Group(string groupName) => Proxy($"group:{groupName}");
public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) =>
Proxy($"group:{groupName}", excludedConnectionIds);
public IClientProxy Groups(IReadOnlyList groupNames) => new FanOutProxy(groupNames.Select(g => Proxy($"group:{g}")).ToList());
public IClientProxy User(string userId) => Proxy($"user:{userId}");
public IClientProxy Users(IReadOnlyList userIds) => Proxy($"users:{string.Join(",", userIds)}");
}
private sealed class CapturingProxy(CapturingHubContext owner, string target, IReadOnlyList? excluded) : IClientProxy
{
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
{
owner.Record(target, excluded, method, args);
return Task.CompletedTask;
}
}
private sealed class FanOutProxy(IReadOnlyList proxies) : IClientProxy
{
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) =>
Task.WhenAll(proxies.Select(p => p.SendCoreAsync(method, args, cancellationToken)));
}
private sealed class NoopGroups : 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;
}
}