using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Vanguard.Server.Configuration; using Vanguard.Server.Matches; using Vanguard.Server.Matchmaking; using Vanguard.Server.Profiles; using Vanguard.Server.Tickets; namespace Vanguard.Server.Tests; public class MatchmakingServiceTests { [Fact] public void Two_Players_Are_Matched_And_Get_One_Time_Tickets() { var harness = NewHarness(); var first = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); var second = harness.Matchmaking.Enqueue("p2", ServerTestData.BuildDeck(), GameMode.ClassicRanked); harness.Matchmaking.Matchmake(harness.Time.GetUtcNow()); var firstStatus = harness.Matchmaking.Poll(first.QueueId, "p1"); var secondStatus = harness.Matchmaking.Poll(second.QueueId, "p2"); Assert.NotNull(firstStatus); Assert.NotNull(secondStatus); Assert.Equal("matched", firstStatus!.Status); Assert.Equal("matched", secondStatus!.Status); Assert.Equal(firstStatus.MatchId, secondStatus.MatchId); Assert.Equal(0, firstStatus.Seat); Assert.Equal(1, secondStatus.Seat); Assert.Equal("p2", firstStatus.Opponent); Assert.Equal("p1", secondStatus.Opponent); Assert.NotNull(firstStatus.Ticket); Assert.NotNull(secondStatus.Ticket); var firstTicket = harness.Tickets.Redeem(firstStatus.Ticket); var secondTicket = harness.Tickets.Redeem(secondStatus.Ticket); Assert.Equal(0, firstTicket!.Seat); Assert.Equal(1, secondTicket!.Seat); Assert.Equal(firstStatus.MatchId, firstTicket.MatchId); Assert.Equal(1, harness.Registry.Count); Assert.Equal(1, harness.Registry.ActiveCount); Assert.Equal(0, harness.Matchmaking.QueuedCount); } [Fact] public void Ranked_And_Casual_Queues_Are_Separate() { var harness = NewHarness(); harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); var casual = harness.Matchmaking.Enqueue("p2", ServerTestData.BuildDeck(), GameMode.ClassicCasual); harness.Matchmaking.Matchmake(harness.Time.GetUtcNow()); Assert.Equal("queued", harness.Matchmaking.Poll(casual.QueueId, "p2")!.Status); Assert.Equal(0, harness.Registry.Count); Assert.Equal(2, harness.Matchmaking.QueuedCount); } [Fact] public void Window_Expands_While_Waiting() { var harness = NewHarness(); var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); var initial = harness.Matchmaking.Poll(ticket.QueueId, "p1")!; Assert.Equal(100, initial.Window); harness.Time.Advance(TimeSpan.FromSeconds(20)); var later = harness.Matchmaking.Poll(ticket.QueueId, "p1")!; Assert.Equal(300, later.Window); // 100 + 4 * 50 harness.Time.Advance(TimeSpan.FromSeconds(200)); var capped = harness.Matchmaking.Poll(ticket.QueueId, "p1")!; Assert.Equal(2000, capped.Window); } [Fact] public void Invalid_Deck_Is_Rejected_At_Enqueue() { var harness = NewHarness(); var deck = ServerTestData.BuildDeck(); deck.RemoveAt(0); var exception = Assert.Throws( () => harness.Matchmaking.Enqueue("p1", deck, GameMode.ClassicRanked)); Assert.Contains(exception.Violations, violation => violation.Code == "deck.size"); } [Fact] public void Deck_Without_Starting_Vanguard_Is_Rejected() { var harness = NewHarness(); var deck = new List(); foreach (var printing in ServerTestData.Catalog.Printings .Where(printing => !printing.IsParallel) .OrderBy(printing => printing.PrintingId, StringComparer.Ordinal)) { if (deck.Count >= 50) break; var card = ServerTestData.Catalog.CardOf(printing.PrintingId); if (card.Grade == 0 || card.IsSentinel || card.Trigger == Vanguard.Rules.Cards.TriggerType.Heal) continue; for (var i = 0; i < 4 && deck.Count < 50; i++) deck.Add(printing.PrintingId); } Assert.Equal(50, deck.Count); var exception = Assert.Throws( () => harness.Matchmaking.Enqueue("p1", deck, GameMode.ClassicRanked)); Assert.Contains(exception.Violations, violation => violation.Code == "deck.starting-vanguard"); } [Fact] public void Cancel_Removes_From_Queue() { var harness = NewHarness(); var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); Assert.True(harness.Matchmaking.Cancel(ticket.QueueId, "p1")); var status = harness.Matchmaking.Poll(ticket.QueueId, "p1")!; Assert.Equal("cancelled", status.Status); Assert.Equal(0, harness.Matchmaking.QueuedCount); Assert.False(harness.Matchmaking.Cancel(ticket.QueueId, "p1")); } [Fact] public void Second_Queue_Request_Replaces_The_First() { var harness = NewHarness(); var old = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); var current = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); Assert.Equal("cancelled", harness.Matchmaking.Poll(old.QueueId, "p1")!.Status); Assert.Equal("queued", harness.Matchmaking.Poll(current.QueueId, "p1")!.Status); Assert.Equal(1, harness.Matchmaking.QueuedCount); } [Fact] public void Queue_Expires_After_Timeout() { var harness = NewHarness(); var ticket = harness.Matchmaking.Enqueue("p1", ServerTestData.BuildDeck(), GameMode.ClassicRanked); harness.Time.Advance(TimeSpan.FromSeconds(harness.Options.QueueExpirySeconds + 1)); harness.Matchmaking.Matchmake(harness.Time.GetUtcNow()); Assert.Equal("expired", harness.Matchmaking.Poll(ticket.QueueId, "p1")!.Status); Assert.Equal(0, harness.Matchmaking.QueuedCount); } [Fact] public async Task Ranked_Result_Changes_Hidden_Mmr_Casual_Does_Not() { var profiles = new PlayerProfileStore(); await profiles.RecordAsync(new MatchResult( "m1", GameMode.ClassicRanked, "winner", "loser", 0)); Assert.True(profiles.GetMmr("winner") > 1000); Assert.True(profiles.GetMmr("loser") < 1000); var winnerAfterRanked = profiles.GetMmr("winner"); var loserAfterRanked = profiles.GetMmr("loser"); await profiles.RecordAsync(new MatchResult( "m2", GameMode.ClassicCasual, "winner", "loser", 0)); Assert.Equal(winnerAfterRanked, profiles.GetMmr("winner")); Assert.Equal(loserAfterRanked, profiles.GetMmr("loser")); } private static Harness NewHarness() { var time = new FakeTimeProvider(new DateTimeOffset(2026, 10, 21, 12, 0, 0, TimeSpan.Zero)); var options = ServerTestData.SmallOptions(); var profiles = new PlayerProfileStore(); var tickets = new GameTicketService(options, time); var services = new ServiceCollection(); services.AddSingleton(ServerTestData.Catalog); services.AddSingleton(options); services.AddSingleton(time); services.AddSingleton(profiles); services.AddLogging(); var provider = services.BuildServiceProvider(); var registry = new MatchGrainRegistry(provider, options, NullLogger.Instance); var matchmaking = new MatchmakingService( ServerTestData.Catalog, registry, tickets, profiles, options, time, NullLogger.Instance); return new Harness(time, options, profiles, tickets, registry, matchmaking); } private sealed record Harness( FakeTimeProvider Time, GameOptions Options, PlayerProfileStore Profiles, GameTicketService Tickets, MatchGrainRegistry Registry, MatchmakingService Matchmaking); }