using Newtonsoft.Json.Linq;
using OpenAC.MosswartMassacre.Infrastructure;
using OpenAC.MosswartMassacre.Tests.Fakes;
namespace OpenAC.MosswartMassacre.Tests;
/// A clock a test sets by hand.
internal sealed class FixedClock : IClock
{
public DateTime UtcNow { get; set; } = new(2026, 9, 25, 8, 30, 15, 123, DateTimeKind.Utc);
public DateTime Now { get; set; } = new(2026, 9, 25, 10, 30, 15, 123, DateTimeKind.Local);
public void Advance(TimeSpan by)
{
UtcNow += by;
Now += by;
}
}
///
/// The plugin built on a fake host and a fixed clock, logged in as a named
/// character with streaming on, and every frame it sends recorded.
///
internal sealed class PluginHarness : IDisposable
{
public PluginHarness(string characterName = "Mossy", bool webSocketEnabled = true, Action? arrange = null)
{
Host.Automation.IsAvailable = true;
Host.Automation.Character.Name = characterName;
Host.Automation.Character.IsInWorld = false;
Host.Automation.Character.ObjectId = 0x50000001;
arrange?.Invoke(Host);
Plugin.Initialize(Host);
Plugin.Build(Host, Clock);
Context.FrameTap = frame => Frames.Add(frame);
// The stored settings keep streaming off so a login never opens a real
// socket; Login() turns it on in memory afterwards, which is all the
// frame gate reads.
Context.Settings.Load(characterName);
Context.Settings.Current.WebSocketEnabled = false;
Context.Settings.Current.BackendUrl = "ws://127.0.0.1:1/unused/";
Context.Settings.Save();
_webSocketEnabled = webSocketEnabled;
}
private readonly bool _webSocketEnabled;
public FakeHost Host { get; } = new();
public FixedClock Clock { get; } = new();
public MosswartMassacrePlugin Plugin { get; } = new();
public MmContext Context => Plugin.Context!;
public List Frames { get; } = [];
public IReadOnlyList ChatLines => Host.ChatLines;
/// Logs in without starting the socket.
public void Login()
{
Host.Automation.Character.IsInWorld = true;
Plugin.OnLogin();
Context.Settings.Current.WebSocketEnabled = _webSocketEnabled;
}
/// Runs what the plugin runs right after the socket connects and registers.
public void SimulateConnected() => Session.OnConnected(Clock.UtcNow);
public OpenAC.MosswartMassacre.Backend.BackendSession Session =>
Plugin.Features.OfType().Single();
/// A frame as if the backend had sent it; routed on the next tick.
public void Receive(string frame) => Session.Route(frame);
public T Feature() where T : IMmFeature => Plugin.Features.OfType().Single();
public void Tick(double seconds) => Host.Events.RaiseTick(seconds);
/// Advances both the scheduler and the clock by in small steps.
public void Run(double seconds, double step = 0.1)
{
for (double t = 0; t < seconds - 1e-9; t += step)
{
Clock.Advance(TimeSpan.FromSeconds(step));
Tick(step);
}
}
public IEnumerable FramesOfType(string type) =>
Frames.Select(JObject.Parse).Where(frame => (string?)frame["type"] == type);
public void Dispose()
{
Plugin.OnLogoff();
foreach (IMmFeature feature in Plugin.Features)
feature.Dispose();
}
}