using System.Text.Json; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Wire; using SVSim.Database; using SVSim.Database.Models; using SVSim.EmulatedEntrypoint; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.BattleNode.Integration; [TestFixture] public class BattleNodeFlowTests { /// /// End-to-end smoke for the v1 scripted lifecycle. Boots the EmulatedEntrypoint via /// SVSimTestFactory, mints a battle through IMatchingBridge with a fixture MatchContext, /// opens a raw Socket.IO v2 client against the in-process TestServer, and drives /// InitNetwork → Loaded → Swap, asserting the right scripted frames come back in order. /// [Test] [Timeout(30000)] public async Task ClientWalksHandshakeToReady_ReceivesAllScriptedFrames() { await using var factory = new SVSimTestFactory(); var bridge = factory.Services.GetRequiredService(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var ct = cts.Token; var pending = bridge.RegisterPendingBattle(viewerId: 906243102, context: FixtureCtx()); var key = MakeKey(); var encryptedVid = NodeCrypto.EncryptForNode("906243102", key); var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket"); var wsClient = factory.Server.CreateWebSocketClient(); var ws = await wsClient.ConnectAsync(wsUri, ct); await using var client = new RawSocketIoTestClient(ws); await client.ConsumeHandshakeAsync(ct); await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork)); await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched)); await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 3), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart)); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal)); await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 4, body: new Dictionary { ["idxList"] = new List() }), key, ct); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Swap)); Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Ready)); } private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary? body = null) => new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0, Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General : uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching : EmitCategory.Battle, PubSeq: pubSeq, PlaySeq: null, Body: new RawBody(body ?? new Dictionary())); private static string MakeKey() { var seq = 0; return NodeCrypto.GenerateKey(() => (seq++ * 13) % 16); } internal static MatchContext FixtureCtx(IReadOnlyList? deck = null) => new( SelfDeckCardIds: deck ?? Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(), ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015", CountryCode: "KOR", UserName: "Player", SleeveId: "3000011", EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0, BattleType: 11); /// /// End-to-end: a viewer with a real TK2 run sees their drafted card-ids in the Matched /// frame's selfDeck. This is the "visible win" — proves the full plumbing chain works /// against an actual seeded viewer. /// [Test] [Timeout(30000)] public async Task Matched_frame_contains_drafted_deck_cards() { await using var factory = new SVSimTestFactory(); var vid = await factory.SeedViewerAsync(); var draftedDeck = Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToList(); using (var seedScope = factory.Services.CreateScope()) { var db = seedScope.ServiceProvider.GetRequiredService(); db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun { ViewerId = vid, EntryId = 1, ClassId = 1, LeaderSkinId = 1, SelectedCardIdsJson = JsonSerializer.Serialize(draftedDeck), IsSelectCompleted = true, MaxBattleCount = 5, CandidateClassIdsJson = "[1,2,3]", PendingPickSetsJson = "[]", ResultListJson = "[]", NextCandidateId = 1, }); await db.SaveChangesAsync(); } using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); var ctx = await builder.BuildForTwoPickAsync(vid); var bridge = factory.Services.GetRequiredService(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); var ct = cts.Token; var pending = bridge.RegisterPendingBattle(viewerId: vid, context: ctx); var key = MakeKey(); var encryptedVid = NodeCrypto.EncryptForNode(vid.ToString(), key); var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket"); var wsClient = factory.Server.CreateWebSocketClient(); var ws = await wsClient.ConnectAsync(wsUri, ct); await using var client = new RawSocketIoTestClient(ws); await client.ConsumeHandshakeAsync(ct); // InitNetwork → ack await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct); await client.ReceiveSynchronizeAsync(ct); // InitBattle → Matched (this is the frame we care about) await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitBattle, pubSeq: 2), key, ct); var matched = await client.ReceiveSynchronizeAsync(ct); Assert.That(matched.Uri, Is.EqualTo(NetworkBattleUri.Matched)); // MsgEnvelope.FromJson always inflates Body as a RawBody dictionary — selfDeck is a // List of nested dicts with int "idx" + long "cardId" keys. var body = ((RawBody)matched.Body).Entries; var selfDeck = (List)body["selfDeck"]!; Assert.That(selfDeck.Count, Is.EqualTo(30)); for (int i = 0; i < 30; i++) { var entry = (Dictionary)selfDeck[i]!; Assert.That((long)entry["idx"]!, Is.EqualTo(i + 1L), $"slot {i}: idx should be 1-based position"); Assert.That((long)entry["cardId"]!, Is.EqualTo(draftedDeck[i]), $"slot {i}: cardId should match the drafted card"); } } private static MsgEnvelope MakeEnvelopeWith(long vid, NetworkBattleUri uri, long pubSeq) => new(uri, ViewerId: vid, Uuid: "udid-test", Bid: null, Try: 0, Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General : uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching : EmitCategory.Battle, PubSeq: pubSeq, PlaySeq: null, Body: new RawBody(new Dictionary())); }