using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]public class SessionMaintenanceFlightGateTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-25T19:00:00Z");
private static PlayerState Ship(DateTimeOffset at)=>new(){CommanderId="maintenance",CurrentShipId=8,CargoCapacity=1200,
InGame=true,LoadGameAt=Now.AddHours(-1),CargoReliable=true,CreditsReliable=true,CargoCount=0,StatusReadAt=at,Ships=[new(){ShipId=8,HullHealth=1}]};
private static JsonElement Raw(DateTimeOffset at,bool health=true,bool damage=false,bool emergency=false,double hull=1,bool manual=false,string entity="7",string world="9",long healthSequence=1)
=>Check.Event(new{sequence=1,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),entityId=entity,referenceWorldAddress=world,
inGame=true,localPlayerShipVerified=true,playerOverride=manual,shipHealth=new{sequence=healthSequence,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),
entityId=entity,verified=health,damageObserved=damage,emergency,hullFraction=hull}});
private static JsonElement Local(DateTimeOffset at,double speed=0,double throttle=0,int fsd=2,bool sc=false,bool duty=false,string entity="7",string world="9")
=>Check.Event(new{sequence=1,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),entityId=entity,referenceWorldAddress=world,
rawFsdState=fsd,inSupercruise=sc,speedMetresPerSecond=speed,throttle,docking=new{verified=true,nativeDutyActive=duty,nativeLaunchActive=false}});
[Test]public void EmptyStationaryHealthyShipRequiresTenContinuousSecondsBeforeMenuUpdate()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));Check.That(!gate.Observe(Ship(Now),Raw(Now),Local(Now),Now));
var at=Now.AddSeconds(9);Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
at=Now.AddSeconds(10);Check.That(gate.Observe(Ship(at),Raw(at),Local(at),at));
}
[Test]public void CargoUnreliableStateAndExternalThreatCannotAuthorizeSpaceUpdate()
{
var at=Now.AddSeconds(10);
foreach(var current in new[]{Ship(at) with{CargoCount=1},Ship(at) with{CargoReliable=false},Ship(at) with{CreditsReliable=false},
Ship(at) with{StatusReadAt=at.AddSeconds(-3)},Ship(at) with{Navigation=new(){ExternalThreat=true}},Ship(at) with{Docked=true}})
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
Check.That(!gate.Observe(current,Raw(at),Local(at),at));
Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
}
}
[Test]public void NativeDamageMotionActiveDutyAndMismatchedOrStaleFramesResetWaiting()
{
var at=Now.AddSeconds(10);
foreach(var (raw,local) in new[]{(Raw(at,health:false),Local(at)),(Raw(at,damage:true),Local(at)),(Raw(at,emergency:true),Local(at)),
(Raw(at,hull:.8),Local(at)),(Raw(at),Local(at,speed:1)),(Raw(at),Local(at,throttle:.1)),(Raw(at),Local(at,fsd:3)),
(Raw(at),Local(at,sc:true)),(Raw(at),Local(at,duty:true)),(Raw(at),Local(at,entity:"8")),(Raw(at),Local(at,world:"10")),
(Raw(at.AddSeconds(-1)),Local(at)),(Raw(at),Local(at.AddSeconds(-1))),(Raw(at,healthSequence:2),Local(at))})
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
Check.That(!gate.Observe(Ship(at),raw,local,at));Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
}
}
[Test]public void IdentitySessionChangesAndPhysicalTakeoverRemainTerminal()
{
foreach(var current in new[]{Ship(Now) with{CommanderId="other"},Ship(Now) with{CurrentShipId=9},Ship(Now) with{CargoCapacity=2000},Ship(Now) with{LoadGameAt=Now}})
Check.Throws<InvalidOperationException>(()=>new SessionMaintenanceFlightGate(Ship(Now)).Observe(current,Raw(Now),Local(Now),Now));
Check.Throws<InvalidOperationException>(()=>new SessionMaintenanceFlightGate(Ship(Now)).Observe(Ship(Now),Raw(Now,manual:true),Local(Now),Now));
}
[Test]public void ChangedEntityOrWorldNeedsANewCompleteStabilityWindow()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
var at=Now.AddSeconds(10);Check.That(!gate.Observe(Ship(at),Raw(at,entity:"8",world:"10"),Local(at,entity:"8",world:"10"),at));
at=Now.AddSeconds(20);Check.That(gate.Observe(Ship(at),Raw(at,entity:"8",world:"10"),Local(at,entity:"8",world:"10"),at));
}
[Test]public void StableHistoricalHullCanExitForMaintenanceButAnyFurtherDecreaseRestartsTheWindow()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));
Check.That(!gate.Observe(Ship(Now),Raw(Now,hull:.502883),Local(Now),Now));
var at=Now.AddSeconds(10);Check.That(gate.Observe(Ship(at),Raw(at,hull:.502883),Local(at),at));
at=Now.AddSeconds(11);Check.That(!gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
at=Now.AddSeconds(20);Check.That(!gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
at=Now.AddSeconds(21);Check.That(gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
foreach(var hull in new[]{0d,-.1,1.1})
Check.That(!new SessionMaintenanceFlightGate(Ship(Now)).Observe(Ship(at),Raw(at,hull:hull),Local(at),at));
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]public class SessionMaintenanceFlightGateTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-25T19:00:00Z");
private static PlayerState Ship(DateTimeOffset at)=>new(){CommanderId="maintenance",CurrentShipId=8,CargoCapacity=1200,
InGame=true,LoadGameAt=Now.AddHours(-1),CargoReliable=true,CreditsReliable=true,CargoCount=0,StatusReadAt=at,Ships=[new(){ShipId=8,HullHealth=1}]};
private static JsonElement Raw(DateTimeOffset at,bool health=true,bool damage=false,bool emergency=false,double hull=1,bool manual=false,string entity="7",string world="9",long healthSequence=1)
=>Check.Event(new{sequence=1,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),entityId=entity,referenceWorldAddress=world,
inGame=true,localPlayerShipVerified=true,playerOverride=manual,shipHealth=new{sequence=healthSequence,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),
entityId=entity,verified=health,damageObserved=damage,emergency,hullFraction=hull}});
private static JsonElement Local(DateTimeOffset at,double speed=0,double throttle=0,int fsd=2,bool sc=false,bool duty=false,string entity="7",string world="9")
=>Check.Event(new{sequence=1,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),entityId=entity,referenceWorldAddress=world,
rawFsdState=fsd,inSupercruise=sc,speedMetresPerSecond=speed,throttle,docking=new{verified=true,nativeDutyActive=duty,nativeLaunchActive=false}});
[Test]public void EmptyStationaryHealthyShipRequiresTenContinuousSecondsBeforeMenuUpdate()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));Check.That(!gate.Observe(Ship(Now),Raw(Now),Local(Now),Now));
var at=Now.AddSeconds(9);Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
at=Now.AddSeconds(10);Check.That(gate.Observe(Ship(at),Raw(at),Local(at),at));
}
[Test]public void CargoUnreliableStateAndExternalThreatCannotAuthorizeSpaceUpdate()
{
var at=Now.AddSeconds(10);
foreach(var current in new[]{Ship(at) with{CargoCount=1},Ship(at) with{CargoReliable=false},Ship(at) with{CreditsReliable=false},
Ship(at) with{StatusReadAt=at.AddSeconds(-3)},Ship(at) with{Navigation=new(){ExternalThreat=true}},Ship(at) with{Docked=true}})
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
Check.That(!gate.Observe(current,Raw(at),Local(at),at));
Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
}
}
[Test]public void NativeDamageMotionActiveDutyAndMismatchedOrStaleFramesResetWaiting()
{
var at=Now.AddSeconds(10);
foreach(var (raw,local) in new[]{(Raw(at,health:false),Local(at)),(Raw(at,damage:true),Local(at)),(Raw(at,emergency:true),Local(at)),
(Raw(at,hull:.8),Local(at)),(Raw(at),Local(at,speed:1)),(Raw(at),Local(at,throttle:.1)),(Raw(at),Local(at,fsd:3)),
(Raw(at),Local(at,sc:true)),(Raw(at),Local(at,duty:true)),(Raw(at),Local(at,entity:"8")),(Raw(at),Local(at,world:"10")),
(Raw(at.AddSeconds(-1)),Local(at)),(Raw(at),Local(at.AddSeconds(-1))),(Raw(at,healthSequence:2),Local(at))})
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
Check.That(!gate.Observe(Ship(at),raw,local,at));Check.That(!gate.Observe(Ship(at),Raw(at),Local(at),at));
}
}
[Test]public void IdentitySessionChangesAndPhysicalTakeoverRemainTerminal()
{
foreach(var current in new[]{Ship(Now) with{CommanderId="other"},Ship(Now) with{CurrentShipId=9},Ship(Now) with{CargoCapacity=2000},Ship(Now) with{LoadGameAt=Now}})
Check.Throws<InvalidOperationException>(()=>new SessionMaintenanceFlightGate(Ship(Now)).Observe(current,Raw(Now),Local(Now),Now));
Check.Throws<InvalidOperationException>(()=>new SessionMaintenanceFlightGate(Ship(Now)).Observe(Ship(Now),Raw(Now,manual:true),Local(Now),Now));
}
[Test]public void ChangedEntityOrWorldNeedsANewCompleteStabilityWindow()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));gate.Observe(Ship(Now),Raw(Now),Local(Now),Now);
var at=Now.AddSeconds(10);Check.That(!gate.Observe(Ship(at),Raw(at,entity:"8",world:"10"),Local(at,entity:"8",world:"10"),at));
at=Now.AddSeconds(20);Check.That(gate.Observe(Ship(at),Raw(at,entity:"8",world:"10"),Local(at,entity:"8",world:"10"),at));
}
[Test]public void StableHistoricalHullCanExitForMaintenanceButAnyFurtherDecreaseRestartsTheWindow()
{
var gate=new SessionMaintenanceFlightGate(Ship(Now));
Check.That(!gate.Observe(Ship(Now),Raw(Now,hull:.502883),Local(Now),Now));
var at=Now.AddSeconds(10);Check.That(gate.Observe(Ship(at),Raw(at,hull:.502883),Local(at),at));
at=Now.AddSeconds(11);Check.That(!gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
at=Now.AddSeconds(20);Check.That(!gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
at=Now.AddSeconds(21);Check.That(gate.Observe(Ship(at),Raw(at,hull:.50),Local(at),at));
foreach(var hull in new[]{0d,-.1,1.1})
Check.That(!new SessionMaintenanceFlightGate(Ship(Now)).Observe(Ship(at),Raw(at,hull:hull),Local(at),at));
}
}