using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class GameRouteMirrorTests { private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-23T00:00:00Z"); private static NavWaypoint Point(long id)=>new(id,"System "+id,new(id,0,0),"K"); private static PlayerState Player=>new(){InGame=true,CommanderId="commander",LoadGameAt=Now,Navigation=new(){CurrentSystemAddress=1,Route=[Point(1),Point(2),Point(3)],RouteAt=Now}}; [Test] public void AGamePlottedRouteIsAdoptedOnceWithoutPlottingOrFlightCommands() { var mirror=new GameRouteMirror();var update=mirror.Observe(Player); Check.That(update.Changed&&!update.Cleared&&!update.Arrived);Check.Equal(3L,update.Destination!.SystemAddress); Check.That(!mirror.Observe(Player).Changed); Check.That(!mirror.Observe(Player with{InGame=false,Navigation=new()}).Changed); Check.That(!mirror.Observe(Player with{Navigation=Player.Navigation with{Route=[Point(1),Point(4)]}},true).Changed); } [Test] public void ManualClearAndFinalArrivalRemainDistinctAndANewGameRouteReplacesTheOldOne() { var mirror=new GameRouteMirror();mirror.Observe(Player); var cleared=mirror.Observe(Player with{Navigation=Player.Navigation with{Route=[],RouteAt=Now.AddSeconds(1)}}); Check.That(cleared.Cleared&&!cleared.Arrived); mirror.Observe(Player); var arrived=mirror.Observe(Player with{Navigation=Player.Navigation with{Route=[],CurrentSystemAddress=3,RouteAt=Now.AddSeconds(2)}}); Check.That(arrived.Arrived&&!arrived.Cleared); mirror.Observe(Player); var replaced=mirror.Observe(Player with{Navigation=Player.Navigation with{Route=[Point(1),Point(4)],RouteAt=Now.AddSeconds(3)}}); Check.That(replaced.Replaced);Check.Equal(4L,replaced.Destination!.SystemAddress); } [Test] public void ARecentMatchingFinalJumpClearRetainsTheRouteUntilActualArrival() { var mirror=new GameRouteMirror(()=>Now.AddSeconds(12));mirror.Observe(Player); var jumping=Player with{Navigation=Player.Navigation with{CurrentSystemAddress=2,Route=[],RouteAt=Now.AddSeconds(11),JumpInProgress=true, JumpTargetSystemAddress=3,JumpTargetSystem="System 3",JumpStartedAt=Now.AddSeconds(10)}}; Check.That(!mirror.Observe(jumping).Cleared); var arrived=mirror.Observe(jumping with{Navigation=jumping.Navigation with{JumpInProgress=false,LastJumpAt=Now.AddSeconds(12),CurrentSystemAddress=3}}); Check.That(arrived.Arrived&&!arrived.Cleared); mirror.Observe(Player); Check.That(mirror.Observe(jumping with{Navigation=jumping.Navigation with{JumpTargetSystemAddress=4,JumpTargetSystem="System 4"}}).Cleared); mirror.Observe(Player); Check.That(mirror.Observe(jumping with{Navigation=jumping.Navigation with{JumpStartedAt=Now.AddMinutes(-2)}}).Cleared); } [Test] public async Task CancellationRequiresBothTheOriginalGameAcknowledgementAndTheJournalClear() { var player=Player;var driver=new FakeDriver(); driver.Read=number=> { if(number==2)player=player with{Navigation=player.Navigation with{Route=[],RouteAt=Now.AddSeconds(1)}}; return new(71,true,false,false,0,0,false,true,0,0); }; await new GameRouteCancellation(()=>player,TimeSpan.FromSeconds(2)).CancelAsync(3,driver); Check.Equal(1,driver.Mutations);Check.That(driver.Reads>=3); } [Test] public async Task ChangingTheDestinationDuringCancellationPreservesTheNewRoute() { var player=Player;var driver=new FakeDriver(); driver.Read=_=>{player=player with{Navigation=player.Navigation with{Route=[Point(1),Point(4)]}};return new(71,false,true,false,0,0,false,false,0,0);}; await Rejects(()=>new GameRouteCancellation(()=>player).CancelAsync(3,driver)); Check.Equal(1,driver.Mutations);Check.Equal(4L,player.Navigation.Route[^1].SystemAddress); } [Test] public async Task UnsupportedOrAlreadyChangedRoutesCannotIssueNativeClear() { var driver=new FakeDriver{SupportsGameRouteCancellation=false}; await Rejects(()=>new GameRouteCancellation(()=>Player).CancelAsync(3,driver)); Check.Equal(0,driver.Mutations);driver.SupportsGameRouteCancellation=true; await Rejects(()=>new GameRouteCancellation(()=>Player).CancelAsync(4,driver)); Check.Equal(0,driver.Mutations); } [Test] public async Task AFalseNativeClearNeverClearsTheGameRouteOnScreen() { var driver=new FakeDriver{Read=_=>new(71,true,false,false,0,0,false,true,0,0)}; await Rejects(()=>new GameRouteCancellation(()=>Player,TimeSpan.FromMilliseconds(150)).CancelAsync(3,driver)); Check.Equal(1,driver.Mutations);Check.Equal(3,Player.Navigation.Route.Count); } private static async Task Rejects(Func action)where T:Exception {try{await action();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);} private sealed class FakeDriver:IGameRouteCancellationDriver { public bool SupportsGameRouteCancellation{get;set;}=true; public int Mutations,Reads; public Func Read{get;set;}=_=>new(71,false,true,false,3,2,true,true,3,123); public Task RequestGameRouteCancellationAsync(long destination,CancellationToken token){Mutations++;return Task.FromResult(71L);} public Task ReadGameRouteStatusAsync(long generation,long destination,CancellationToken token)=>Task.FromResult(Read(++Reads)); } }