using Chessistics.Engine.Events;
using Chessistics.Engine.Model;
using Chessistics.Tests.Helpers;
using Xunit;
namespace Chessistics.Tests.Simulation;
///
/// End-to-end solvability tests: each test places pieces, runs the simulation,
/// and asserts VictoryEvent is produced — proving the level is winnable.
///
public class SolvabilityTests
{
[Fact]
public void SingleRook_ShortRelay_Victory()
{
// 3x1: Prod(0,0) — Rook(1,0↔2,0) — Demand(2,0)
// Rook at (1,0) picks up from prod, at (2,0) is ON demand (not adjacent).
// Delivery happens when rook returns to (1,0), adjacent to demand at (2,0).
// Wait — (1,0) is adjacent to (2,0) ✓ so delivery from (1,0).
var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(2, 0, "Depot", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void ThreePieceChain_SharedRelayPoints_Victory()
{
// 5x2: three rooks form a chain with shared relay points.
// Prod(0,0) — A(1,0↔2,0) — B(2,0↔3,0) — C(3,0↔4,0) — Demand(4,0)
// Pieces share cells (2,0) and (3,0) but never collide:
// Odd turns: A@(2,0) B@(3,0) C@(4,0)
// Even turns: A@(1,0) B@(2,0) C@(3,0)
var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40)
.WithStock(PieceKind.Rook, 3)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (2, 0), (3, 0));
sim.Place(PieceKind.Rook, (3, 0), (4, 0));
sim.Start();
var allEvents = sim.StepN(30);
Assert.Contains(allEvents, e => e is VictoryEvent);
// Verify cargo actually traversed the chain (not just a shortcut)
Assert.True(
allEvents.OfType().Count() >= 4,
"Expected at least 4 cargo transfers across the 3-piece chain");
}
[Fact]
public void TwoDemands_SingleSource_BothSatisfied()
{
// 4x3: one production feeds two demands via two rooks.
// Prod(0,0) at origin.
// D1(2,0) along row 0, D2(0,2) along col 0.
// Rook A(1,0↔2,0): picks up at (1,0), delivers to D1 from (1,0).
// Rook B(0,1↔0,2): picks up at (0,1), delivers to D2 from (0,1).
// Both rooks compete for the same buffer; A gets priority (placed first).
var level = new BoardBuilder(4, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(2, 0, "Depot Royal", CargoType.Wood, 2, 30)
.WithDemand(0, 2, "Caserne", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 2)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (0, 1), (0, 2));
sim.Start();
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is VictoryEvent);
// Both demands must have received progress events
var demandProgress = allEvents.OfType().ToList();
Assert.Contains(demandProgress, dp => dp.DemandCell == new Coords(2, 0) && dp.Current == dp.Required);
Assert.Contains(demandProgress, dp => dp.DemandCell == new Coords(0, 2) && dp.Current == dp.Required);
}
[Fact]
public void TwoCargoTypes_ParallelRoutes_Victory()
{
// 4x2: two independent production→demand chains, one Wood, one Stone.
// Row 0: Prod_Wood(0,0) — Rook A(1,0↔2,0) — Demand_Wood(3,0)
// Row 1: Prod_Stone(0,1) — Rook B(1,1↔2,1) — Demand_Stone(3,1)
// Proves two cargo types flow independently to their matching demands.
var level = new BoardBuilder(4, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithProduction(0, 1, "Carriere", CargoType.Stone)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 2, 30)
.WithDemand(3, 1, "Forge", CargoType.Stone, 2, 30)
.WithStock(PieceKind.Rook, 2)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (1, 1), (2, 1));
sim.Start();
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is VictoryEvent);
// Verify no wrong-type delivery (Wood to Stone demand or vice-versa)
var transfers = allEvents.OfType().ToList();
foreach (var t in transfers.Where(t => t.To == new Coords(3, 0)))
Assert.Equal(CargoType.Wood, t.Type);
foreach (var t in transfers.Where(t => t.To == new Coords(3, 1)))
Assert.Equal(CargoType.Stone, t.Type);
}
[Fact]
public void Bishop_DiagonalRelay_Victory()
{
// 4x3: bishop provides the diagonal link in a two-piece chain.
// Prod(0,0), Demand(2,1).
// Rook(0,1↔0,0): at (0,1) picks up from prod.
// Bishop(1,1↔2,2): at (1,1) receives from rook, at (2,2) delivers to demand (2,1).
// Even turns: Rook@(0,1), Bishop@(1,1) — adjacent, transfer.
// Odd turns: Rook@(0,0), Bishop@(2,2) — bishop delivers.
var level = new BoardBuilder(4, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(2, 1, "Depot", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1)
.WithStock(PieceKind.Bishop, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (0, 1), (0, 0));
sim.Place(PieceKind.Bishop, (1, 1), (2, 2));
sim.Start();
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Knight_JumpsWall_Victory()
{
// 5x3: a wall blocks the direct path, knight jumps over it.
// Prod(0,0), Demand(4,0).
// Walls: full column 2 — (2,0), (2,1), (2,2).
// Rook(1,0↔1,1): at (1,0) picks up from prod.
// Knight(1,1↔3,0): L-shape (+2,-1) jumps over wall, at (3,0) delivers to demand (4,0).
// Even turns: Rook@(1,0), Knight@(1,1) — adjacent, transfer.
// Odd turns: Knight@(3,0), adjacent to demand — delivers.
var level = new BoardBuilder(5, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 30)
.WithWall(2, 0).WithWall(2, 1).WithWall(2, 2)
.WithStock(PieceKind.Rook, 1)
.WithStock(PieceKind.Knight, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (1, 1));
sim.Place(PieceKind.Knight, (1, 1), (3, 0));
sim.Start();
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is VictoryEvent);
// Verify the knight actually moved across the wall
Assert.Contains(allEvents, e => e is PieceMovedEvent m && m.To == new Coords(3, 0));
}
[Fact]
public void Victory_ReportsCorrectMetrics()
{
// 3x1: single rook relay, verify PiecesUsed, TurnsTaken, CellsOccupied.
var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood)
.WithDemand(2, 0, "D", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
var allEvents = sim.StepN(20);
var victory = allEvents.OfType().FirstOrDefault();
Assert.NotNull(victory);
Assert.Equal(1, victory.Metrics.PiecesUsed);
Assert.True(victory.Metrics.TurnsTaken > 0);
Assert.Equal(2, victory.Metrics.CellsOccupied); // cells (1,0) and (2,0)
}
[Fact]
public void NoCollision_WithSharedRelayPoints()
{
// Two rooks sharing a relay point never collide.
// A(1,0↔2,0), B(2,0↔3,0) — share cell (2,0) but occupy it on alternate turns.
var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "P", CargoType.Wood)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 40)
.WithStock(PieceKind.Rook, 2)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (2, 0), (3, 0));
sim.Start();
var allEvents = sim.StepN(20);
Assert.DoesNotContain(allEvents, e => e is PieceDestroyedEvent);
}
[Fact]
public void CargoFilter_AutoAssigned_PreventsContamination()
{
// 4x1: two productions side by side, two routes with adjacent pieces.
// Prod_Wood(0,0), Prod_Stone(3,0)
// Rook A(1,0↔2,0) — adjacent to both prods on alternating turns.
// Without CargoFilter, A would pick up both types randomly.
// With CargoFilter, A's start (1,0) is adjacent to prod_Wood(0,0),
// so A is filtered to Wood and ignores Stone.
var level = new BoardBuilder(4, 1)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithProduction(3, 0, "Carriere", CargoType.Stone)
.WithDemand(2, 0, "D_Wood", CargoType.Wood, 2, 20)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
// Verify CargoFilter was auto-assigned
var snapshot = sim.Snapshot;
Assert.Equal(CargoType.Wood, snapshot.Pieces[0].CargoFilter);
sim.Start();
var allEvents = sim.StepN(20);
// Piece should only carry Wood — never Stone
var transfers = allEvents.OfType().ToList();
Assert.All(transfers, t => Assert.Equal(CargoType.Wood, t.Type));
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void CargoFilter_PropagatesThroughChain()
{
// 5x2: chain of 3 rooks, first adjacent to Wood production.
// All should inherit Wood filter via relay chain propagation.
var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40)
.WithStock(PieceKind.Rook, 3)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // adj to prod → Wood
sim.Place(PieceKind.Rook, (2, 0), (3, 0)); // shares (2,0) → inherits Wood
sim.Place(PieceKind.Rook, (3, 0), (4, 0)); // shares (3,0) → inherits Wood
var snapshot = sim.Snapshot;
Assert.Equal(CargoType.Wood, snapshot.Pieces[0].CargoFilter);
Assert.Equal(CargoType.Wood, snapshot.Pieces[1].CargoFilter);
Assert.Equal(CargoType.Wood, snapshot.Pieces[2].CargoFilter);
}
[Fact]
public void StepFromEdit_AutoStartsSimulation()
{
// Stepping from Edit phase should auto-start without needing Start command.
var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood)
.WithDemand(2, 0, "D", CargoType.Wood, 1, 30)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
// No Start() — step directly from Edit
var allEvents = sim.StepN(20);
Assert.Contains(allEvents, e => e is TurnStartedEvent);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
}