using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class AutomaticTradeRoundsTests
{
private static readonly DateTimeOffset Start=DateTimeOffset.UtcNow.AddHours(-2);
private static Station Station(long id)=>new(){MarketId=id,Name="Station "+id,SystemName="System "+id};
private static PlayerState Player=>new(){CommanderId="round-test",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,LocationAt=Start.AddMinutes(5)};
private static AutomaticTradeConfiguration Configuration(string root,bool returnTrade=true)
=>new(){Source=Station(1),Destination=Station(2),WorkingDirectory=root,ReturnTrade=returnTrade};
private static string Journey(string root,string name,long from,long to,int minute,bool passed=true,bool sold=true)
{
var directory=Path.Combine(root,"journey-"+name);Directory.CreateDirectory(directory);
var begin=Start.AddMinutes(minute);
var buy=new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),from,"gold",2,10,1000,980,0,2,"round-test",7,Start.AddMinutes(-1),begin.AddSeconds(1),true,true);
var sale=sold?new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),to,"gold",-2,20,980,1020,2,0,"round-test",7,Start.AddMinutes(-1),begin.AddMinutes(1),true,true):null;
var checkpoint=new TradeJourneyCheckpoint("round-test",7,begin,buy,sale);
TradeJourneyCheckpoints.Save(Path.Combine(directory,"journey.json"),checkpoint);
var leg=new TradeLeg{From=Station(from),To=Station(to),Orders=[new("gold","Gold",2,10,20)]};
File.WriteAllText(Path.Combine(directory,"leg.json"),JsonSerializer.Serialize(leg,ToolboxSettings.JsonOptions));
if(sold)File.WriteAllText(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(
new NativeTradeJourneyResult(passed,checkpoint,TradeProfitSnapshot.Empty,1000,1020,passed?null:"services incomplete",begin.AddMinutes(1).AddSeconds(1)),ToolboxSettings.JsonOptions));
return directory;
}
[Test]public void OutboundSaleAloneCompletesANonReturnRoutePass()
{
using var temporary=new TemporaryDirectory();Journey(temporary.Path,"outbound",1,2,0);
var config=Configuration(temporary.Path,returnTrade:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
}
[Test]public void ReturnRouteNeedsCompletedHomeArrivalAndSurvivesRepeatedRecovery()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player));
Journey(temporary.Path,"return-pending",2,1,3,sold:false);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Journey(temporary.Path,"return-completed",2,1,4);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(1,Directory.EnumerateFiles(Path.Combine(temporary.Path,"rounds-v1"),"*.json",SearchOption.AllDirectories).Count());
}
[Test]public void ConfirmedEmptyReturnCountsOnceAndNewOutboundStartsTheNextRound()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(3)};
Check.Equal(1,AutomaticTradeRounds.Refresh(config,home));
Journey(temporary.Path,"next-outbound",1,2,4,sold:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Journey(temporary.Path,"second-outbound",1,2,5);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(2,AutomaticTradeRounds.Refresh(config,home with{LocationAt=Start.AddMinutes(8)}));
Check.Equal(2,AutomaticTradeRounds.Refresh(config,home with{LocationAt=Start.AddMinutes(8)}));
}
[Test]public void SourcePurchaseProvesTheReturnIfTheWorkerRestartedBeforeSavingItsRound()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);Journey(temporary.Path,"next-purchase",1,2,4,sold:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
}
[Test]public void InitialBackwardFailedForeignAndDifferentRouteJourneysDoNotCreateRounds()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"initial-return",2,1,0);Journey(temporary.Path,"failed-outbound",1,2,4,passed:false);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(8)};
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home));
Journey(temporary.Path,"outbound",1,2,10);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home)); // Old source docking cannot attest a later return.
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home with{CommanderId="other",LocationAt=Start.AddMinutes(15)}));
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home with{CurrentShipId=8,LocationAt=Start.AddMinutes(15)}));
Check.Equal(0,AutomaticTradeRounds.Refresh(config with{Destination=Station(3)},home with{LocationAt=Start.AddMinutes(15)}));
}
[Test]public void DuplicateJourneyCopiesDoNotDuplicateTheOriginalSaleRound()
{
using var temporary=new TemporaryDirectory();var original=Journey(temporary.Path,"outbound",1,2,0);
var copied=Path.Combine(temporary.Path,"journey-copy");Directory.CreateDirectory(copied);
foreach(var file in Directory.EnumerateFiles(original))File.Copy(file,Path.Combine(copied,Path.GetFileName(file)));
Check.Equal(1,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),Player));
}
[Test]public void OneFutureHomeObservationCannotCompleteOverlappingDistinctOutboundClaims()
{
using var temporary=new TemporaryDirectory();Journey(temporary.Path,"first",1,2,0);Journey(temporary.Path,"overlapping",1,2,0);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(5)};
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path),home));
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),home));
}
[Test]public void AChangedReceiptInTheResultCannotCountAsTheValidatedCheckpoint()
{
using var temporary=new TemporaryDirectory();var directory=Journey(temporary.Path,"outbound",1,2,0);
var resultPath=Path.Combine(directory,"result.json");
var result=JsonSerializer.Deserialize<NativeTradeJourneyResult>(File.ReadAllText(resultPath),ToolboxSettings.JsonOptions)!;
result=result with{Checkpoint=result.Checkpoint with{Sold=result.Checkpoint.Sold! with{OperationKey=Guid.NewGuid().ToString("N")}}};
File.WriteAllText(resultPath,JsonSerializer.Serialize(result,ToolboxSettings.JsonOptions));
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),Player));
}
private static void GoalProof(string directory,ulong goal=864)
=>File.WriteAllText(Path.Combine(directory,"membership-before.json"),JsonSerializer.Serialize(new{goalId=goal,contribution=0}));
[Test]public void DynamicCgSourceChangesAndRestartPreserveAllConfirmedRounds()
{
using var temporary=new TemporaryDirectory();
var config=Configuration(temporary.Path,false) with{Mode=AutomaticTradeMode.CommunityGoal,CommunityGoalId=864,Commodities=["gold"]};
GoalProof(Journey(temporary.Path,"first",1,2,0));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
GoalProof(Journey(temporary.Path,"different-source",3,2,4));
var changed=config with{Source=Station(3)};
Check.Equal(2,AutomaticTradeRounds.Refresh(changed,Player));Check.Equal(2,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(0,AutomaticTradeRounds.Refresh(config with{Commodities=["uranium"]},Player));
GoalProof(Journey(temporary.Path,"other-goal",4,2,8),865);
Journey(temporary.Path,"unverified-goal",5,2,12);
Check.Equal(2,AutomaticTradeRounds.Refresh(changed,Player));
Check.Equal(2,Directory.EnumerateFiles(Path.Combine(temporary.Path,"rounds-v1"),"*.json",SearchOption.AllDirectories).Count());
}
[Test]public void DynamicReturnRouteRequiresTheActualSourceOfEachBatch()
{
using var temporary=new TemporaryDirectory();
var config=Configuration(temporary.Path) with{Mode=AutomaticTradeMode.CommunityGoal,CommunityGoalId=864,Commodities=["gold"]};
GoalProof(Journey(temporary.Path,"from-three",3,2,0));
Journey(temporary.Path,"buy-at-other-source",1,2,4,sold:false);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player with{MarketId=1,LocationAt=Start.AddMinutes(5)}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{MarketId=3,LocationAt=Start.AddMinutes(5)}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config with{Source=Station(4)},Player));
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class AutomaticTradeRoundsTests
{
private static readonly DateTimeOffset Start=DateTimeOffset.UtcNow.AddHours(-2);
private static Station Station(long id)=>new(){MarketId=id,Name="Station "+id,SystemName="System "+id};
private static PlayerState Player=>new(){CommanderId="round-test",CurrentShipId=7,InGame=true,Docked=true,MarketId=2,LocationAt=Start.AddMinutes(5)};
private static AutomaticTradeConfiguration Configuration(string root,bool returnTrade=true)
=>new(){Source=Station(1),Destination=Station(2),WorkingDirectory=root,ReturnTrade=returnTrade};
private static string Journey(string root,string name,long from,long to,int minute,bool passed=true,bool sold=true)
{
var directory=Path.Combine(root,"journey-"+name);Directory.CreateDirectory(directory);
var begin=Start.AddMinutes(minute);
var buy=new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),from,"gold",2,10,1000,980,0,2,"round-test",7,Start.AddMinutes(-1),begin.AddSeconds(1),true,true);
var sale=sold?new ConfirmedTradeReceipt(Guid.NewGuid().ToString("N"),to,"gold",-2,20,980,1020,2,0,"round-test",7,Start.AddMinutes(-1),begin.AddMinutes(1),true,true):null;
var checkpoint=new TradeJourneyCheckpoint("round-test",7,begin,buy,sale);
TradeJourneyCheckpoints.Save(Path.Combine(directory,"journey.json"),checkpoint);
var leg=new TradeLeg{From=Station(from),To=Station(to),Orders=[new("gold","Gold",2,10,20)]};
File.WriteAllText(Path.Combine(directory,"leg.json"),JsonSerializer.Serialize(leg,ToolboxSettings.JsonOptions));
if(sold)File.WriteAllText(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(
new NativeTradeJourneyResult(passed,checkpoint,TradeProfitSnapshot.Empty,1000,1020,passed?null:"services incomplete",begin.AddMinutes(1).AddSeconds(1)),ToolboxSettings.JsonOptions));
return directory;
}
[Test]public void OutboundSaleAloneCompletesANonReturnRoutePass()
{
using var temporary=new TemporaryDirectory();Journey(temporary.Path,"outbound",1,2,0);
var config=Configuration(temporary.Path,returnTrade:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
}
[Test]public void ReturnRouteNeedsCompletedHomeArrivalAndSurvivesRepeatedRecovery()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player));
Journey(temporary.Path,"return-pending",2,1,3,sold:false);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Journey(temporary.Path,"return-completed",2,1,4);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(1,Directory.EnumerateFiles(Path.Combine(temporary.Path,"rounds-v1"),"*.json",SearchOption.AllDirectories).Count());
}
[Test]public void ConfirmedEmptyReturnCountsOnceAndNewOutboundStartsTheNextRound()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(3)};
Check.Equal(1,AutomaticTradeRounds.Refresh(config,home));
Journey(temporary.Path,"next-outbound",1,2,4,sold:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
Journey(temporary.Path,"second-outbound",1,2,5);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(2,AutomaticTradeRounds.Refresh(config,home with{LocationAt=Start.AddMinutes(8)}));
Check.Equal(2,AutomaticTradeRounds.Refresh(config,home with{LocationAt=Start.AddMinutes(8)}));
}
[Test]public void SourcePurchaseProvesTheReturnIfTheWorkerRestartedBeforeSavingItsRound()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"outbound",1,2,0);Journey(temporary.Path,"next-purchase",1,2,4,sold:false);
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{Docked=false,MarketId=null}));
}
[Test]public void InitialBackwardFailedForeignAndDifferentRouteJourneysDoNotCreateRounds()
{
using var temporary=new TemporaryDirectory();var config=Configuration(temporary.Path);
Journey(temporary.Path,"initial-return",2,1,0);Journey(temporary.Path,"failed-outbound",1,2,4,passed:false);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(8)};
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home));
Journey(temporary.Path,"outbound",1,2,10);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home)); // Old source docking cannot attest a later return.
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home with{CommanderId="other",LocationAt=Start.AddMinutes(15)}));
Check.Equal(0,AutomaticTradeRounds.Refresh(config,home with{CurrentShipId=8,LocationAt=Start.AddMinutes(15)}));
Check.Equal(0,AutomaticTradeRounds.Refresh(config with{Destination=Station(3)},home with{LocationAt=Start.AddMinutes(15)}));
}
[Test]public void DuplicateJourneyCopiesDoNotDuplicateTheOriginalSaleRound()
{
using var temporary=new TemporaryDirectory();var original=Journey(temporary.Path,"outbound",1,2,0);
var copied=Path.Combine(temporary.Path,"journey-copy");Directory.CreateDirectory(copied);
foreach(var file in Directory.EnumerateFiles(original))File.Copy(file,Path.Combine(copied,Path.GetFileName(file)));
Check.Equal(1,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),Player));
}
[Test]public void OneFutureHomeObservationCannotCompleteOverlappingDistinctOutboundClaims()
{
using var temporary=new TemporaryDirectory();Journey(temporary.Path,"first",1,2,0);Journey(temporary.Path,"overlapping",1,2,0);
var home=Player with{MarketId=1,LocationAt=Start.AddMinutes(5)};
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path),home));
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),home));
}
[Test]public void AChangedReceiptInTheResultCannotCountAsTheValidatedCheckpoint()
{
using var temporary=new TemporaryDirectory();var directory=Journey(temporary.Path,"outbound",1,2,0);
var resultPath=Path.Combine(directory,"result.json");
var result=JsonSerializer.Deserialize<NativeTradeJourneyResult>(File.ReadAllText(resultPath),ToolboxSettings.JsonOptions)!;
result=result with{Checkpoint=result.Checkpoint with{Sold=result.Checkpoint.Sold! with{OperationKey=Guid.NewGuid().ToString("N")}}};
File.WriteAllText(resultPath,JsonSerializer.Serialize(result,ToolboxSettings.JsonOptions));
Check.Equal(0,AutomaticTradeRounds.Refresh(Configuration(temporary.Path,false),Player));
}
private static void GoalProof(string directory,ulong goal=864)
=>File.WriteAllText(Path.Combine(directory,"membership-before.json"),JsonSerializer.Serialize(new{goalId=goal,contribution=0}));
[Test]public void DynamicCgSourceChangesAndRestartPreserveAllConfirmedRounds()
{
using var temporary=new TemporaryDirectory();
var config=Configuration(temporary.Path,false) with{Mode=AutomaticTradeMode.CommunityGoal,CommunityGoalId=864,Commodities=["gold"]};
GoalProof(Journey(temporary.Path,"first",1,2,0));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player));
GoalProof(Journey(temporary.Path,"different-source",3,2,4));
var changed=config with{Source=Station(3)};
Check.Equal(2,AutomaticTradeRounds.Refresh(changed,Player));Check.Equal(2,AutomaticTradeRounds.Refresh(config,Player));
Check.Equal(0,AutomaticTradeRounds.Refresh(config with{Commodities=["uranium"]},Player));
GoalProof(Journey(temporary.Path,"other-goal",4,2,8),865);
Journey(temporary.Path,"unverified-goal",5,2,12);
Check.Equal(2,AutomaticTradeRounds.Refresh(changed,Player));
Check.Equal(2,Directory.EnumerateFiles(Path.Combine(temporary.Path,"rounds-v1"),"*.json",SearchOption.AllDirectories).Count());
}
[Test]public void DynamicReturnRouteRequiresTheActualSourceOfEachBatch()
{
using var temporary=new TemporaryDirectory();
var config=Configuration(temporary.Path) with{Mode=AutomaticTradeMode.CommunityGoal,CommunityGoalId=864,Commodities=["gold"]};
GoalProof(Journey(temporary.Path,"from-three",3,2,0));
Journey(temporary.Path,"buy-at-other-source",1,2,4,sold:false);
Check.Equal(0,AutomaticTradeRounds.Refresh(config,Player with{MarketId=1,LocationAt=Start.AddMinutes(5)}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config,Player with{MarketId=3,LocationAt=Start.AddMinutes(5)}));
Check.Equal(1,AutomaticTradeRounds.Refresh(config with{Source=Station(4)},Player));
}
}