using XFEEDToolBox.Core.Data;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class FleetSwapTests
{
private const string AnacondaLoadout="""
{"timestamp":"2026-10-23T05:00:00Z","event":"Loadout","Ship":"Anaconda","ShipID":7,"ShipName":"Ana","ShipIdent":"ANA-07","CargoCapacity":16,"FuelCapacity":{"Main":32,"Reserve":1},"Modules":[
{"Slot":"MainEngines","Item":"int_engine_size7_class5","On":true,"Priority":0,"Health":1,"Engineering":{"Engineer":"Professor Palin","BlueprintName":"Engine_Dirty","Level":5,"Quality":1,"ExperimentalEffect":"special_engine_overloaded","Modifiers":[{"Label":"EngineOptimalMass","Value":1700,"OriginalValue":1800,"LessIsGood":0}]}}
]}
""";
private const string StoredBeforeSwap="""
{"timestamp":"2026-10-23T05:32:55Z","event":"StoredShips","StationName":"Verrier Terminal","MarketID":3223708928,"StarSystem":"Omicron Gruis","ShipsHere":[{"ShipID":3,"ShipType":"mandalay","Value":62152446,"Hot":false}],"ShipsRemote":[]}
""";
// The real game's StoreOldShip field contains a ship type, not a boolean.
private const string SwapToMandalay="""
{"timestamp":"2026-10-23T05:46:53Z","event":"ShipyardSwap","ShipType":"mandalay","ShipID":3,"StoreOldShip":"Anaconda","StoreShipID":7,"MarketID":3223708928}
""";
[Test] public void SwitchingFromAnaToMandalayKeepsAnaAndHerHistoricalEngineering()
{
var player=new PlayerProjector();
player.Apply(Check.Json(AnacondaLoadout));
var original=player.State.Ships.Single();
player.Apply(Check.Json(StoredBeforeSwap));
Check.Equal(2,player.State.Ships.Count);
player.Apply(Check.Json(SwapToMandalay));
player.Apply(Check.Json("""{"timestamp":"2026-10-23T05:46:54Z","event":"Loadout","Ship":"Mandalay","ShipID":3,"Modules":[]}"""));
Check.Equal(3L,player.State.CurrentShipId!.Value);
Check.Equal(2,player.State.Ships.Count);
var ana=player.State.Ships.Single(s=>s.ShipId==7);
Check.Equal("Ana",ana.Name);Check.Equal("ANA-07",ana.Ident);
Check.Equal(original.ConfigurationAt,ana.ConfigurationAt);
Check.That(ReferenceEquals(original.Modules,ana.Modules),"Changing ships must preserve the previous ship's complete loadout.");
Check.Equal("Engine_Dirty",ana.Modules.Single().Engineering!.Blueprint);
Check.Equal(5,ana.Modules.Single().Engineering!.Level);
}
[Test] public void SwapCanRecoverAStoredShipEvenWithoutAnEarlierLoadout()
{
var fleet=new FleetProjector();
fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json(SwapToMandalay),"Omicron Gruis");
var ana=fleet.Ships.Single(s=>s.ShipId==7);
Check.Equal("Anaconda",ana.Type);
Check.Equal("Omicron Gruis",ana.SystemName);
Check.Equal(3223708928L,ana.MarketId!.Value);
Check.That(ana.ConfigurationAt is null&&ana.Modules.Count==0,"A swap records ownership and location, not an unobserved loadout.");
}
[Test] public void AStoredShipWithIdZeroIsRetainedAndSellingItStillRemovesIt()
{
var fleet=new FleetProjector();
fleet.Apply(Check.Json(AnacondaLoadout.Replace("\"ShipID\":7","\"ShipID\":0")),"Omicron Gruis");
fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json(SwapToMandalay.Replace("\"StoreShipID\":7","\"StoreShipID\":0")),"Omicron Gruis");
Check.That(fleet.Ships.Any(s=>s.ShipId==0));
fleet.Apply(Check.Json("""{"timestamp":"2026-10-23T05:47:00Z","event":"ShipyardSell","SellShipID":0}"""),"Omicron Gruis");
Check.That(fleet.Ships.All(s=>s.ShipId!=0));
Check.Equal(3L,fleet.CurrentShipId!.Value);
}
[Test] public void MissingOrInvalidStoredIdDoesNotCreateAPhantomShip()
{
foreach(var storedId in new[]{"",",\"StoreShipID\":-1",",\"StoreShipID\":null"})
{
var fleet=new FleetProjector();fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json("{\"timestamp\":\"2026-10-23T05:46:53Z\",\"event\":\"ShipyardSwap\",\"ShipType\":\"mandalay\",\"ShipID\":3,\"StoreOldShip\":\"Anaconda\""+storedId+"}"),"Omicron Gruis");
Check.Equal(1,fleet.Ships.Count);Check.Equal(3L,fleet.Ships.Single().ShipId);
}
}
}
using XFEEDToolBox.Core.Data;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class FleetSwapTests
{
private const string AnacondaLoadout="""
{"timestamp":"2026-10-23T05:00:00Z","event":"Loadout","Ship":"Anaconda","ShipID":7,"ShipName":"Ana","ShipIdent":"ANA-07","CargoCapacity":16,"FuelCapacity":{"Main":32,"Reserve":1},"Modules":[
{"Slot":"MainEngines","Item":"int_engine_size7_class5","On":true,"Priority":0,"Health":1,"Engineering":{"Engineer":"Professor Palin","BlueprintName":"Engine_Dirty","Level":5,"Quality":1,"ExperimentalEffect":"special_engine_overloaded","Modifiers":[{"Label":"EngineOptimalMass","Value":1700,"OriginalValue":1800,"LessIsGood":0}]}}
]}
""";
private const string StoredBeforeSwap="""
{"timestamp":"2026-10-23T05:32:55Z","event":"StoredShips","StationName":"Verrier Terminal","MarketID":3223708928,"StarSystem":"Omicron Gruis","ShipsHere":[{"ShipID":3,"ShipType":"mandalay","Value":62152446,"Hot":false}],"ShipsRemote":[]}
""";
// The real game's StoreOldShip field contains a ship type, not a boolean.
private const string SwapToMandalay="""
{"timestamp":"2026-10-23T05:46:53Z","event":"ShipyardSwap","ShipType":"mandalay","ShipID":3,"StoreOldShip":"Anaconda","StoreShipID":7,"MarketID":3223708928}
""";
[Test] public void SwitchingFromAnaToMandalayKeepsAnaAndHerHistoricalEngineering()
{
var player=new PlayerProjector();
player.Apply(Check.Json(AnacondaLoadout));
var original=player.State.Ships.Single();
player.Apply(Check.Json(StoredBeforeSwap));
Check.Equal(2,player.State.Ships.Count);
player.Apply(Check.Json(SwapToMandalay));
player.Apply(Check.Json("""{"timestamp":"2026-10-23T05:46:54Z","event":"Loadout","Ship":"Mandalay","ShipID":3,"Modules":[]}"""));
Check.Equal(3L,player.State.CurrentShipId!.Value);
Check.Equal(2,player.State.Ships.Count);
var ana=player.State.Ships.Single(s=>s.ShipId==7);
Check.Equal("Ana",ana.Name);Check.Equal("ANA-07",ana.Ident);
Check.Equal(original.ConfigurationAt,ana.ConfigurationAt);
Check.That(ReferenceEquals(original.Modules,ana.Modules),"Changing ships must preserve the previous ship's complete loadout.");
Check.Equal("Engine_Dirty",ana.Modules.Single().Engineering!.Blueprint);
Check.Equal(5,ana.Modules.Single().Engineering!.Level);
}
[Test] public void SwapCanRecoverAStoredShipEvenWithoutAnEarlierLoadout()
{
var fleet=new FleetProjector();
fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json(SwapToMandalay),"Omicron Gruis");
var ana=fleet.Ships.Single(s=>s.ShipId==7);
Check.Equal("Anaconda",ana.Type);
Check.Equal("Omicron Gruis",ana.SystemName);
Check.Equal(3223708928L,ana.MarketId!.Value);
Check.That(ana.ConfigurationAt is null&&ana.Modules.Count==0,"A swap records ownership and location, not an unobserved loadout.");
}
[Test] public void AStoredShipWithIdZeroIsRetainedAndSellingItStillRemovesIt()
{
var fleet=new FleetProjector();
fleet.Apply(Check.Json(AnacondaLoadout.Replace("\"ShipID\":7","\"ShipID\":0")),"Omicron Gruis");
fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json(SwapToMandalay.Replace("\"StoreShipID\":7","\"StoreShipID\":0")),"Omicron Gruis");
Check.That(fleet.Ships.Any(s=>s.ShipId==0));
fleet.Apply(Check.Json("""{"timestamp":"2026-10-23T05:47:00Z","event":"ShipyardSell","SellShipID":0}"""),"Omicron Gruis");
Check.That(fleet.Ships.All(s=>s.ShipId!=0));
Check.Equal(3L,fleet.CurrentShipId!.Value);
}
[Test] public void MissingOrInvalidStoredIdDoesNotCreateAPhantomShip()
{
foreach(var storedId in new[]{"",",\"StoreShipID\":-1",",\"StoreShipID\":null"})
{
var fleet=new FleetProjector();fleet.Apply(Check.Json(StoredBeforeSwap),"Omicron Gruis");
fleet.Apply(Check.Json("{\"timestamp\":\"2026-10-23T05:46:53Z\",\"event\":\"ShipyardSwap\",\"ShipType\":\"mandalay\",\"ShipID\":3,\"StoreOldShip\":\"Anaconda\""+storedId+"}"),"Omicron Gruis");
Check.Equal(1,fleet.Ships.Count);Check.Equal(3L,fleet.Ships.Single().ShipId);
}
}
}