using System.Collections.Generic;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
namespace SVSim.UnitTests.BattleNode.Integration;
///
/// Headless-Conductor milestone tests (M-HC-*). The oracle is a node-native battle:
/// a FIXED master seed + FIXED decks drive the engine's receive path headless, and we
/// assert on engine board-state. By construction the node assigns idx = position in the
/// shuffled order, so the engine's headless draw reproduces the node's draw order.
///
/// Task 1 (M-HC-0a) exit criterion: the engine seats headless (IsReady) in the
/// SVSim.UnitTests process.
///
/// Task 2 (M-HC-0b) exit criterion: a node-generated Deal seats the 3-card hand and a
/// vanilla hand-card Play resolves on ENGINE board state (card left hand, PP dropped
/// by cost, board reflects the play) — driven through the receive CONDUCTOR, not the
/// direct ActionProcessor path the M2-M12 oracles use.
///
/// Task 3 (M-HC-1) exit criterion: the mulligan ops (Swap seats the post-mulligan hand —
/// idx-3 swapped for the next unused deck idx-4) and turn ops (Ready/TurnStart/
/// TurnEnd) resolve headless, so two full turns of a node-native battle track on engine
/// state (hand/board/PP/deck/turn/leader-life on both seats match the deterministic progression
/// at each boundary). All driven through the same receive conductor.
///
[TestFixture]
[NonParallelizable]
public class HeadlessConductorTests
{
[Test]
public void Harness_seats_engine_headless_and_is_ready()
{
using var harness = NodeNativeBattleHarness.Create();
Assert.That(harness.IsReady, Is.True,
"Engine must seat headless: EngineGlobalInit ran + both decks seeded. " +
"If false, the most likely cause is a missing cards.json content link in " +
"SVSim.UnitTests.csproj (EngineGlobalInit reads AppContext.BaseDirectory/Data/cards.json).");
// Non-vacuous: a seated engine has live board state for BOTH seats. Reading these off a
// not-really-set-up engine would throw (Seat() guards on _mgr). Leader life is the headless
// default (20) before any frame is ingested.
Assert.That(harness.LeaderLife(playerSeat: true), Is.EqualTo(20), "seat A leader life");
Assert.That(harness.LeaderLife(playerSeat: false), Is.EqualTo(20), "seat B leader life");
}
// The node's BuildDeal opening hand: pos->idx (0,1),(1,2),(2,3). hand == deck idx 1,2,3, i.e.
// the top 3 of the node-native shuffled deck. Both seats deal the same idx triple.
private static Dictionary DealBody() => new()
{
["self"] = PosIdxList((0, 1), (1, 2), (2, 3)),
["oppo"] = PosIdxList((0, 1), (1, 2), (2, 3)),
};
// A minimal vanilla hand-card play: type 30 == PLAY_HAND; playIdx is the played card's index.
// No targetList/orderList — a vanilla follower auto-resolves with no selection.
private static Dictionary PlayBody(int playIdx) => new()
{
["playIdx"] = playIdx,
["type"] = 30,
};
// A pos->idx list (the wire shape NetworkParameter.self/oppo carry: an ordered list of
// {pos, idx} dicts). The receiver re-sorts by pos into the seat's idx list.
private static List