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); } } }