using System.Text.Json;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class SystemOrbitSourceTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-25T10:00:00Z");
private static SystemLocation Body(ulong id,string name,SystemLocationCategory category,string? identity=null)
=>new((int)id,id,name,"",0,category,null,new((double)id,0,0),null,false,identity??id.ToString());
private static readonly SystemLocation Star=Body(101,"Fixture A",SystemLocationCategory.Star);
private static readonly SystemLocation Planet=Body(102,"Fixture A 1",SystemLocationCategory.Planet);
private static CurrentSystemSnapshot Snapshot(params SystemLocation[] bodies)=>new(7,"Fixture",42,1,At,bodies);
private static JsonElement Scan(int body,string name,int parent,bool star=false,long system=7,DateTimeOffset? at=null,double a=1e9,double e=.2)
=>JsonSerializer.SerializeToElement(new{ @event="Scan",timestamp=at??At,SystemAddress=system,BodyID=body,BodyName=name,
StarType=star?"G":"",Parents=new[]{new Dictionary<string,int>{{"Star",parent}}},SemiMajorAxis=a,Eccentricity=e,
OrbitalInclination=20.0,Periapsis=30.0,AscendingNode=40.0,OrbitalPeriod=86400.0,MeanAnomaly=50.0 });
[Test]public void RealJournalElementsBindBySystemAndUniqueOriginalBodyNameWithoutInventingNativeAxes()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(1,Star.Name,0,star:true));source.Apply(Scan(2,Planet.Name,1));
var result=source.Bind(Snapshot(Star,Planet)).Single(orbit=>orbit.ChildIdentity==Planet.Identity);
Check.Equal(Planet.WorldAddress,result.ChildWorldAddress);Check.Equal(Star.WorldAddress,result.ParentWorldAddress);
Check.Equal(2,result.JournalBodyId);Check.Equal(1e9,result.SemiMajorAxisMetres);Check.Equal(.2,result.Eccentricity);
Check.Equal(At,result.Epoch);Check.Equal(86400d,result.PeriodSeconds);Check.Equal(50d,result.MeanAnomalyDegrees);
Check.Equal(SystemOrbitQuality.ElementsOnly,result.Quality);Check.That(!result.Renderable);
Check.That(result.PeriapsisDirection is null&&result.TransverseDirection is null);
}
[Test]public void CrossSystemOrAmbiguousCatalogueNamesCannotAcquireOrbitIdentity()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,1,system:8));
Check.Equal(SystemOrbitQuality.Unavailable,source.Bind(Snapshot(Star,Planet)).Last().Quality);
source.Apply(Scan(2,Planet.Name,1));
var duplicate=Body(103,Planet.Name,SystemLocationCategory.Planet);
Check.That(source.Bind(Snapshot(Star,Planet,duplicate)).All(orbit=>orbit.JournalBodyId is null&&!orbit.Renderable));
Check.That(source.Bind(Snapshot(Body(104,Planet.Name,SystemLocationCategory.Star))).Single().JournalBodyId is null);
}
[Test]public void IncompleteParentAndInvalidShapeStayExplicitlyUnavailable()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,99));
var unresolved=source.Bind(Snapshot(Planet)).Single();Check.That(unresolved.ParentWorldAddress is null&&!unresolved.Renderable);
Check.That(unresolved.UnavailableReason.Contains("父天体"));
source.Apply(Scan(2,Planet.Name,99,at:At.AddSeconds(1),e:1));
Check.Equal(SystemOrbitQuality.Unavailable,source.Bind(Snapshot(Planet)).Single().Quality);
source.Apply(Scan(2,Planet.Name,99,at:At.AddSeconds(2),a:0));
Check.That(!source.Bind(Snapshot(Planet)).Single().HasEllipticElements);
}
[Test]public void OlderJournalReplayCannotReplaceNewerOrbitEpochAndSignalsGetNoGuessedCircles()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,1,at:At.AddSeconds(5),a:2e9));
source.Apply(Scan(2,Planet.Name,1));
var signal=Body(103,"Signal",SystemLocationCategory.Signal);
var result=source.Bind(Snapshot(Planet,signal));Check.Equal(1,result.Count);Check.Equal(2e9,result[0].SemiMajorAxisMetres);
Check.Equal(At.AddSeconds(5),result[0].Epoch);
}
[Test]public void NativeQualityAloneCannotRenderUnknownShapeOrNonOrthonormalAxes()
{
var valid=new SystemOrbit{SystemAddress=7,ChildWorldAddress=102,ChildIdentity="child",ParentWorldAddress=101,ParentIdentity="parent",
SemiMajorAxisMetres=1e9,Eccentricity=.2,PeriapsisDirection=new(1,0,0),TransverseDirection=new(0,1,0),Quality=SystemOrbitQuality.NativeVerified};
Check.That(valid.Renderable);
foreach(var invalid in new[]{valid with{Quality=SystemOrbitQuality.ElementsOnly},valid with{SemiMajorAxisMetres=double.PositiveInfinity},
valid with{Eccentricity=1},valid with{PeriapsisDirection=new(2,0,0)},valid with{TransverseDirection=new(1,0,0)},
valid with{ParentWorldAddress=102},valid with{ParentIdentity=null}})
Check.That(!invalid.Renderable);
}
[Test]public void MalformedParentShapeIsIgnoredInsteadOfCrashingJournalReplay()
{
var source=new JournalSystemOrbitCatalogue();
source.Apply(JsonSerializer.SerializeToElement(new{@event="Scan",timestamp=At,SystemAddress=7,BodyID=2,BodyName=Planet.Name,Parents=new[]{new{Star="invalid"}}}));
Check.That(source.Bind(Snapshot(Planet)).Single().JournalBodyId is null);
}
[Test]public void OriginalAdLeonisScanKeepsObservedEpochUnitsAndAnUnlocatedBarycentreExplicit()
{
using var doc=JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory,"Fixtures","ad-leonis-orbit-scans.json")));
var source=new JournalSystemOrbitCatalogue();foreach(var scan in doc.RootElement.EnumerateArray())source.Apply(scan);
var star=Body(101,"AD Leonis A",SystemLocationCategory.Star);
var planet=Body(102,"AD Leonis A 4",SystemLocationCategory.Planet);
var snapshot=new CurrentSystemSnapshot(5368172661576,"AD Leonis",42,1,At,[star,planet]);
var result=source.Bind(snapshot);var stellar=result.Single(orbit=>orbit.ChildWorldAddress==101);
Check.Equal(8019258618354.797,stellar.SemiMajorAxisMetres);Check.Equal(.169544,stellar.Eccentricity);
Check.That(stellar.ParentWorldAddress is null&&stellar.JournalParents[0].Kind=="Null");
var planetary=result.Single(orbit=>orbit.ChildWorldAddress==102);
Check.Equal(star.WorldAddress,planetary.ParentWorldAddress);Check.Equal(298840373754.50134,planetary.SemiMajorAxisMetres);
Check.Equal(199594521.522522,planetary.PeriodSeconds);Check.Equal(-8.789576,planetary.AscendingNodeDegrees);
Check.Equal(DateTimeOffset.Parse("2026-10-24T03:57:00Z"),planetary.Epoch);
Check.That(result.All(orbit=>orbit.Quality==SystemOrbitQuality.ElementsOnly&&!orbit.Renderable));
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class SystemOrbitSourceTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-25T10:00:00Z");
private static SystemLocation Body(ulong id,string name,SystemLocationCategory category,string? identity=null)
=>new((int)id,id,name,"",0,category,null,new((double)id,0,0),null,false,identity??id.ToString());
private static readonly SystemLocation Star=Body(101,"Fixture A",SystemLocationCategory.Star);
private static readonly SystemLocation Planet=Body(102,"Fixture A 1",SystemLocationCategory.Planet);
private static CurrentSystemSnapshot Snapshot(params SystemLocation[] bodies)=>new(7,"Fixture",42,1,At,bodies);
private static JsonElement Scan(int body,string name,int parent,bool star=false,long system=7,DateTimeOffset? at=null,double a=1e9,double e=.2)
=>JsonSerializer.SerializeToElement(new{ @event="Scan",timestamp=at??At,SystemAddress=system,BodyID=body,BodyName=name,
StarType=star?"G":"",Parents=new[]{new Dictionary<string,int>{{"Star",parent}}},SemiMajorAxis=a,Eccentricity=e,
OrbitalInclination=20.0,Periapsis=30.0,AscendingNode=40.0,OrbitalPeriod=86400.0,MeanAnomaly=50.0 });
[Test]public void RealJournalElementsBindBySystemAndUniqueOriginalBodyNameWithoutInventingNativeAxes()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(1,Star.Name,0,star:true));source.Apply(Scan(2,Planet.Name,1));
var result=source.Bind(Snapshot(Star,Planet)).Single(orbit=>orbit.ChildIdentity==Planet.Identity);
Check.Equal(Planet.WorldAddress,result.ChildWorldAddress);Check.Equal(Star.WorldAddress,result.ParentWorldAddress);
Check.Equal(2,result.JournalBodyId);Check.Equal(1e9,result.SemiMajorAxisMetres);Check.Equal(.2,result.Eccentricity);
Check.Equal(At,result.Epoch);Check.Equal(86400d,result.PeriodSeconds);Check.Equal(50d,result.MeanAnomalyDegrees);
Check.Equal(SystemOrbitQuality.ElementsOnly,result.Quality);Check.That(!result.Renderable);
Check.That(result.PeriapsisDirection is null&&result.TransverseDirection is null);
}
[Test]public void CrossSystemOrAmbiguousCatalogueNamesCannotAcquireOrbitIdentity()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,1,system:8));
Check.Equal(SystemOrbitQuality.Unavailable,source.Bind(Snapshot(Star,Planet)).Last().Quality);
source.Apply(Scan(2,Planet.Name,1));
var duplicate=Body(103,Planet.Name,SystemLocationCategory.Planet);
Check.That(source.Bind(Snapshot(Star,Planet,duplicate)).All(orbit=>orbit.JournalBodyId is null&&!orbit.Renderable));
Check.That(source.Bind(Snapshot(Body(104,Planet.Name,SystemLocationCategory.Star))).Single().JournalBodyId is null);
}
[Test]public void IncompleteParentAndInvalidShapeStayExplicitlyUnavailable()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,99));
var unresolved=source.Bind(Snapshot(Planet)).Single();Check.That(unresolved.ParentWorldAddress is null&&!unresolved.Renderable);
Check.That(unresolved.UnavailableReason.Contains("父天体"));
source.Apply(Scan(2,Planet.Name,99,at:At.AddSeconds(1),e:1));
Check.Equal(SystemOrbitQuality.Unavailable,source.Bind(Snapshot(Planet)).Single().Quality);
source.Apply(Scan(2,Planet.Name,99,at:At.AddSeconds(2),a:0));
Check.That(!source.Bind(Snapshot(Planet)).Single().HasEllipticElements);
}
[Test]public void OlderJournalReplayCannotReplaceNewerOrbitEpochAndSignalsGetNoGuessedCircles()
{
var source=new JournalSystemOrbitCatalogue();source.Apply(Scan(2,Planet.Name,1,at:At.AddSeconds(5),a:2e9));
source.Apply(Scan(2,Planet.Name,1));
var signal=Body(103,"Signal",SystemLocationCategory.Signal);
var result=source.Bind(Snapshot(Planet,signal));Check.Equal(1,result.Count);Check.Equal(2e9,result[0].SemiMajorAxisMetres);
Check.Equal(At.AddSeconds(5),result[0].Epoch);
}
[Test]public void NativeQualityAloneCannotRenderUnknownShapeOrNonOrthonormalAxes()
{
var valid=new SystemOrbit{SystemAddress=7,ChildWorldAddress=102,ChildIdentity="child",ParentWorldAddress=101,ParentIdentity="parent",
SemiMajorAxisMetres=1e9,Eccentricity=.2,PeriapsisDirection=new(1,0,0),TransverseDirection=new(0,1,0),Quality=SystemOrbitQuality.NativeVerified};
Check.That(valid.Renderable);
foreach(var invalid in new[]{valid with{Quality=SystemOrbitQuality.ElementsOnly},valid with{SemiMajorAxisMetres=double.PositiveInfinity},
valid with{Eccentricity=1},valid with{PeriapsisDirection=new(2,0,0)},valid with{TransverseDirection=new(1,0,0)},
valid with{ParentWorldAddress=102},valid with{ParentIdentity=null}})
Check.That(!invalid.Renderable);
}
[Test]public void MalformedParentShapeIsIgnoredInsteadOfCrashingJournalReplay()
{
var source=new JournalSystemOrbitCatalogue();
source.Apply(JsonSerializer.SerializeToElement(new{@event="Scan",timestamp=At,SystemAddress=7,BodyID=2,BodyName=Planet.Name,Parents=new[]{new{Star="invalid"}}}));
Check.That(source.Bind(Snapshot(Planet)).Single().JournalBodyId is null);
}
[Test]public void OriginalAdLeonisScanKeepsObservedEpochUnitsAndAnUnlocatedBarycentreExplicit()
{
using var doc=JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory,"Fixtures","ad-leonis-orbit-scans.json")));
var source=new JournalSystemOrbitCatalogue();foreach(var scan in doc.RootElement.EnumerateArray())source.Apply(scan);
var star=Body(101,"AD Leonis A",SystemLocationCategory.Star);
var planet=Body(102,"AD Leonis A 4",SystemLocationCategory.Planet);
var snapshot=new CurrentSystemSnapshot(5368172661576,"AD Leonis",42,1,At,[star,planet]);
var result=source.Bind(snapshot);var stellar=result.Single(orbit=>orbit.ChildWorldAddress==101);
Check.Equal(8019258618354.797,stellar.SemiMajorAxisMetres);Check.Equal(.169544,stellar.Eccentricity);
Check.That(stellar.ParentWorldAddress is null&&stellar.JournalParents[0].Kind=="Null");
var planetary=result.Single(orbit=>orbit.ChildWorldAddress==102);
Check.Equal(star.WorldAddress,planetary.ParentWorldAddress);Check.Equal(298840373754.50134,planetary.SemiMajorAxisMetres);
Check.Equal(199594521.522522,planetary.PeriodSeconds);Check.Equal(-8.789576,planetary.AscendingNodeDegrees);
Check.Equal(DateTimeOffset.Parse("2026-10-24T03:57:00Z"),planetary.Epoch);
Check.That(result.All(orbit=>orbit.Quality==SystemOrbitQuality.ElementsOnly&&!orbit.Renderable));
}
}