using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class GameJournalClockTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-26T05:49:28.536Z");
private static PlayerState Ahead=>new(){InGame=true,Status=new(16777232,0,32,1,0,null,null,null,null,[]),
LoadGameAt=Now.AddMinutes(-2),StatusAt=Now.AddSeconds(6.464),StatusWrittenAt=Now,StatusReadAt=Now.AddMilliseconds(10)};
[Test]public void AVerifiedLocalWriteExplainsTheRealSixSecondGameClockLead()
{
var now=Now.AddMilliseconds(20);Check.That(TravelPreparation.StatusConfirmed(Ahead,now));
Check.Equal(TimeSpan.FromSeconds(6.464),GameJournalClock.Offset(Ahead,now)!.Value);
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusWrittenAt=null},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusAt=Now.AddSeconds(31)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusReadAt=Now.AddSeconds(-3)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusWrittenAt=Now.AddSeconds(1)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{LoadGameAt=Now.AddSeconds(7)},now));
}
[Test]public void MatchingFinalJumpClearUsesTheJournalClockWithoutWeakeningWallClockFreshness()
{
NavWaypoint Point(long id)=>new(id,"System "+id,new(id,0,0),"K");
var player=Ahead with{CommanderId="F1",Navigation=new(){CurrentSystemAddress=1,Route=[Point(1),Point(2)],RouteAt=Now.AddMinutes(-1)}};
var mirror=new GameRouteMirror(()=>Now.AddMilliseconds(20));mirror.Observe(player);
var jumping=player with{Navigation=player.Navigation with{Route=[],RouteAt=Now.AddSeconds(6.464),JumpStartedAt=Now.AddSeconds(3),
JumpInProgress=true,JumpTargetSystemAddress=2,JumpTargetSystem="System 2"}};
Check.That(!mirror.Observe(jumping).Cleared);
var arrived=jumping with{Navigation=jumping.Navigation with{CurrentSystemAddress=2,JumpInProgress=false,LastJumpAt=Now.AddSeconds(7)}};
Check.That(mirror.Observe(arrived).Arrived);
}
[Test]public void ArrivalTailWindowsUseOnlyTheVerifiedJournalClockCorrection()
{
var player=Ahead with{Navigation=new(){LastJumpAt=Now.AddSeconds(6.464)}};var now=Now.AddMilliseconds(20);
Check.That(GameJournalClock.IsRecentArrival(player,now));
Check.That(!GameJournalClock.IsRecentArrival(player with{StatusWrittenAt=null},now));
Check.That(!GameJournalClock.IsRecentArrival(player with{Navigation=player.Navigation with{LastJumpAt=Now.AddSeconds(-25)}},now));
Check.That(!GameJournalClock.IsRecentArrival(player with{Navigation=player.Navigation with{LastJumpAt=Now.AddMinutes(1)}},now));
}
[Test]public async Task StableFileMetadataIsRetainedAcrossUnchangedReadsWithoutInventingStatusEvents()
{
using var directory=new TemporaryDirectory();var path=directory.File("Status.json");var written=DateTimeOffset.UtcNow;
await File.WriteAllTextAsync(path,JsonSerializer.Serialize(new{timestamp=written.AddSeconds(6.5),@event="Status",Flags=16777232}));
File.SetLastWriteTimeUtc(path,written.UtcDateTime);await using var monitor=new JournalMonitor(directory.Path);await monitor.StartAsync();
var original=monitor.State.StatusWrittenAt;Check.Equal(written,original!.Value);Check.That(TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow));
await Task.Delay(850);Check.Equal(original,monitor.State.StatusWrittenAt);Check.That(TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow));
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class GameJournalClockTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-26T05:49:28.536Z");
private static PlayerState Ahead=>new(){InGame=true,Status=new(16777232,0,32,1,0,null,null,null,null,[]),
LoadGameAt=Now.AddMinutes(-2),StatusAt=Now.AddSeconds(6.464),StatusWrittenAt=Now,StatusReadAt=Now.AddMilliseconds(10)};
[Test]public void AVerifiedLocalWriteExplainsTheRealSixSecondGameClockLead()
{
var now=Now.AddMilliseconds(20);Check.That(TravelPreparation.StatusConfirmed(Ahead,now));
Check.Equal(TimeSpan.FromSeconds(6.464),GameJournalClock.Offset(Ahead,now)!.Value);
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusWrittenAt=null},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusAt=Now.AddSeconds(31)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusReadAt=Now.AddSeconds(-3)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{StatusWrittenAt=Now.AddSeconds(1)},now));
Check.That(!TravelPreparation.StatusConfirmed(Ahead with{LoadGameAt=Now.AddSeconds(7)},now));
}
[Test]public void MatchingFinalJumpClearUsesTheJournalClockWithoutWeakeningWallClockFreshness()
{
NavWaypoint Point(long id)=>new(id,"System "+id,new(id,0,0),"K");
var player=Ahead with{CommanderId="F1",Navigation=new(){CurrentSystemAddress=1,Route=[Point(1),Point(2)],RouteAt=Now.AddMinutes(-1)}};
var mirror=new GameRouteMirror(()=>Now.AddMilliseconds(20));mirror.Observe(player);
var jumping=player with{Navigation=player.Navigation with{Route=[],RouteAt=Now.AddSeconds(6.464),JumpStartedAt=Now.AddSeconds(3),
JumpInProgress=true,JumpTargetSystemAddress=2,JumpTargetSystem="System 2"}};
Check.That(!mirror.Observe(jumping).Cleared);
var arrived=jumping with{Navigation=jumping.Navigation with{CurrentSystemAddress=2,JumpInProgress=false,LastJumpAt=Now.AddSeconds(7)}};
Check.That(mirror.Observe(arrived).Arrived);
}
[Test]public void ArrivalTailWindowsUseOnlyTheVerifiedJournalClockCorrection()
{
var player=Ahead with{Navigation=new(){LastJumpAt=Now.AddSeconds(6.464)}};var now=Now.AddMilliseconds(20);
Check.That(GameJournalClock.IsRecentArrival(player,now));
Check.That(!GameJournalClock.IsRecentArrival(player with{StatusWrittenAt=null},now));
Check.That(!GameJournalClock.IsRecentArrival(player with{Navigation=player.Navigation with{LastJumpAt=Now.AddSeconds(-25)}},now));
Check.That(!GameJournalClock.IsRecentArrival(player with{Navigation=player.Navigation with{LastJumpAt=Now.AddMinutes(1)}},now));
}
[Test]public async Task StableFileMetadataIsRetainedAcrossUnchangedReadsWithoutInventingStatusEvents()
{
using var directory=new TemporaryDirectory();var path=directory.File("Status.json");var written=DateTimeOffset.UtcNow;
await File.WriteAllTextAsync(path,JsonSerializer.Serialize(new{timestamp=written.AddSeconds(6.5),@event="Status",Flags=16777232}));
File.SetLastWriteTimeUtc(path,written.UtcDateTime);await using var monitor=new JournalMonitor(directory.Path);await monitor.StartAsync();
var original=monitor.State.StatusWrittenAt;Check.Equal(written,original!.Value);Check.That(TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow));
await Task.Delay(850);Check.Equal(original,monitor.State.StatusWrittenAt);Check.That(TravelPreparation.StatusConfirmed(monitor.State,DateTimeOffset.UtcNow));
}
}