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<string>{"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<string>{"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<string>{"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<string>{"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<string>{"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<string>{"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<string,GalaxyStationMarket>{{"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<string>{"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<string>{"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<string>{"actualcontroller"}};
var progress=new List<GalaxyQueryResult>();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<string>{"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<string>{"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<string>{"localplayerclaimed"}},new(){Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"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<string>{"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<string>{"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<string>()};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<string>{"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<string>{"oldcontroller"}};
var old=new GalaxySystemData{Power="Old controller",ConfirmedQueries=new HashSet<string>{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<string>{filter.QueryKey}}};Check.That(filter.Matches(omitted));
}
[Test] public void AStationsNewRecordedStateReplacesItsOldMarkerAndKeepsOtherStationsIndependent()
{
var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"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<string>{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<string>{"actualcontroller"},PowerStateValues=new HashSet<string>{"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<HttpResponseMessage> 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")};
}
}
}
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<string>{"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<string>{"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<string>{"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<string>{"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<string>{"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<string>{"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<string,GalaxyStationMarket>{{"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<string>{"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<string>{"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<string>{"actualcontroller"}};
var progress=new List<GalaxyQueryResult>();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<string>{"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<string>{"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<string>{"localplayerclaimed"}},new(){Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"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<string>{"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<string>{"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<string>()};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<string>{"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<string>{"oldcontroller"}};
var old=new GalaxySystemData{Power="Old controller",ConfirmedQueries=new HashSet<string>{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<string>{filter.QueryKey}}};Check.That(filter.Matches(omitted));
}
[Test] public void AStationsNewRecordedStateReplacesItsOldMarkerAndKeepsOtherStationsIndependent()
{
var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"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<string>{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<string>{"actualcontroller"},PowerStateValues=new HashSet<string>{"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<HttpResponseMessage> 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")};
}
}
}