using System.Xml.Linq;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Notifications;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NavigationArrivalTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-23T04:00:00Z");
private static PlayerState Player()=>new(){InGame=true,CommanderId="commander",LoadGameAt=At,SystemName="Sol",Navigation=new(){CurrentSystemAddress=42}};
private static NavigationProgress Progress(NavigationRunState state,string destination="Sol")=>new(state,destination,"已抵达","");
[Test] public void OnlyConfirmedCompletionNotifiesOnceAndAnotherRunCanNotifyAgain()
{
var observer=new NavigationArrivalObserver();var player=Player();
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
Check.That(observer.Observe(Progress(NavigationRunState.Running),player,At) is null);
var arrival=observer.Observe(Progress(NavigationRunState.Completed),player,At)!;
Check.Equal(42L,arrival.SystemAddress);Check.Equal("Sol",arrival.Destination);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
observer.Observe(Progress(NavigationRunState.Running),player,At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is not null);
}
[Test] public void StopsErrorsWrongSystemsAndUnconfirmedJumpsDoNotNotify()
{
foreach(var state in new[]{NavigationRunState.Stopped,NavigationRunState.Failed,NavigationRunState.Idle})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(state),Player(),At) is null);
}
foreach(var player in new[]{Player() with{InGame=false},Player() with{SystemName="Achenar"},Player() with{Navigation=new(){CurrentSystemAddress=42,JumpInProgress=true}}})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
}
}
[Test] public void StationArrivalRequiresTheConfirmedRequestedDockingPort()
{
var target=new SystemNavigationTarget(42,99,"Galileo",SystemTargetKind.Station,500);
foreach(var player in new[]{Player(),Player() with{Docked=true,MarketId=600}})
{
var observer=new NavigationArrivalObserver();observer.Observe(new SystemNavigationProgress(NavigationRunState.Running,target,SystemNavigationPhase.NormalApproach,""),player,At);
Check.That(observer.Observe(new SystemNavigationProgress(NavigationRunState.Completed,target,SystemNavigationPhase.Completed,""),player,At) is null);
}
var docked=Player() with{Docked=true,MarketId=500};var success=new NavigationArrivalObserver();
success.Observe(new SystemNavigationProgress(NavigationRunState.Running,target,SystemNavigationPhase.NativeLanding,""),docked,At);
var arrival=success.Observe(new SystemNavigationProgress(NavigationRunState.Completed,target,SystemNavigationPhase.Completed,""),docked,At)!;
Check.Equal("Galileo",arrival.Destination);Check.Equal("current-system",arrival.Page);Check.That(arrival.Docked);
}
[Test] public void ScreenshotSceneIsBoundToTheCommanderSessionAndSystem()
{
var player=Player();var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),player,At);
var arrival=observer.Observe(Progress(NavigationRunState.Completed),player,At)!;
Check.That(arrival.SameScene(player));Check.That(!arrival.SameScene(player with{CommanderId="other"}));
Check.That(!arrival.SameScene(player with{LoadGameAt=At.AddHours(1)}));
Check.That(!arrival.SameScene(player with{Navigation=new(){CurrentSystemAddress=99}}));
}
[Test] public void ACompletionFromAnOlderCommanderOrSessionCannotNotifyTheNewSession()
{
foreach(var changed in new[]{Player() with{CommanderId="other"},Player() with{LoadGameAt=At.AddHours(1)}})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),changed,At) is null);
}
}
[Test] public void NativeToastEscapesTextAndUsesTheLocalFullWidthHeroAndClickArguments()
{
var arrival=new NavigationArrival("id",42,"A & B","<Galileo>",false,false,"",At,At);
var image=Path.GetFullPath("arrival with space & name.png");var xml=ArrivalToastContent.Build(arrival,image);var toast=XElement.Parse(xml);
Check.Equal("已抵达 <Galileo>",toast.Descendants("text").First().Value);
Check.Equal("hero",toast.Descendants("image").Single().Attribute("placement")!.Value);
Check.Equal(new Uri(image).AbsoluteUri,toast.Descendants("image").Single().Attribute("src")!.Value);
Check.Equal("action=arrival&page=navigation&system=42",toast.Attribute("launch")!.Value);
Check.That(!XElement.Parse(ArrivalToastContent.Build(arrival)).Descendants("image").Any());
}
[Test] public void BrightWarmAndBlueStellarDiscsAreRecognized()
{
foreach(var colour in new[]{(R:(byte)255,G:(byte)130,B:(byte)30),(R:(byte)150,G:(byte)185,B:(byte)255)})
{
var pixels=Frame((x,y)=>(x-160)*(x-160)+(y-55)*(y-55)<18*18?colour:((byte)2,(byte)3,(byte)8));
Check.That(StarFrameCandidate.HasStar(320,180,pixels));
}
}
[Test] public void BlankTransitionAndBottomHudFramesCannotBeUsedAsStarPictures()
{
foreach(var pixels in new[]{Frame((_,_)=>(0,0,0)),Frame((_,_)=>(255,255,255)),Frame((x,y)=>y>160&&x is >40 and <280?((byte)255,(byte)180,(byte)20):((byte)2,(byte)3,(byte)8))})
Check.That(!StarFrameCandidate.HasStar(320,180,pixels));
}
[Test] public void InvalidScreenshotDimensionsAreRejected()
{
Check.That(!StarFrameCandidate.HasStar(1,1,new byte[4]));Check.That(!StarFrameCandidate.HasStar(320,180,new byte[8]));
}
private static byte[] Frame(Func<int,int,(byte R,byte G,byte B)> pixel)
{
var pixels=new byte[320*180*4];
for(var y=0;y<180;y++)for(var x=0;x<320;x++){var (r,g,b)=pixel(x,y);var p=(y*320+x)*4;pixels[p]=b;pixels[p+1]=g;pixels[p+2]=r;pixels[p+3]=255;}
return pixels;
}
}
using System.Xml.Linq;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Notifications;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NavigationArrivalTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-23T04:00:00Z");
private static PlayerState Player()=>new(){InGame=true,CommanderId="commander",LoadGameAt=At,SystemName="Sol",Navigation=new(){CurrentSystemAddress=42}};
private static NavigationProgress Progress(NavigationRunState state,string destination="Sol")=>new(state,destination,"已抵达","");
[Test] public void OnlyConfirmedCompletionNotifiesOnceAndAnotherRunCanNotifyAgain()
{
var observer=new NavigationArrivalObserver();var player=Player();
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
Check.That(observer.Observe(Progress(NavigationRunState.Running),player,At) is null);
var arrival=observer.Observe(Progress(NavigationRunState.Completed),player,At)!;
Check.Equal(42L,arrival.SystemAddress);Check.Equal("Sol",arrival.Destination);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
observer.Observe(Progress(NavigationRunState.Running),player,At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is not null);
}
[Test] public void StopsErrorsWrongSystemsAndUnconfirmedJumpsDoNotNotify()
{
foreach(var state in new[]{NavigationRunState.Stopped,NavigationRunState.Failed,NavigationRunState.Idle})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(state),Player(),At) is null);
}
foreach(var player in new[]{Player() with{InGame=false},Player() with{SystemName="Achenar"},Player() with{Navigation=new(){CurrentSystemAddress=42,JumpInProgress=true}}})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),player,At) is null);
}
}
[Test] public void StationArrivalRequiresTheConfirmedRequestedDockingPort()
{
var target=new SystemNavigationTarget(42,99,"Galileo",SystemTargetKind.Station,500);
foreach(var player in new[]{Player(),Player() with{Docked=true,MarketId=600}})
{
var observer=new NavigationArrivalObserver();observer.Observe(new SystemNavigationProgress(NavigationRunState.Running,target,SystemNavigationPhase.NormalApproach,""),player,At);
Check.That(observer.Observe(new SystemNavigationProgress(NavigationRunState.Completed,target,SystemNavigationPhase.Completed,""),player,At) is null);
}
var docked=Player() with{Docked=true,MarketId=500};var success=new NavigationArrivalObserver();
success.Observe(new SystemNavigationProgress(NavigationRunState.Running,target,SystemNavigationPhase.NativeLanding,""),docked,At);
var arrival=success.Observe(new SystemNavigationProgress(NavigationRunState.Completed,target,SystemNavigationPhase.Completed,""),docked,At)!;
Check.Equal("Galileo",arrival.Destination);Check.Equal("current-system",arrival.Page);Check.That(arrival.Docked);
}
[Test] public void ScreenshotSceneIsBoundToTheCommanderSessionAndSystem()
{
var player=Player();var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),player,At);
var arrival=observer.Observe(Progress(NavigationRunState.Completed),player,At)!;
Check.That(arrival.SameScene(player));Check.That(!arrival.SameScene(player with{CommanderId="other"}));
Check.That(!arrival.SameScene(player with{LoadGameAt=At.AddHours(1)}));
Check.That(!arrival.SameScene(player with{Navigation=new(){CurrentSystemAddress=99}}));
}
[Test] public void ACompletionFromAnOlderCommanderOrSessionCannotNotifyTheNewSession()
{
foreach(var changed in new[]{Player() with{CommanderId="other"},Player() with{LoadGameAt=At.AddHours(1)}})
{
var observer=new NavigationArrivalObserver();observer.Observe(Progress(NavigationRunState.Running),Player(),At);
Check.That(observer.Observe(Progress(NavigationRunState.Completed),changed,At) is null);
}
}
[Test] public void NativeToastEscapesTextAndUsesTheLocalFullWidthHeroAndClickArguments()
{
var arrival=new NavigationArrival("id",42,"A & B","<Galileo>",false,false,"",At,At);
var image=Path.GetFullPath("arrival with space & name.png");var xml=ArrivalToastContent.Build(arrival,image);var toast=XElement.Parse(xml);
Check.Equal("已抵达 <Galileo>",toast.Descendants("text").First().Value);
Check.Equal("hero",toast.Descendants("image").Single().Attribute("placement")!.Value);
Check.Equal(new Uri(image).AbsoluteUri,toast.Descendants("image").Single().Attribute("src")!.Value);
Check.Equal("action=arrival&page=navigation&system=42",toast.Attribute("launch")!.Value);
Check.That(!XElement.Parse(ArrivalToastContent.Build(arrival)).Descendants("image").Any());
}
[Test] public void BrightWarmAndBlueStellarDiscsAreRecognized()
{
foreach(var colour in new[]{(R:(byte)255,G:(byte)130,B:(byte)30),(R:(byte)150,G:(byte)185,B:(byte)255)})
{
var pixels=Frame((x,y)=>(x-160)*(x-160)+(y-55)*(y-55)<18*18?colour:((byte)2,(byte)3,(byte)8));
Check.That(StarFrameCandidate.HasStar(320,180,pixels));
}
}
[Test] public void BlankTransitionAndBottomHudFramesCannotBeUsedAsStarPictures()
{
foreach(var pixels in new[]{Frame((_,_)=>(0,0,0)),Frame((_,_)=>(255,255,255)),Frame((x,y)=>y>160&&x is >40 and <280?((byte)255,(byte)180,(byte)20):((byte)2,(byte)3,(byte)8))})
Check.That(!StarFrameCandidate.HasStar(320,180,pixels));
}
[Test] public void InvalidScreenshotDimensionsAreRejected()
{
Check.That(!StarFrameCandidate.HasStar(1,1,new byte[4]));Check.That(!StarFrameCandidate.HasStar(320,180,new byte[8]));
}
private static byte[] Frame(Func<int,int,(byte R,byte G,byte B)> pixel)
{
var pixels=new byte[320*180*4];
for(var y=0;y<180;y++)for(var x=0;x<320;x++){var (r,g,b)=pixel(x,y);var p=(y*320+x)*4;pixels[p]=b;pixels[p+1]=g;pixels[p+2]=r;pixels[p+3]=255;}
return pixels;
}
}