using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Repositories.Globals; using SVSim.Database.Repositories.Viewer; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Services; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Services; public class ArenaTwoPickServiceDraftTests { private sealed class FakePool : IArenaTwoPickCardPoolService { public List GeneratePickSetsForTurn(int classId, int turn, long startingPairId, IRandom rng) => new() { new() { Id = startingPairId, Turn = turn, SetNum = 1, CardId1 = 1000 + turn * 10 + 1, CardId2 = 1000 + turn * 10 + 2, IsSelected = false }, new() { Id = startingPairId + 1, Turn = turn, SetNum = 2, CardId1 = 2000 + turn * 10 + 1, CardId2 = 2000 + turn * 10 + 2, IsSelected = false }, }; } private sealed class FakeEntitlements : IViewerEntitlements { public bool IsFreeplay { get; init; } public long EffectiveBalance(SVSim.Database.Models.Viewer viewer, SpendCurrency currency) => 0; public bool OwnsCard(SVSim.Database.Models.Viewer viewer, long cardId) => IsFreeplay; public bool OwnsCosmetic(SVSim.Database.Models.Viewer viewer, CosmeticType type, int id) => IsFreeplay; public Task> EffectiveOwnedCardsAsync(SVSim.Database.Models.Viewer viewer, CancellationToken ct = default) => Task.FromResult>(new List()); public Task EffectiveCosmeticsAsync(SVSim.Database.Models.Viewer viewer, CancellationToken ct = default) => throw new NotSupportedException(); } private static async Task<(IArenaTwoPickService, IArenaTwoPickRunRepository, long viewerId)> SetupWithActiveRunAsync(int classChosen = 0) { var factory = new SVSimTestFactory(); var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(); var viewer = new SVSim.Database.Models.Viewer { Id = 7, DisplayName = "v", Currency = new ViewerCurrency() }; db.Viewers.Add(viewer); await db.SaveChangesAsync(); var runs = new ArenaTwoPickRunRepository(db); await runs.UpsertAsync(new ViewerArenaTwoPickRun { ViewerId = 7, EntryId = 4242, CandidateClassIdsJson = "[1,7,8]", ClassId = classChosen, MaxBattleCount = 7, SelectTurn = classChosen == 0 ? 0 : 1, PendingPickSetsJson = classChosen == 0 ? "[]" : JsonSerializer.Serialize(new List { new() { Id = 1, Turn = 1, SetNum = 1, CardId1 = 11, CardId2 = 12 }, new() { Id = 2, Turn = 1, SetNum = 2, CardId1 = 21, CardId2 = 22 }, }), NextCandidateId = classChosen == 0 ? 1 : 3, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, }); var svc = new ArenaTwoPickService( runs, scope.ServiceProvider.GetRequiredService(), new FakePool(), scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService(), new FakeEntitlements(), new SystemRandom(seed: 1), db, scope.ServiceProvider.GetRequiredService()); return (svc, runs, 7); } [Test] public async Task ChooseClassAsync_persists_class_and_emits_first_pick_sets() { var (svc, runs, vid) = await SetupWithActiveRunAsync(); var dto = await svc.ChooseClassAsync(vid, classId: 7); Assert.That(dto.ClassInfo.SelectedClassId, Is.EqualTo(7)); Assert.That(dto.DeckInfo.SelectTurn, Is.EqualTo(1)); Assert.That(dto.DeckInfo.IsSelectCompleted, Is.False); Assert.That(dto.CandidateCardList.Count, Is.EqualTo(2)); Assert.That(dto.CandidateCardList[0].Id, Is.EqualTo(1)); Assert.That(dto.CandidateCardList[1].Id, Is.EqualTo(2)); var row = await runs.GetByViewerIdAsync(vid); Assert.That(row!.ClassId, Is.EqualTo(7)); Assert.That(row.NextCandidateId, Is.EqualTo(3)); } [Test] public async Task ChooseClassAsync_rejects_class_not_offered() { var (svc, _, vid) = await SetupWithActiveRunAsync(); var ex = Assert.ThrowsAsync(() => svc.ChooseClassAsync(vid, classId: 4)); Assert.That(ex!.ErrorCode, Is.EqualTo("arena_two_pick_class_not_offered")); } [Test] public async Task ChooseClassAsync_rejects_when_class_already_chosen() { var (svc, _, vid) = await SetupWithActiveRunAsync(classChosen: 1); var ex = Assert.ThrowsAsync(() => svc.ChooseClassAsync(vid, classId: 1)); Assert.That(ex!.ErrorCode, Is.EqualTo("arena_two_pick_invalid_state")); } [Test] public async Task ChooseCardAsync_appends_two_cards_and_advances_turn() { var (svc, runs, vid) = await SetupWithActiveRunAsync(classChosen: 1); var dto = await svc.ChooseCardAsync(vid, selectedId: 2); Assert.That(dto.DeckInfo.SelectedCardIds, Is.EqualTo(new List { 21, 22 })); Assert.That(dto.DeckInfo.SelectTurn, Is.EqualTo(2)); Assert.That(dto.CandidateCardList!.Count, Is.EqualTo(2)); var row = await runs.GetByViewerIdAsync(vid); Assert.That(row!.NextCandidateId, Is.EqualTo(5)); } [Test] public async Task ChooseCardAsync_at_turn_15_completes_the_draft_and_omits_pick_list() { var (svc, runs, vid) = await SetupWithActiveRunAsync(classChosen: 1); // Fast-forward to turn 15 by writing the row directly. var row = await runs.GetByViewerIdAsync(vid); row!.SelectTurn = 15; var pending = new List { new() { Id = 100, Turn = 15, SetNum = 1, CardId1 = 71, CardId2 = 72 }, new() { Id = 101, Turn = 15, SetNum = 2, CardId1 = 81, CardId2 = 82 }, }; row.PendingPickSetsJson = JsonSerializer.Serialize(pending); row.SelectedCardIdsJson = JsonSerializer.Serialize(Enumerable.Range(1, 28).Select(i => (long)i).ToList()); await runs.UpsertAsync(row); var dto = await svc.ChooseCardAsync(vid, selectedId: 100); Assert.That(dto.DeckInfo.IsSelectCompleted, Is.True); Assert.That(dto.DeckInfo.SelectedCardIds.Count, Is.EqualTo(30)); Assert.That(dto.CandidateCardList, Is.Null, "omitted on completion per spec"); } [Test] public async Task ChooseCardAsync_rejects_invalid_selected_id() { var (svc, _, vid) = await SetupWithActiveRunAsync(classChosen: 1); var ex = Assert.ThrowsAsync(() => svc.ChooseCardAsync(vid, selectedId: 999)); Assert.That(ex!.ErrorCode, Is.EqualTo("arena_two_pick_invalid_selection")); } }