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(); } }