using System.Net; using System.Text; using System.Text.Json; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GalaxyQueryServiceTests { [Test] public async Task EconomyQueriesBothActualFieldsWithinTheDisplayedVolumeAndReturnsMeasuredStars() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Economy,Values=new HashSet{"hightech"}}; var result=await query.QueryAsync(filter,new(0,0,0),30); Check.Equal(2,handler.Requests.Count(x=>x.Url.EndsWith("/systems/search"))); var first=handler.Requests.First(x=>x.Url.EndsWith("/systems/search")).Json; Check.Equal(30d,first.GetProperty("filters").GetProperty("distance").GetProperty("max").GetDouble()); Check.Equal("High Tech",first.GetProperty("filters").GetProperty("primary_economy").GetProperty("value")[0].GetString()); Check.That(handler.Requests.Any(x=>x.Json.ValueKind==JsonValueKind.Object&&x.Json.TryGetProperty("filters",out var f)&&f.TryGetProperty("secondary_economy",out _))); Check.Equal(1,result.Systems.Count);Check.Equal("Measured",result.Systems[0].Name);Check.Equal(new Coordinates(2,3,4),result.Systems[0].Position);Check.That(result.Systems[0].Data.Visited is null); Check.That(filter.Matches(result.Systems[0]));Check.That(!filter.Matches(new("Unknown",new(0,0,0)))); } [Test] public async Task ServiceAndCommodityQueriesUseVerifiedNestedStationSchemaAndRealSubtypeFacts() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var service=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Services,Values=new HashSet{"materialtraderraw"}}; var result=await query.QueryAsync(service,new(0,0,0),50); var request=handler.Requests.Last(x=>x.Url.EndsWith("/stations/search")).Json; Check.Equal("Raw",request.GetProperty("filters").GetProperty("material_trader").GetProperty("value")[0].GetString()); Check.That(result.Systems.Single().Data.Services!.Contains("materialtraderraw")); var commodity=service with{Category=GalaxyFilterCategory.Commodity,Values=new HashSet{"gold"}}; result=await query.QueryAsync(commodity,new(0,0,0),50); var market=handler.Requests.Last(x=>x.Url.EndsWith("/stations/search")).Json.GetProperty("filters").GetProperty("marketplace")[0]; Check.Equal("Gold",market.GetProperty("commodity").GetProperty("value")[0].GetString());Check.Equal(1L,market.GetProperty("supply").GetProperty("value")[0].GetInt64()); Check.That(result.Systems.Single().Data.Commodities!.Contains("Gold")); var general=service with{Values=new HashSet{"rearm"}};await query.QueryAsync(general,new(0,0,0),50); var nested=handler.Requests.Last(x=>x.Url.EndsWith("/systems/search")).Json.GetProperty("filters").GetProperty("stations")[0]; Check.Equal("Restock",nested.GetProperty("services").GetProperty("value")[0].GetString()); } [Test] public void RichCatalogueDataKeepsRealControlSeparateFromInfluenceAndDoesNotInventPlayerState() { var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Powerplay,Category=GalaxyFilterCategory.Power,Values=new HashSet{"actualcontroller"}}; var root=Check.Json("""{"results":[{"name":"Measured","id64":27,"x":1,"y":2,"z":3,"power":["Other influence"],"controlling_power":"Actual controller","power_state":"Stronghold","population":1000,"is_colonised":true,"power_state_reinforcement":15,"stations":[{"type":"Drake-Class Carrier","services":["Refuel","Restock"]}],"bodies":[{"is_main_star":true,"subtype":"G (White-Yellow) Star"},{"landmarks":[{"type":"Guardian Ruin"}]}]}]}"""); var s=GalaxyQueryService.Parse(root,"systems",filter,true,new(0,0,0),20).Single(); Check.Equal("Actual controller",s.Data.Power);Check.Equal("Stronghold",s.Data.PowerState);Check.Equal("colonised",s.Data.Colonisation);Check.That(s.Data.Civilizations!.Contains("guardian"));Check.That(s.Data.PowerActivities!.Contains("reinforcement")); Check.That(s.Data.FleetCarrier==true&&s.Data.Visited is null&&s.Data.Exploration is null&&s.Data.TradeData is null); Check.That(GalaxyQueryService.Parse(root,"systems",filter,true,new(1000,0,0),1).Count==0); } [Test] public async Task CommodityImportDemandIsIndependentOfExportSupplyAndOldStationMarketsCannotRestoreClearedSupply() { using var http=new HttpClient(new Api());var service=new GalaxyQueryService(http); var exports=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Commodity,Values=new HashSet{"gold"}}; var imports=exports with{CommodityDirection=GalaxyCommodityDirection.Imports};var plan=(await service.PlansAsync(imports)).Single(); var payload=JsonSerializer.SerializeToElement(plan.Filters).GetProperty("marketplace")[0]; Check.Equal(1L,payload.GetProperty("demand").GetProperty("value")[0].GetInt64());Check.That(!payload.TryGetProperty("supply",out _));Check.That(exports.QueryKey!=imports.QueryKey); GalacticSystem Read(string time,int supply,int demand,GalaxyMapFilter filter)=>GalaxyQueryService.Parse(Check.Json($$"""{"results":[{"name":"Ehrlich City","system_name":"Sol","system_id64":1,"system_x":0,"system_y":0,"system_z":0,"market_updated_at":"{{time}}","market":[{"commodity":"Gold","supply":{{supply}},"demand":{{demand}}}]}]}"""),"stations",filter,true,new(0,0,0),50).Single(); var previous=Read("2026-10-22T00:00:00Z",3,0,exports);var current=Read("2026-10-23T00:00:00Z",0,38515,imports); Check.That(imports.Matches(current)&&!exports.Matches(current)); var merged=current with{Data=previous.Data.Merge(current.Data)};Check.That(imports.Matches(merged)&&!exports.Matches(merged)); var late=merged with{Data=merged.Data.Merge(previous.Data)};Check.That(imports.Matches(late)&&!exports.Matches(late)); Check.That(ReferenceEquals(merged.Data,merged.Data.Merge(current.Data))); var other=new GalaxySystemData{StationMarkets=new Dictionary{{"Independent station",new(["Gold"],[],DateTimeOffset.Parse("2026-10-23T00:00:00Z"))}}}; Check.That(exports.Matches(merged with{Data=merged.Data.Merge(other)})); } [Test] public async Task UnsupportedPlayerOwnershipIsNeverTranslatedIntoAPublicPopulationGuess() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); foreach(var category in new[]{GalaxyFilterCategory.Visited,GalaxyFilterCategory.Exploration,GalaxyFilterCategory.TradeData}) {Check.That(!GalaxyQueryService.UsesPublicCatalog(category));Check.Equal(0,(await query.PlansAsync(new(){Mode=GalaxyMapMode.Map,Category=category})).Count);} var owned=new GalaxyMapFilter{Mode=GalaxyMapMode.Colonisation,Category=GalaxyFilterCategory.Colonisation,Values=new HashSet{"localplayerclaimed"}}; Check.Equal(0,(await query.PlansAsync(owned)).Count);Check.Equal(0,handler.Requests.Count); } [Test] public async Task CancelledSupersededSearchCannotPublishAnIgnoringHandlersLateResponse() { var handler=new Api{SlowFirstSearch=true};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);using var stop=new CancellationTokenSource(); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet{"actualcontroller"}}; var published=0;var stale=query.QueryAsync(filter,new(0,0,0),50,stop.Token,_=>published++); await handler.SearchStarted.Task;stop.Cancel();var fresh=await query.QueryAsync(filter,new(0,0,0),50); try{await stale;throw new InvalidOperationException("Cancelled search unexpectedly completed.");}catch(OperationCanceledException){ } Check.Equal(0,published);Check.Equal(1,fresh.Systems.Count); } [Test] public async Task PerspectiveQueryIncludesActualVisibleStarsFarBeyondAPivotRadiusAndPublishesItsExactBounds() { var camera=GalaxyCamera.Create(new(0,0,0),1600,900,0,.4,.1); var d=camera.Forward*(camera.Distance*3)+camera.Right*camera.Distance; var point=new Coordinates(camera.Eye.X+d.X,camera.Eye.Y+d.Y,camera.Eye.Z+d.Z); Check.That(camera.Project(point).IsVisible&&point.DistanceTo(camera.Focus)>1000); var viewport=GalaxyViewport.Create(new(0,0,0),1600,900,0,.4,.1,0,0) with{Camera=camera}; var handler=new Api{Point=point};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet{"actualcontroller"}}; var progress=new List();var result=await query.QueryViewportAsync(filter,viewport,progress:progress.Add); Check.Equal(1,result.Systems.Count);Check.That(result.Bounds!.Contains(point));Check.That(result.CoverageComplete);Check.That(progress.Count>0); var request=handler.Requests.First(x=>x.Url.EndsWith("/systems/search")).Json; Check.That(!request.GetProperty("filters").TryGetProperty("distance",out _)); var x=request.GetProperty("filters").GetProperty("x").GetProperty("value");Check.That(x[0].GetDouble()<=point.X&&x[1].GetDouble()>=point.X); } [Test] public async Task DensePublicPartitionsRefineInsteadOfStoppingAtAnArbitraryNearestRowsLimit() { var handler=new Api{DenseFirstSearch=true};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),2000,2000,2000); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet{"actualcontroller"}}; var result=await query.QueryViewportAsync(filter,viewport); Check.That(result.CompletedQueries>=9&&result.CoverageComplete);Check.Equal(0,result.PendingQueries);Check.That(!result.Truncated); var searches=handler.Requests.Where(x=>x.Url.EndsWith("/systems/search")).ToArray();Check.That(searches.Length>=9); Check.That(searches.Skip(1).Any(x=>x.Json.GetProperty("filters").GetProperty("x").GetProperty("value")[1].GetDouble()-x.Json.GetProperty("filters").GetProperty("x").GetProperty("value")[0].GetDouble()<4000)); } [Test] public async Task StarportMarkerUsesActualStationStateInsteadOfSubstitutingBgsWar() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet{"repairing"}}; var plans=await query.PlansAsync(filter);Check.Equal(1,plans.Count);Check.Equal("stations",plans[0].Index); var payload=JsonSerializer.SerializeToElement(plans[0].Filters);Check.Equal("UnderRepairs",payload.GetProperty("state").GetProperty("value")[0].GetString()); var rows=Check.Json("""{"results":[{"system_name":"Repair","system_x":0,"system_y":0,"system_z":0,"state":"UnderRepairs","controlling_minor_faction_state":"War"},{"system_name":"JustWar","system_x":1,"system_y":0,"system_z":0,"controlling_minor_faction_state":"War"}]}"""); var parsed=GalaxyQueryService.Parse(rows,"stations",filter,false,new(0,0,0),10); Check.That(filter.Matches(parsed[0]));Check.That(!filter.Matches(parsed[1])); } [Test] public async Task PrivateColonialEligibilityAndUnresolvedMarkersNeverClaimCompletePublicCoverage() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),10,10,10); foreach(var filter in new[]{new GalaxyMapFilter{Mode=GalaxyMapMode.Colonisation,Category=GalaxyFilterCategory.Colonisation,Values=new HashSet{"localplayerclaimed"}},new(){Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet{"friend"}}}) {var result=await query.QueryViewportAsync(filter,viewport);Check.That(!result.CoverageComplete);Check.Equal(0,result.Systems.Count);} Check.Equal(0,handler.Requests.Count); } [Test] public async Task MissingOrPartlyUnsupportedPublicSelectionsDoNotClaimCompleteCoverage() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),10,10,10); var none=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Civilization,Values=new HashSet{"none"}}; var result=await query.QueryViewportAsync(none,viewport);Check.That(!result.CoverageComplete&&result.Truncated);Check.Equal(0,result.Systems.Count); var partial=none with{Category=GalaxyFilterCategory.Economy,Values=new HashSet{"hightech","unpublished_original_option"}}; result=await query.QueryViewportAsync(partial,viewport);Check.Equal(1,result.Systems.Count);Check.That(!result.CoverageComplete&&result.Truncated); var empty=partial with{Values=new HashSet()};result=await query.QueryViewportAsync(empty,viewport);Check.That(result.CoverageComplete&&!result.Truncated);Check.Equal(0,result.Systems.Count); } [Test] public void ReportedStationListWithoutACarrierCannotProveNoCarrierExists() { var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.FleetCarrier,Values=new HashSet{"absent"}}; var rows=Check.Json("""{"results":[{"name":"Reported","x":0,"y":0,"z":0,"stations":[{"type":"Coriolis Starport"}]}]}"""); var parsed=GalaxyQueryService.Parse(rows,"systems",filter,false,new(0,0,0),10).Single();Check.That(parsed.Data.FleetCarrier is null);Check.That(!filter.Matches(parsed)); } [Test] public void NewExplicitControllerCannotBeOverriddenByAnOlderQueriesPositiveEvidence() { var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet{"oldcontroller"}}; var old=new GalaxySystemData{Power="Old controller",ConfirmedQueries=new HashSet{filter.QueryKey}}; var system=new GalacticSystem("Changed control",new(0,0,0)){Data=old.Merge(new(){Power="New controller"})}; Check.That(!filter.Matches(system));Check.That(!filter.Evaluate(system).Matches); var omitted=new GalacticSystem("Proven result",new(0,0,0)){Data=new(){ConfirmedQueries=new HashSet{filter.QueryKey}}};Check.That(filter.Matches(omitted)); } [Test] public void AStationsNewRecordedStateReplacesItsOldMarkerAndKeepsOtherStationsIndependent() { var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet{"repairing"}}; GalaxySystemData Observation(string json)=>GalaxySystemData.FromJournal(Check.Json(json)); var first=Observation("""{"StationName":"Port A","StationState":"UnderRepairs","timestamp":"2026-10-22T00:00:00Z"}""") with{ConfirmedQueries=new HashSet{filter.QueryKey}}; var updated=first.Merge(Observation("""{"StationName":"Port A","StationState":"Damaged","timestamp":"2026-10-23T00:00:00Z"}""")); Check.That(!filter.Matches(new("Measured",new(0,0,0)){Data=updated})); var historical=updated.Merge(first);Check.That(!filter.Matches(new("Measured",new(0,0,0)){Data=historical})); var second=updated.Merge(Observation("""{"StationName":"Port B","StationState":"UnderRepairs","timestamp":"2026-10-23T00:00:00Z"}""")); Check.That(filter.Matches(new("Measured",new(0,0,0)){Data=second}));Check.Equal(2,second.StationStates!.Count); } [Test] public async Task PublicPowerplayCandidatesStillRequireTheActualNativeTerritoryOwnerAndStage() { var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http); var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Powerplay,Category=GalaxyFilterCategory.Power,Values=new HashSet{"actualcontroller"},PowerStateValues=new HashSet{"stronghold"}}; var plans=await query.PlansAsync(filter);Check.Equal(1,plans.Count);var fields=JsonSerializer.SerializeToElement(plans[0].Filters); Check.Equal("Actual controller",fields.GetProperty("controlling_power").GetProperty("value")[0].GetString());Check.Equal("Stronghold",fields.GetProperty("power_state").GetProperty("value")[0].GetString()); var system=new GalacticSystem("Measured",new(0,0,0)){Data=new(){Power="Actual controller",PowerState="Stronghold",NativeFilterFacts=new("F1",42,100){PowerStatus=new(3,6,2,7,0,0,true),PowerTerritoryOwnerName="Actual controller"}}};Check.That(filter.Matches(system)); Check.That(!filter.Matches(system with{Data=system.Data with{NativeFilterFacts=system.Data.NativeFilterFacts! with{PowerStatus=new(1,4,2,7,0,0,true)}}})); Check.That(!filter.Matches(system with{Data=system.Data with{NativeFilterFacts=system.Data.NativeFilterFacts! with{PowerTerritoryOwnerName="Other influence"}}})); var unknown=system with{Data=system.Data with{NativeFilterFacts=null}};Check.That(!filter.Matches(unknown));Check.That((filter with{IncludeUnknown=true}).Matches(unknown)); } private sealed class Api:HttpMessageHandler { public readonly List<(string Url,JsonElement Json)> Requests=[]; public readonly TaskCompletionSource SearchStarted=new(TaskCreationOptions.RunContinuationsAsynchronously); public bool SlowFirstSearch,DenseFirstSearch;public Coordinates? Point;private int searches; protected override async Task SendAsync(HttpRequestMessage request,CancellationToken token) { var url=request.RequestUri!.AbsolutePath;var payload=request.Content is null?default:Check.Json(await request.Content.ReadAsStringAsync(CancellationToken.None));lock(Requests)Requests.Add((url,payload)); string body; if(url.Contains("field_values"))body=url.EndsWith("/power_state")?"""{"values":["Unoccupied","Exploited","Fortified","Stronghold"]}""":url.EndsWith("/state")?"""{"values":["Construction","Damaged","DamagedHuman","UnderAttack","UnderRepairs"]}""":url.EndsWith("marketplace")?"""{"values":{"commodity":["Gold"]}}""":url.EndsWith("stations")?"""{"values":{"services":["Refuel","Restock","Material Trader"]}}""":"""{"values":["High Tech","Industrial","Actual controller"]}"""; else { var sequence=Interlocked.Increment(ref searches);if(sequence==1&&SlowFirstSearch){SearchStarted.SetResult();await Task.Delay(100,CancellationToken.None);} body=url.Contains("/stations/")?"""{"count":1,"results":[{"system_name":"Measured","system_id64":1,"system_x":2,"system_y":3,"system_z":4,"services":[{"name":"Refuel"}],"material_trader":"Raw","market":[{"commodity":"Gold","supply":3}]}]}""":"""{"count":1,"results":[{"name":"Measured","id64":1,"x":2,"y":3,"z":4,"primary_economy":"High Tech","secondary_economy":"Industrial","controlling_power":"Actual controller","stations":[{"services":["Restock"]}]}]}"""; if(DenseFirstSearch&&sequence==1)body=body.Replace("\"count\":1,","\"count\":1000,"); if(Point is {} point)body=body.Replace("\"x\":2,\"y\":3,\"z\":4",FormattableString.Invariant($"\"x\":{point.X:R},\"y\":{point.Y:R},\"z\":{point.Z:R}")); } return new(HttpStatusCode.OK){Content=new StringContent(body,Encoding.UTF8,"application/json")}; } } }