using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using SVSim.BattleNode.Bridge;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Wire;
using SVSim.UnitTests.Infrastructure;
namespace SVSim.UnitTests.BattleNode.Integration;
///
/// Wire-shape conformance of our server-authored synchronize frames against real prod TK2
/// captures (data_dumps/captures/battle-traffic_tk2_regular.ndjson +
/// …_tk2_second.ndjson, captured 2026-05-31 from a real client mid-PvP).
///
/// What this guards: for every frame our server *authors* (as opposed to forwarding a
/// client's bytes), the payload it emits must carry every key prod sent, with a matching value
/// *category* (object / array / string / number / bool). This is the bug class that has bitten the
/// node repeatedly — wrong casing (card_id vs cardID), a missing field the client
/// reads without a guard, or a string where the client expects a number. The existing
/// assert frame *ordering and routing*; they never inspect the
/// body. This closes that gap and turns the prod captures into a permanent regression oracle that
/// survives the June-2026 server shutdown.
///
/// Direction of the check is capture ⊆ ours — we must emit at least what prod emits
/// (missing/miscased/mistyped = fail), but we may emit extra envelope fields (we send
/// viewerId/uuid/try/cat on pushes; prod's receive frames omit them). Pure
/// envelope/sequencing keys (viewerId, uuid, try, cat, bid, pubSeq, playSeq) are excluded
/// from the comparison: they're transport concerns assigned by the sequencer, covered by the
/// reliability layer + integration tests, and legitimately vary per frame (e.g. the no-stock
/// BattleFinish frame is played immediately whether or not it carries a playSeq).
/// The check is on *body shape*.
///
/// Coverage: a two-client PvP session emits all ten server-authored URIs
/// (InitNetwork, Matched, BattleStart, Deal, Swap, Ready, TurnStart, TurnEnd, Judge,
/// BattleFinish). PvP authors the handshake/mulligan frames through the same shared
/// builders, and the turn cycle
/// (TurnStart/TurnEnd/Judge) falls out of the real two-client handover. Forwarded frames
/// (PlayActions / TurnEndActions / ChatStamp / TurnEndFinal) relay the
/// client's own bytes verbatim, so their shape is the client's contract, not ours — out of scope
/// here.
///
[TestFixture]
public class CaptureConformanceTests
{
// Top-level keys that are envelope/transport, not body shape. Excluded from the comparison
// at the root level only (nested objects never contain these).
private static readonly HashSet IgnoredEnvelopeKeys = new()
{
"viewerId", "uuid", "try", "cat", "bid", "pubSeq", "playSeq",
};
[Test]
[Timeout(60000)]
public async Task ServerAuthoredFrames_MatchProdCaptureShapes()
{
await using var factory = new SVSimTestFactory();
var bridge = factory.Services.GetRequiredService();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
var ct = cts.Token;
// Two-client PvP drive. PvP authors the same handshake/mulligan frames the old Scripted
// path did (via the shared server-frame builders) PLUS the turn-cycle frames
// (TurnStart/TurnEnd/Judge) the scripted bot used to fake — so a two-client session
// harvests all ten server-authored URIs. The shape check is category-based, so PvP's
// spin:0 still matches prod's spin:189.
const long vidA = 906243102L;
const long vidB = 847666884L;
var pending = bridge.RegisterBattle(
new BattlePlayer(vidA, BattleNodeFlowTests.FixtureCtx()),
new BattlePlayer(vidB, BattleNodeFlowTests.FixtureCtx()),
SVSim.BattleNode.Sessions.BattleType.Pvp);
var key = MakeKey();
var (clientA, clientB) = await ConnectBothAsync(factory, pending.BattleId, vidA, vidB, key, ct);
await using var _a = clientA;
await using var _b = clientB;
await Task.WhenAll(clientA.ConsumeHandshakeAsync(ct), clientB.ConsumeHandshakeAsync(ct));
var harvested = new Dictionary();
void Harvest(MsgEnvelope env) => harvested[env.Uri] = env;
long seqA = 1, seqB = 1;
// A walks the handshake; Ready is withheld by the mulligan barrier until B also swaps.
await clientA.SendMsgAsync(MakeEnvelope(vidA, NetworkBattleUri.InitNetwork, seqA++), key, ct);
Harvest(await clientA.ReceiveSynchronizeAsync(ct)); // InitNetwork ack
await clientA.SendMsgAsync(MakeEnvelope(vidA, NetworkBattleUri.InitBattle, seqA++), key, ct);
Harvest(await clientA.ReceiveSynchronizeAsync(ct)); // Matched
await clientA.SendMsgAsync(MakeEnvelope(vidA, NetworkBattleUri.Loaded, seqA++), key, ct);
Harvest(await clientA.ReceiveSynchronizeAsync(ct)); // BattleStart
Harvest(await clientA.ReceiveSynchronizeAsync(ct)); // Deal
await clientA.SendMsgAsync(MakeEnvelope(vidA, NetworkBattleUri.Swap, seqA++,
new Dictionary { ["idxList"] = new List