using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class DockedPluginMaintenanceTests
{
private static readonly DateTimeOffset Start=DateTimeOffset.Parse("2026-10-25T12:00:00Z");
private static PlayerState Player(DateTimeOffset now)=>new(){InGame=true,Docked=true,CommanderId="actual-owner",GameMode="Solo",CurrentShipId=8,LoadGameAt=Start.AddHours(-1),
Ship="panthermkii",SystemName="AD Leonis",StationName="Homer Platform",MarketId=4210924803,StatusReadAt=now,CargoReliable=true,CreditsReliable=true,Credits=223879276,
CargoCapacity=752,Status=new(9,0,128,1,0,null,null,null,null,[]),Ships=[new(){ShipId=8,HullHealth=1,ConfigurationAt=Start.AddMinutes(-5)}]};
private static JsonElement Json(object value,Action<JsonObject>? change=null)
{var row=JsonSerializer.SerializeToNode(value)!.AsObject();change?.Invoke(row);return JsonSerializer.SerializeToElement(row);}
private static JsonElement Session(DateTimeOffset now,Action<JsonObject>? change=null)=>Json(new{state="InGame",nextState="InGame",sequence=5,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds()},change);
private static JsonElement Raw(DateTimeOffset now,int sequence=1,Action<JsonObject>? change=null)=>Json(new{inGame=true,localPlayerShipVerified=true,playerOverride=false,controlApplied=false,
entityId="11825808265181",referenceWorldAddress="1224984466817436488",sequence,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds(),speedMetresPerSecond=0d,
shipHealth=new{verified=true,entityId="11825808265181",sequence,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds(),hullFraction=1d,shieldFraction=1d,damageObserved=false,emergency=true},
fsd=new{ownedCharging=false}},change);
private static JsonElement Dock(Action<JsonObject>? change=null)=>Json(new{fresh=true,entityId="11825808265181",marketId=4210924803L,actualMarketId=4210924803L,rawDockingState=5,ready=false,stage="health",issue=(string?)null},change);
[Test]public void HistoricalEmergencyIsPreservedAndOnlyIndependentDockedFactsAuthorizeMaintenance()
{
var proof=new DockedPluginMaintenanceProof();
for(var i=0;i<12;i++){var now=Start.AddMilliseconds(i*200);proof.Accept(Player(now),Session(now),Raw(now,i+1),Dock(),now);}
Check.That(proof.Ready);Check.Equal(12,proof.Samples.Count);Check.That(proof.Samples.All(row=>row.OriginalEmergency));
Check.That(proof.Samples.All(row=>row.Raw.GetProperty("shipHealth").GetProperty("emergency").GetBoolean()));
}
[Test]public void PauseExitErrorsAndTransitionsCannotUseTheNonPlayingRecoveryFallback()
{
foreach(var state in new[]{"Pause","ExitOptions","MainMenu","Rebirth","NetworkError"})
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start,row=>{row["state"]=state;row["nextState"]=state;}),Raw(Start),Dock(),Start));
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start,row=>row["nextState"]="Pause"),Raw(Start),Dock(),Start));
}
[Test]public void MovingDamagedUnknownOrActivelyControlledShipsAreRejected()
{
foreach(var change in new Action<JsonObject>[]
{
row=>row["speedMetresPerSecond"]=.051,
row=>row["shipHealth"]!["hullFraction"]=.99,
row=>row["shipHealth"]!["shieldFraction"]=.99,
row=>row["shipHealth"]!["damageObserved"]=true,
row=>row["localPlayerShipVerified"]=false,
row=>row["playerOverride"]=true,
row=>row["controlApplied"]=true,
row=>row["fsd"]!["ownedCharging"]=true,
row=>row["observedAtUnixMilliseconds"]=Start.AddSeconds(-1).ToUnixTimeMilliseconds(),
row=>row.Remove("controlApplied")
})Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start),Raw(Start,change:change),Dock(),Start));
}
[Test]public void NativeDockingJournalAndSameShipBaselineMustAllAgree()
{
foreach(var mode in new[]{0,1,2,3,4})
{
var proof=new DockedPluginMaintenanceProof();proof.Accept(Player(Start),Session(Start),Raw(Start),Dock(),Start);
var now=Start.AddMilliseconds(200);var player=Player(now);var dock=Dock();var raw=Raw(now,2);
if(mode==0)player=player with{CurrentShipId=9,Ships=[new(){ShipId=9,HullHealth=1,ConfigurationAt=Start.AddMinutes(-5)}]};
else if(mode==1)player=player with{Docked=false};
else if(mode==2)dock=Dock(row=>row["actualMarketId"]=123);
else if(mode==3)raw=Raw(now,2,row=>{row["entityId"]="999";row["shipHealth"]!["entityId"]="999";});
else player=player with{CargoCount=1,Inventory=[new("gold","Gold",1)]};
Check.Throws<InvalidOperationException>(()=>proof.Accept(player,Session(now),raw,dock,now));
}
}
[Test]public void ReplayedNativeSamplesAndStaleStatusCannotSatisfyTheObservationWindow()
{
var proof=new DockedPluginMaintenanceProof();proof.Accept(Player(Start),Session(Start),Raw(Start),Dock(),Start);
var now=Start.AddMilliseconds(200);
Check.Throws<InvalidOperationException>(()=>proof.Accept(Player(now),Session(now),Raw(now),Dock(),now));
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start) with{StatusReadAt=Start.AddSeconds(-2)},Session(Start),Raw(Start),Dock(),Start));
Check.That(!proof.Ready);
}
}
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class DockedPluginMaintenanceTests
{
private static readonly DateTimeOffset Start=DateTimeOffset.Parse("2026-10-25T12:00:00Z");
private static PlayerState Player(DateTimeOffset now)=>new(){InGame=true,Docked=true,CommanderId="actual-owner",GameMode="Solo",CurrentShipId=8,LoadGameAt=Start.AddHours(-1),
Ship="panthermkii",SystemName="AD Leonis",StationName="Homer Platform",MarketId=4210924803,StatusReadAt=now,CargoReliable=true,CreditsReliable=true,Credits=223879276,
CargoCapacity=752,Status=new(9,0,128,1,0,null,null,null,null,[]),Ships=[new(){ShipId=8,HullHealth=1,ConfigurationAt=Start.AddMinutes(-5)}]};
private static JsonElement Json(object value,Action<JsonObject>? change=null)
{var row=JsonSerializer.SerializeToNode(value)!.AsObject();change?.Invoke(row);return JsonSerializer.SerializeToElement(row);}
private static JsonElement Session(DateTimeOffset now,Action<JsonObject>? change=null)=>Json(new{state="InGame",nextState="InGame",sequence=5,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds()},change);
private static JsonElement Raw(DateTimeOffset now,int sequence=1,Action<JsonObject>? change=null)=>Json(new{inGame=true,localPlayerShipVerified=true,playerOverride=false,controlApplied=false,
entityId="11825808265181",referenceWorldAddress="1224984466817436488",sequence,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds(),speedMetresPerSecond=0d,
shipHealth=new{verified=true,entityId="11825808265181",sequence,observedAtUnixMilliseconds=now.ToUnixTimeMilliseconds(),hullFraction=1d,shieldFraction=1d,damageObserved=false,emergency=true},
fsd=new{ownedCharging=false}},change);
private static JsonElement Dock(Action<JsonObject>? change=null)=>Json(new{fresh=true,entityId="11825808265181",marketId=4210924803L,actualMarketId=4210924803L,rawDockingState=5,ready=false,stage="health",issue=(string?)null},change);
[Test]public void HistoricalEmergencyIsPreservedAndOnlyIndependentDockedFactsAuthorizeMaintenance()
{
var proof=new DockedPluginMaintenanceProof();
for(var i=0;i<12;i++){var now=Start.AddMilliseconds(i*200);proof.Accept(Player(now),Session(now),Raw(now,i+1),Dock(),now);}
Check.That(proof.Ready);Check.Equal(12,proof.Samples.Count);Check.That(proof.Samples.All(row=>row.OriginalEmergency));
Check.That(proof.Samples.All(row=>row.Raw.GetProperty("shipHealth").GetProperty("emergency").GetBoolean()));
}
[Test]public void PauseExitErrorsAndTransitionsCannotUseTheNonPlayingRecoveryFallback()
{
foreach(var state in new[]{"Pause","ExitOptions","MainMenu","Rebirth","NetworkError"})
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start,row=>{row["state"]=state;row["nextState"]=state;}),Raw(Start),Dock(),Start));
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start,row=>row["nextState"]="Pause"),Raw(Start),Dock(),Start));
}
[Test]public void MovingDamagedUnknownOrActivelyControlledShipsAreRejected()
{
foreach(var change in new Action<JsonObject>[]
{
row=>row["speedMetresPerSecond"]=.051,
row=>row["shipHealth"]!["hullFraction"]=.99,
row=>row["shipHealth"]!["shieldFraction"]=.99,
row=>row["shipHealth"]!["damageObserved"]=true,
row=>row["localPlayerShipVerified"]=false,
row=>row["playerOverride"]=true,
row=>row["controlApplied"]=true,
row=>row["fsd"]!["ownedCharging"]=true,
row=>row["observedAtUnixMilliseconds"]=Start.AddSeconds(-1).ToUnixTimeMilliseconds(),
row=>row.Remove("controlApplied")
})Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start),Session(Start),Raw(Start,change:change),Dock(),Start));
}
[Test]public void NativeDockingJournalAndSameShipBaselineMustAllAgree()
{
foreach(var mode in new[]{0,1,2,3,4})
{
var proof=new DockedPluginMaintenanceProof();proof.Accept(Player(Start),Session(Start),Raw(Start),Dock(),Start);
var now=Start.AddMilliseconds(200);var player=Player(now);var dock=Dock();var raw=Raw(now,2);
if(mode==0)player=player with{CurrentShipId=9,Ships=[new(){ShipId=9,HullHealth=1,ConfigurationAt=Start.AddMinutes(-5)}]};
else if(mode==1)player=player with{Docked=false};
else if(mode==2)dock=Dock(row=>row["actualMarketId"]=123);
else if(mode==3)raw=Raw(now,2,row=>{row["entityId"]="999";row["shipHealth"]!["entityId"]="999";});
else player=player with{CargoCount=1,Inventory=[new("gold","Gold",1)]};
Check.Throws<InvalidOperationException>(()=>proof.Accept(player,Session(now),raw,dock,now));
}
}
[Test]public void ReplayedNativeSamplesAndStaleStatusCannotSatisfyTheObservationWindow()
{
var proof=new DockedPluginMaintenanceProof();proof.Accept(Player(Start),Session(Start),Raw(Start),Dock(),Start);
var now=Start.AddMilliseconds(200);
Check.Throws<InvalidOperationException>(()=>proof.Accept(Player(now),Session(now),Raw(now),Dock(),now));
Check.Throws<InvalidOperationException>(()=>new DockedPluginMaintenanceProof().Accept(Player(Start) with{StatusReadAt=Start.AddSeconds(-2)},Session(Start),Raw(Start),Dock(),Start));
Check.That(!proof.Ready);
}
}