using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using TheLivingWorld.Api.Simulation; using TheLivingWorld.Api.Storage; using TheLivingWorld.Core.Contracts; using TheLivingWorld.Core.Simulation; namespace TheLivingWorld.Tests; public sealed class WorldSimulationHostTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), $"tlw-sim-{Guid.NewGuid():n}"); private readonly WorldStore _store; private readonly WorldSimulationHost _host; public WorldSimulationHostTests() { _store = new WorldStore( Options.Create(new WorldStorageOptions { RootDirectory = _root }), NullLogger.Instance); _host = new WorldSimulationHost(_store, NullLogger.Instance); } [Fact] public async Task Attach_hydrates_legacy_worlds_without_catch_up_from_created_at() { var legacy = new WorldSummaryDto { Id = "legacy-11111111", Name = "Legacy", Latitude = 31.9, Longitude = -100.5, SizeMeters = 10_000, Status = WorldStatus.Ready, CreatedAt = DateTimeOffset.UtcNow - TimeSpan.FromDays(30), }; await _store.SaveSummaryAsync(legacy); // Mimic host startup hydration for worlds that lack a clock. var seeded = legacy with { Clock = WorldSimulation.DefaultClock(), LastTickedAt = DateTimeOffset.UtcNow, }; await _store.SaveSummaryAsync(seeded); _host.Attach(seeded with { LastTickedAt = null }); // Attach without prior tick → no catch-up // Force attach path used for legacy: Create with catchUp false via AttachSeeded behaviour — // Attach with LastTickedAt null means catchUp is false. var clock = _host.TryGetClock(legacy.Id); Assert.NotNull(clock); Assert.Equal(GameTime.DefaultStart, clock.GameTime); } [Fact] public async Task UpdateClock_changes_pause_and_Detach_stops_simulation() { var summary = new WorldSummaryDto { Id = "ready-22222222", Name = "Ready", Latitude = 31.9, Longitude = -100.5, SizeMeters = 10_000, Status = WorldStatus.Ready, CreatedAt = DateTimeOffset.UtcNow, Clock = WorldSimulation.DefaultClock(), LastTickedAt = DateTimeOffset.UtcNow, }; await _store.SaveSummaryAsync(summary); _host.Attach(summary); var paused = _host.UpdateClock(summary.Id, new UpdateClockRequest { Paused = true }); Assert.True(paused.Paused); var scaled = _host.UpdateClock(summary.Id, new UpdateClockRequest { TimeScale = 3 }); Assert.Equal(3, scaled.TimeScale); Assert.True(scaled.Paused); _host.Detach(summary.Id); Assert.False(_host.IsAttached(summary.Id)); Assert.Null(_host.TryGetClock(summary.Id)); } [Fact] public void Overlay_strips_last_ticked_at_for_api() { var summary = new WorldSummaryDto { Id = "ready-33333333", Name = "Ready", Latitude = 31.9, Longitude = -100.5, SizeMeters = 10_000, Status = WorldStatus.Ready, CreatedAt = DateTimeOffset.UtcNow, Clock = WorldSimulation.DefaultClock(), LastTickedAt = DateTimeOffset.UtcNow, }; _host.Attach(summary); var overlaid = _host.Overlay(summary); Assert.NotNull(overlaid.Clock); Assert.Null(overlaid.LastTickedAt); } public void Dispose() { foreach (var id in new[] { "legacy-11111111", "ready-22222222", "ready-33333333" }) _host.Detach(id); if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); GC.SuppressFinalize(this); } }