using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeTargetSelectionRecoveryTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-27T07:06:03+08:00");
private static JsonObject Raw()=>new()
{
["sequence"]=7,["observedAtUnixMilliseconds"]=Now.ToUnixTimeMilliseconds(),["entityId"]="42",["referenceWorldAddress"]="1",
["localPlayerShipVerified"]=true,["inGame"]=true,["playerOverride"]=false,["rawFsdState"]=2,
["shipHealth"]=new JsonObject{["verified"]=true,["entityId"]="42",["sequence"]=7,["observedAtUnixMilliseconds"]=Now.ToUnixTimeMilliseconds(),
["hullFraction"]=1,["emergency"]=false,["damageObserved"]=false}
};
private static JsonObject Fsd()=>new(){["fresh"]=true,["entityId"]="42",["ownedCharging"]=false,["state"]=2,["targetValid"]=true,["targetAddress"]="2"};
private static NativeTargetSelectionReadiness Inspect(JsonObject raw,JsonObject fsd)=>NativeTargetSelectionRecovery.Inspect(JsonSerializer.SerializeToElement(raw),JsonSerializer.SerializeToElement(fsd),2,1,Now);
private static PlayerState Arrived=>new(){InGame=true,CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=Now.AddHours(-1),CargoCount=1200,
Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,CurrentSystemAt=Now.AddSeconds(-6),LastJumpAt=Now.AddSeconds(-6),Route=[new(1,"Here",new(0,0,0),"M"),new(2,"Next",new(1,0,0),"M")]}};
[Test]public void FreshIdleCanRequestButDoesNotItselfAcknowledgeADestination()
{
var check=Inspect(Raw(),Fsd());Check.That(check.CanRequest&&!check.Reject);
var raw=Raw();var fsd=Fsd();raw["rawFsdState"]=1;fsd["state"]=1;
Check.That(Inspect(raw,fsd).CanRequest);fsd["targetAddress"]="3";Check.That(!Inspect(raw,fsd).CanRequest);
}
[Test]public void OwnedChargingJumpingAndDifferentNativePhasesOnlyWaitWithoutRetargeting()
{
foreach(var (rawState,state,owned) in new[]{(3,3,false),(4,4,false),(2,2,true),(4,2,false),(2,4,false),(99,99,false)})
{
var raw=Raw();var fsd=Fsd();raw["rawFsdState"]=rawState;fsd["state"]=state;fsd["ownedCharging"]=owned;
var check=Inspect(raw,fsd);Check.That(!check.CanRequest&&!check.Reject);
}
}
[Test]public void SourceIdentityWorldFreshnessAndHealthMustAllMatchTheSameRawFrame()
{
foreach(Action<JsonObject,JsonObject> alter in new Action<JsonObject,JsonObject>[]
{
(r,f)=>f["entityId"]="43",(r,f)=>r["referenceWorldAddress"]="3",(r,f)=>f["fresh"]=false,
(r,f)=>r["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds(),
(r,f)=>r["shipHealth"]!["sequence"]=6,(r,f)=>r["shipHealth"]!["observedAtUnixMilliseconds"]=Now.AddMilliseconds(-1).ToUnixTimeMilliseconds(),
(r,f)=>r["shipHealth"]!["entityId"]="43",(r,f)=>r["localPlayerShipVerified"]=false
})
{var raw=Raw();var fsd=Fsd();alter(raw,fsd);Check.That(!Inspect(raw,fsd).CanRequest);}
}
[Test]public void ExplicitManualDamageHealthAndSessionEndRemainImmediateRejections()
{
foreach(Action<JsonObject> alter in new Action<JsonObject>[]
{r=>r["playerOverride"]=true,r=>r["shipHealth"]!["emergency"]=true,r=>r["shipHealth"]!["damageObserved"]=true,r=>r["shipHealth"]!["hullFraction"]=0,r=>r["inGame"]=false})
{var raw=Raw();alter(raw);var result=Inspect(raw,Fsd());Check.That(result.Reject&&!result.CanRequest);}
}
[Test]public void MissingSafetyOrOwnershipFieldsNeverDefaultToFalse()
{
foreach(var field in new[]{"playerOverride","inGame","localPlayerShipVerified","shipHealth"})
{var raw=Raw();raw.Remove(field);Check.That(!Inspect(raw,Fsd()).CanRequest);}
var fsd=Fsd();fsd.Remove("ownedCharging");Check.That(!Inspect(Raw(),fsd).CanRequest);
foreach(var field in new[]{"emergency","damageObserved","sequence","observedAtUnixMilliseconds"})
{var raw=Raw();raw["shipHealth"]!.AsObject().Remove(field);Check.That(!Inspect(raw,Fsd()).CanRequest);}
}
[Test]public void RecentArrivalMustBelongToTheSameLoadedSessionAndCurrentSystemEvent()
{
var initial=Arrived;Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.Zero) is null);
var zero=initial with{CurrentShipId=0};Check.That(NativeTargetSelectionRecovery.ContextFailure(zero,zero,2,Now,TimeSpan.Zero) is null);
foreach(var altered in new[]{initial with{LoadGameAt=Now},initial with{Navigation=initial.Navigation with{CurrentSystemAt=Now.AddSeconds(-5)}},
initial with{Navigation=initial.Navigation with{JumpInProgress=true}},initial with{CommanderId=""}})
Check.That(NativeTargetSelectionRecovery.ContextFailure(altered,altered,2,Now,TimeSpan.Zero) is not null);
}
[Test]public void TenSecondDeadlineAndChangesToCargoRouteShipSessionOrSystemAreTerminalContextFailures()
{
var initial=Arrived;Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.FromMilliseconds(9999)) is null);
Check.Equal("arrival-target-selection-deadline",NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.FromSeconds(10))!);
foreach(var current in new[]{initial with{CurrentShipId=9},initial with{CommanderId="F2"},initial with{CargoCount=1199},initial with{GameMode="Open"},
initial with{LoadGameAt=Now},initial with{Docked=true},initial with{Navigation=initial.Navigation with{CurrentSystemAddress=3}},
initial with{Navigation=initial.Navigation with{TargetSystemAddress=3}},
initial with{Navigation=initial.Navigation with{Route=[new(3,"Other",new(1,0,0),"M")]}}})
Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,current,2,Now,TimeSpan.FromSeconds(1)) is not null);
}
[Test]public void FailureDiagnosticsAreBoundedAndKeepBothNativePhasesInsteadOfCopyingFullTelemetry()
{
var raw=Raw();raw["unrelatedLargePayload"]=new string('x',100000);raw["rawFsdState"]=4;
var result=Inspect(raw,Fsd());Check.That(!result.CanRequest&&result.Diagnostics.GetRawText().Length<2048);
Check.Equal(4L,result.Diagnostics.GetProperty("rawState").GetInt64());Check.Equal(2L,result.Diagnostics.GetProperty("fsdState").GetInt64());
Check.Equal(0,result.Diagnostics.GetProperty("mutationsSent").GetInt32());
}
}
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeTargetSelectionRecoveryTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-27T07:06:03+08:00");
private static JsonObject Raw()=>new()
{
["sequence"]=7,["observedAtUnixMilliseconds"]=Now.ToUnixTimeMilliseconds(),["entityId"]="42",["referenceWorldAddress"]="1",
["localPlayerShipVerified"]=true,["inGame"]=true,["playerOverride"]=false,["rawFsdState"]=2,
["shipHealth"]=new JsonObject{["verified"]=true,["entityId"]="42",["sequence"]=7,["observedAtUnixMilliseconds"]=Now.ToUnixTimeMilliseconds(),
["hullFraction"]=1,["emergency"]=false,["damageObserved"]=false}
};
private static JsonObject Fsd()=>new(){["fresh"]=true,["entityId"]="42",["ownedCharging"]=false,["state"]=2,["targetValid"]=true,["targetAddress"]="2"};
private static NativeTargetSelectionReadiness Inspect(JsonObject raw,JsonObject fsd)=>NativeTargetSelectionRecovery.Inspect(JsonSerializer.SerializeToElement(raw),JsonSerializer.SerializeToElement(fsd),2,1,Now);
private static PlayerState Arrived=>new(){InGame=true,CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=Now.AddHours(-1),CargoCount=1200,
Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,CurrentSystemAt=Now.AddSeconds(-6),LastJumpAt=Now.AddSeconds(-6),Route=[new(1,"Here",new(0,0,0),"M"),new(2,"Next",new(1,0,0),"M")]}};
[Test]public void FreshIdleCanRequestButDoesNotItselfAcknowledgeADestination()
{
var check=Inspect(Raw(),Fsd());Check.That(check.CanRequest&&!check.Reject);
var raw=Raw();var fsd=Fsd();raw["rawFsdState"]=1;fsd["state"]=1;
Check.That(Inspect(raw,fsd).CanRequest);fsd["targetAddress"]="3";Check.That(!Inspect(raw,fsd).CanRequest);
}
[Test]public void OwnedChargingJumpingAndDifferentNativePhasesOnlyWaitWithoutRetargeting()
{
foreach(var (rawState,state,owned) in new[]{(3,3,false),(4,4,false),(2,2,true),(4,2,false),(2,4,false),(99,99,false)})
{
var raw=Raw();var fsd=Fsd();raw["rawFsdState"]=rawState;fsd["state"]=state;fsd["ownedCharging"]=owned;
var check=Inspect(raw,fsd);Check.That(!check.CanRequest&&!check.Reject);
}
}
[Test]public void SourceIdentityWorldFreshnessAndHealthMustAllMatchTheSameRawFrame()
{
foreach(Action<JsonObject,JsonObject> alter in new Action<JsonObject,JsonObject>[]
{
(r,f)=>f["entityId"]="43",(r,f)=>r["referenceWorldAddress"]="3",(r,f)=>f["fresh"]=false,
(r,f)=>r["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds(),
(r,f)=>r["shipHealth"]!["sequence"]=6,(r,f)=>r["shipHealth"]!["observedAtUnixMilliseconds"]=Now.AddMilliseconds(-1).ToUnixTimeMilliseconds(),
(r,f)=>r["shipHealth"]!["entityId"]="43",(r,f)=>r["localPlayerShipVerified"]=false
})
{var raw=Raw();var fsd=Fsd();alter(raw,fsd);Check.That(!Inspect(raw,fsd).CanRequest);}
}
[Test]public void ExplicitManualDamageHealthAndSessionEndRemainImmediateRejections()
{
foreach(Action<JsonObject> alter in new Action<JsonObject>[]
{r=>r["playerOverride"]=true,r=>r["shipHealth"]!["emergency"]=true,r=>r["shipHealth"]!["damageObserved"]=true,r=>r["shipHealth"]!["hullFraction"]=0,r=>r["inGame"]=false})
{var raw=Raw();alter(raw);var result=Inspect(raw,Fsd());Check.That(result.Reject&&!result.CanRequest);}
}
[Test]public void MissingSafetyOrOwnershipFieldsNeverDefaultToFalse()
{
foreach(var field in new[]{"playerOverride","inGame","localPlayerShipVerified","shipHealth"})
{var raw=Raw();raw.Remove(field);Check.That(!Inspect(raw,Fsd()).CanRequest);}
var fsd=Fsd();fsd.Remove("ownedCharging");Check.That(!Inspect(Raw(),fsd).CanRequest);
foreach(var field in new[]{"emergency","damageObserved","sequence","observedAtUnixMilliseconds"})
{var raw=Raw();raw["shipHealth"]!.AsObject().Remove(field);Check.That(!Inspect(raw,Fsd()).CanRequest);}
}
[Test]public void RecentArrivalMustBelongToTheSameLoadedSessionAndCurrentSystemEvent()
{
var initial=Arrived;Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.Zero) is null);
var zero=initial with{CurrentShipId=0};Check.That(NativeTargetSelectionRecovery.ContextFailure(zero,zero,2,Now,TimeSpan.Zero) is null);
foreach(var altered in new[]{initial with{LoadGameAt=Now},initial with{Navigation=initial.Navigation with{CurrentSystemAt=Now.AddSeconds(-5)}},
initial with{Navigation=initial.Navigation with{JumpInProgress=true}},initial with{CommanderId=""}})
Check.That(NativeTargetSelectionRecovery.ContextFailure(altered,altered,2,Now,TimeSpan.Zero) is not null);
}
[Test]public void TenSecondDeadlineAndChangesToCargoRouteShipSessionOrSystemAreTerminalContextFailures()
{
var initial=Arrived;Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.FromMilliseconds(9999)) is null);
Check.Equal("arrival-target-selection-deadline",NativeTargetSelectionRecovery.ContextFailure(initial,initial,2,Now,TimeSpan.FromSeconds(10))!);
foreach(var current in new[]{initial with{CurrentShipId=9},initial with{CommanderId="F2"},initial with{CargoCount=1199},initial with{GameMode="Open"},
initial with{LoadGameAt=Now},initial with{Docked=true},initial with{Navigation=initial.Navigation with{CurrentSystemAddress=3}},
initial with{Navigation=initial.Navigation with{TargetSystemAddress=3}},
initial with{Navigation=initial.Navigation with{Route=[new(3,"Other",new(1,0,0),"M")]}}})
Check.That(NativeTargetSelectionRecovery.ContextFailure(initial,current,2,Now,TimeSpan.FromSeconds(1)) is not null);
}
[Test]public void FailureDiagnosticsAreBoundedAndKeepBothNativePhasesInsteadOfCopyingFullTelemetry()
{
var raw=Raw();raw["unrelatedLargePayload"]=new string('x',100000);raw["rawFsdState"]=4;
var result=Inspect(raw,Fsd());Check.That(!result.CanRequest&&result.Diagnostics.GetRawText().Length<2048);
Check.Equal(4L,result.Diagnostics.GetProperty("rawState").GetInt64());Check.Equal(2L,result.Diagnostics.GetProperty("fsdState").GetInt64());
Check.Equal(0,result.Diagnostics.GetProperty("mutationsSent").GetInt32());
}
}