using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using SVSim.BattleNode.Bridge; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Controllers; /// /// Cross-family contract for /do_matching. The single load-bearing assertion: when /// is true, every family's /// first poll must bypass pair-up and return a SUCCEEDED matching_state with a battle_id + /// node_server_url — not the 3002 RETRY of the normal pair-up path. /// /// Adding a new family is the failure trigger for this test: the new controller MUST route /// through , or this test /// fails. That's the point — the test enforces "stay in line" across families. /// /// [TestFixture] public class DoMatchingContractTests { private static readonly object DoMatchingBody = new { deck_no = 1L, need_init = 1, log = 1, excluded_field_id_list = Array.Empty(), use_stage_select = 1, is_default_skin = 0, viewer_id = "0", steam_id = 0, steam_session_ticket = "", }; [TestCase("/arena_two_pick_battle/do_matching", FamilyKind.TwoPick)] [TestCase("/rotation_rank_battle/do_matching", FamilyKind.RankRotation)] [TestCase("/unlimited_rank_battle/do_matching", FamilyKind.RankUnlimited)] public async Task SoloDefaultsToScripted_short_circuits_every_family_to_immediate_SUCCEEDED(string url, FamilyKind family) { await using var factory = new SVSimTestFactory(); factory.Services.GetRequiredService().SoloDefaultsToScripted = true; var viewerId = await factory.SeedViewerAsync(); await SetupFamilyAsync(factory, viewerId, family); using var client = factory.CreateAuthenticatedClient(viewerId); var resp = await client.PostAsJsonAsync(url, DoMatchingBody); Assert.That(resp.IsSuccessStatusCode, Is.True, $"Expected 2xx from {url}, got {resp.StatusCode}."); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); var root = doc.RootElement; var state = root.GetProperty("matching_state").GetInt32(); Assert.That(state, Is.Not.EqualTo(3002), $"{url}: SoloDefaultsToScripted=true must bypass pair-up; saw matching_state=3002 RETRY which means the family didn't honor the flag (probably forgot to route through IMatchingResolver)."); Assert.That(state, Is.AnyOf(3004, 3007, 3011), $"{url}: matching_state must be SUCCEEDED (3004), SUCCEEDED_OWNER (3007), or AI_SUCCEEDED (3011) — got {state}."); Assert.That(root.GetProperty("battle_id").GetString(), Is.Not.Null.And.Not.Empty, $"{url}: SUCCEEDED responses must carry battle_id."); Assert.That(root.GetProperty("node_server_url").GetString(), Does.Contain("/socket.io/"), $"{url}: node_server_url must point at the WS endpoint."); } // Each family has different prerequisites — TK2 needs an active draft run, rank needs // a deck for the requested format. The factory's seeders are sufficient for both. public enum FamilyKind { TwoPick, RankRotation, RankUnlimited } private static async Task SetupFamilyAsync(SVSimTestFactory factory, long viewerId, FamilyKind family) { switch (family) { case FamilyKind.TwoPick: await SeedCompleteTwoPickRunAsync(factory, viewerId); break; case FamilyKind.RankRotation: await factory.SeedGlobalsAsync(); await factory.SeedDeckAsync(viewerId, Format.Rotation, number: 1); break; case FamilyKind.RankUnlimited: await factory.SeedGlobalsAsync(); await factory.SeedDeckAsync(viewerId, Format.Unlimited, number: 1); break; default: throw new ArgumentOutOfRangeException(nameof(family)); } } // Mirrors ArenaTwoPickBattleControllerTests.SeedCompleteTwoPickRunAsync. Duplicated // rather than promoted because the original is a private static there and only this // test class needs to share it cross-family today; promote if a third caller surfaces. private static async Task SeedCompleteTwoPickRunAsync(SVSimTestFactory factory, long viewerId) { using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var deck = Enumerable.Range(1, 30).Select(i => 100_011_000L + i).ToList(); db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun { ViewerId = viewerId, EntryId = 1, ClassId = 1, LeaderSkinId = 1, SelectedCardIdsJson = JsonSerializer.Serialize(deck), IsSelectCompleted = true, MaxBattleCount = 5, CandidateClassIdsJson = "[1,2,3]", PendingPickSetsJson = "[]", ResultListJson = "[]", NextCandidateId = 1, }); await db.SaveChangesAsync(); } }