using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NavigationSafetyTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-22T01:00:00Z");
private static readonly StarEnvelope Star=new(1,"G",Vector3d.Zero,100,null,null);
private static readonly PlayerState Hot=new(){InGame=true,InDanger=true,Status=new((1L<<22)|(1L<<20)|16,0,20,1,0,null,null,null,null,[])};
private static readonly FlightFrame Frame=new(){ObservedAt=Now,GeometryVerified=true,AdaptiveFlightControl=true,NativeHeatWarning=true,ThermalDangerExplained=true,HeatPercent=98,
CurrentStar=Star,Position=new(108,0,0),Forward=new(0,1,0),Up=new(0,0,1),TargetPosition=new(10000,0,0),Sequence=1,
SystemAddress=1,TargetSystemAddress=2,JumpCommitInterlockVerified=true,FuelMain=20,FuelRequiredForJump=3,FuelNeededToNextRefuel=4,Fsd=FsdPhase.Charging};
[Test] public void ConfirmedThermalWarningWidensTheNearbyOrbitAndNeverCharges()
{
Check.That(!NavigationSafety.MustStop(Hot,Frame,Now));
var decision=new FlightNavigator().Decide(Frame,new(2,"Next",new(1,0,0),"G"),null,Now);
Check.That(decision.CancelCharging&&!decision.BeginCharging&&!decision.PermitJump);
Check.That(Vector3d.Dot(Frame.Position,decision.Direction!.Value)>0,"A hot ship must move slightly outward while retaining its orbit.");
Check.Equal(0d,decision.Throttle);
var outward=Frame with{Sequence=2,ObservedAt=Now.AddMilliseconds(100),HeatPercent=98.1,Forward=decision.Direction.Value};
var moving=new FlightNavigator().Decide(outward,new(2,"Next",new(1,0,0),"G"),null,outward.ObservedAt);
Check.That(moving.Throttle>0&&Vector3d.Dot(Frame.Position,moving.Direction!.Value)>0,"Cooling thrust is allowed only after the actual nose points outward.");
}
[Test] public void GenericDangerAloneCannotBePromotedToAnAttackWitness()
{
Check.That(!NavigationSafety.MustStop(Hot,null,Now));
Check.That(!NavigationSafety.MustStop(Hot,Frame with{ThermalDangerExplained=false},Now));
Check.That(!NavigationSafety.HasExternalThreat(new(){InDanger=true}));
}
[Test] public void CoolingKeepsTheConfirmedThermalEpisodeUntilTheGameClearsItsDangerMarker()
{
const long danger=1L<<22;var tracker=new ThermalDangerTracker();
Check.That(!tracker.Observe(danger,false,false,true),"A cold, unexplained danger cannot start an exception.");
Check.That(!tracker.Observe(danger,false,true,false),"Initial thermal evidence must be near a star.");
Check.That(tracker.Observe(danger,false,true,true));
for(var i=0;i<100;i++)Check.That(tracker.Observe(danger,false,false,false),"Cooling must not discard a confirmed cause while the same marker persists.");
Check.That(!NavigationSafety.MustStop(Hot,Frame with{NativeHeatWarning=false,HeatPercent=60,Position=new(400,0,0)},Now));
Check.That(!tracker.Observe(0,false,false,false));
Check.That(!tracker.Observe(danger,false,false,true),"A later unexplained danger is a new episode.");
Check.That(tracker.Observe(danger,false,true,true));
Check.That(!tracker.Observe(danger,true,true,true));
Check.That(!tracker.Observe(danger,false,false,true));
}
[Test] public void AttackAndInterdictionOverrideTheThermalWarningException()
{
Check.That(NavigationSafety.MustStop(Hot with{Navigation=new(){ExternalThreat=true}},Frame,Now));
Check.That(NavigationSafety.MustStop(Hot with{Status=Hot.Status! with{Flags=(1L<<23)|(1L<<20)}},Frame,Now));
Check.That(!NavigationSafety.MustStop(new(){InDanger=true},Frame,Now));
}
[Test] public void AttackLatchCannotBeClearedByAnUnchangedOrOlderStatusTimestamp()
{
var p=new NavigationProjector();
void Event(string name,int seconds,long flags=0)=>p.Apply(Check.Event(new{timestamp=Now.AddSeconds(seconds),@event=name,Flags=flags}));
Event("UnderAttack",0);Event("Status",0);Check.That(p.State.ExternalThreat);
Event("Status",-1);Check.That(p.State.ExternalThreat);
Event("Status",1,1L<<22);Check.That(p.State.ExternalThreat);
Event("Status",2);Check.That(!p.State.ExternalThreat);
Event("Interdicted",3);Event("LoadGame",4);Check.That(!p.State.ExternalThreat);
}
}
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NavigationSafetyTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-22T01:00:00Z");
private static readonly StarEnvelope Star=new(1,"G",Vector3d.Zero,100,null,null);
private static readonly PlayerState Hot=new(){InGame=true,InDanger=true,Status=new((1L<<22)|(1L<<20)|16,0,20,1,0,null,null,null,null,[])};
private static readonly FlightFrame Frame=new(){ObservedAt=Now,GeometryVerified=true,AdaptiveFlightControl=true,NativeHeatWarning=true,ThermalDangerExplained=true,HeatPercent=98,
CurrentStar=Star,Position=new(108,0,0),Forward=new(0,1,0),Up=new(0,0,1),TargetPosition=new(10000,0,0),Sequence=1,
SystemAddress=1,TargetSystemAddress=2,JumpCommitInterlockVerified=true,FuelMain=20,FuelRequiredForJump=3,FuelNeededToNextRefuel=4,Fsd=FsdPhase.Charging};
[Test] public void ConfirmedThermalWarningWidensTheNearbyOrbitAndNeverCharges()
{
Check.That(!NavigationSafety.MustStop(Hot,Frame,Now));
var decision=new FlightNavigator().Decide(Frame,new(2,"Next",new(1,0,0),"G"),null,Now);
Check.That(decision.CancelCharging&&!decision.BeginCharging&&!decision.PermitJump);
Check.That(Vector3d.Dot(Frame.Position,decision.Direction!.Value)>0,"A hot ship must move slightly outward while retaining its orbit.");
Check.Equal(0d,decision.Throttle);
var outward=Frame with{Sequence=2,ObservedAt=Now.AddMilliseconds(100),HeatPercent=98.1,Forward=decision.Direction.Value};
var moving=new FlightNavigator().Decide(outward,new(2,"Next",new(1,0,0),"G"),null,outward.ObservedAt);
Check.That(moving.Throttle>0&&Vector3d.Dot(Frame.Position,moving.Direction!.Value)>0,"Cooling thrust is allowed only after the actual nose points outward.");
}
[Test] public void GenericDangerAloneCannotBePromotedToAnAttackWitness()
{
Check.That(!NavigationSafety.MustStop(Hot,null,Now));
Check.That(!NavigationSafety.MustStop(Hot,Frame with{ThermalDangerExplained=false},Now));
Check.That(!NavigationSafety.HasExternalThreat(new(){InDanger=true}));
}
[Test] public void CoolingKeepsTheConfirmedThermalEpisodeUntilTheGameClearsItsDangerMarker()
{
const long danger=1L<<22;var tracker=new ThermalDangerTracker();
Check.That(!tracker.Observe(danger,false,false,true),"A cold, unexplained danger cannot start an exception.");
Check.That(!tracker.Observe(danger,false,true,false),"Initial thermal evidence must be near a star.");
Check.That(tracker.Observe(danger,false,true,true));
for(var i=0;i<100;i++)Check.That(tracker.Observe(danger,false,false,false),"Cooling must not discard a confirmed cause while the same marker persists.");
Check.That(!NavigationSafety.MustStop(Hot,Frame with{NativeHeatWarning=false,HeatPercent=60,Position=new(400,0,0)},Now));
Check.That(!tracker.Observe(0,false,false,false));
Check.That(!tracker.Observe(danger,false,false,true),"A later unexplained danger is a new episode.");
Check.That(tracker.Observe(danger,false,true,true));
Check.That(!tracker.Observe(danger,true,true,true));
Check.That(!tracker.Observe(danger,false,false,true));
}
[Test] public void AttackAndInterdictionOverrideTheThermalWarningException()
{
Check.That(NavigationSafety.MustStop(Hot with{Navigation=new(){ExternalThreat=true}},Frame,Now));
Check.That(NavigationSafety.MustStop(Hot with{Status=Hot.Status! with{Flags=(1L<<23)|(1L<<20)}},Frame,Now));
Check.That(!NavigationSafety.MustStop(new(){InDanger=true},Frame,Now));
}
[Test] public void AttackLatchCannotBeClearedByAnUnchangedOrOlderStatusTimestamp()
{
var p=new NavigationProjector();
void Event(string name,int seconds,long flags=0)=>p.Apply(Check.Event(new{timestamp=Now.AddSeconds(seconds),@event=name,Flags=flags}));
Event("UnderAttack",0);Event("Status",0);Check.That(p.State.ExternalThreat);
Event("Status",-1);Check.That(p.State.ExternalThreat);
Event("Status",1,1L<<22);Check.That(p.State.ExternalThreat);
Event("Status",2);Check.That(!p.State.ExternalThreat);
Event("Interdicted",3);Event("LoadGame",4);Check.That(!p.State.ExternalThreat);
}
}