using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Services; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Services; public class RewardGrantServiceTests { [Test] public async Task Sleeve_added_to_viewer_collection() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const int testSleeveId = 2_000_000_000; var sleeve = new SleeveEntry { Id = testSleeveId }; ctx.Sleeves.Add(sleeve); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers.Include(v => v.Sleeves).FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Sleeve, detailId: testSleeveId, num: 1); await ctx.SaveChangesAsync(); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(viewer.Sleeves.Any(s => s.Id == testSleeveId), Is.True); Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Sleeve)); Assert.That(result[0].RewardId, Is.EqualTo((long)testSleeveId)); Assert.That(result[0].RewardNum, Is.EqualTo(1)); } [Test] public async Task Rupy_sets_currency_post_state_total() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId); viewer.Currency.Rupees = 100UL; await ctx.SaveChangesAsync(); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Rupy, detailId: 0, num: 50); await ctx.SaveChangesAsync(); Assert.That(viewer.Currency.Rupees, Is.EqualTo(150UL)); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].RewardNum, Is.EqualTo(150)); } [Test] public async Task LeaderSkin_added_idempotently() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const int testSkinId = 9_999_999; ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "Round1Reward" }); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); await svc.ApplyAsync(viewer, UserGoodsType.Skin, testSkinId, 1); await svc.ApplyAsync(viewer, UserGoodsType.Skin, testSkinId, 1); await ctx.SaveChangesAsync(); Assert.That(viewer.LeaderSkins.Count(s => s.Id == testSkinId), Is.EqualTo(1)); } [Test] public async Task Card_fresh_grant_inserts_owned_entry_and_returns_post_state_count() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long testCardId = 999_001_001L; ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "RGSTestCard", Rarity = Rarity.Bronze }); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1); await ctx.SaveChangesAsync(); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Card)); Assert.That(result[0].RewardId, Is.EqualTo(testCardId)); Assert.That(result[0].RewardNum, Is.EqualTo(1)); Assert.That(viewer.Cards.Single(c => c.Card.Id == testCardId).Count, Is.EqualTo(1)); } [Test] public async Task Card_existing_grant_increments_count() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long testCardId = 999_001_002L; var card = new ShadowverseCardEntry { Id = testCardId, Name = "RGSTestCard2", Rarity = Rarity.Bronze }; ctx.Cards.Add(card); var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = 2, IsProtected = false }); await ctx.SaveChangesAsync(); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1); await ctx.SaveChangesAsync(); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].RewardNum, Is.EqualTo(3)); Assert.That(viewer.Cards.Single(c => c.Card.Id == testCardId).Count, Is.EqualTo(3)); } [Test] public async Task Card_with_cascade_rows_emits_card_plus_cosmetics() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long testCardId = 999_002_010L; const int testSkinId = 999_002_011; ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "CascadeTestCard", Rarity = Rarity.Gold }); ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "CascadeTestSkin" }); ctx.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = testCardId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1, }); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .Include(v => v.LeaderSkins) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1); await ctx.SaveChangesAsync(); Assert.That(result, Has.Count.EqualTo(2)); Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Card && r.RewardId == testCardId), Is.True); Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Skin && r.RewardId == testSkinId), Is.True); Assert.That(viewer.LeaderSkins.Any(s => s.Id == testSkinId), Is.True); } [Test] public async Task Card_cascade_skips_already_owned_cosmetic() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long testCardId = 999_002_020L; const int testSkinId = 999_002_021; ctx.Cards.Add(new ShadowverseCardEntry { Id = testCardId, Name = "CascadeOwnedTestCard", Rarity = Rarity.Gold }); var skin = new LeaderSkinEntry { Id = testSkinId, Name = "CascadeOwnedTestSkin" }; ctx.LeaderSkins.Add(skin); ctx.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = testCardId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1, }); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .Include(v => v.LeaderSkins) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId); viewer.LeaderSkins.Add(skin); await ctx.SaveChangesAsync(); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, testCardId, 1); await ctx.SaveChangesAsync(); Assert.That(result, Has.Count.EqualTo(1)); Assert.That(result[0].RewardType, Is.EqualTo((int)UserGoodsType.Card)); Assert.That(result[0].RewardId, Is.EqualTo(testCardId)); } [Test] public async Task Card_foil_grant_resolves_cascade_to_non_foil_id() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long nonFoilId = 999_002_030L; const long foilId = 999_002_031L; const int testSkinId = 999_002_032; ctx.Cards.Add(new ShadowverseCardEntry { Id = nonFoilId, Name = "FoilCascadeBase", Rarity = Rarity.Gold }); ctx.Cards.Add(new ShadowverseCardEntry { Id = foilId, Name = "FoilCascadeFoil", Rarity = Rarity.Gold, IsFoil = true }); ctx.LeaderSkins.Add(new LeaderSkinEntry { Id = testSkinId, Name = "FoilCascadeSkin" }); ctx.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = nonFoilId, Type = CosmeticType.Skin, CosmeticId = testSkinId, Quantity = 1, }); await ctx.SaveChangesAsync(); var viewer = await ctx.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .Include(v => v.LeaderSkins) .AsSplitQuery() .FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); var result = await svc.ApplyAsync(viewer, UserGoodsType.Card, foilId, 1); await ctx.SaveChangesAsync(); Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Card && r.RewardId == foilId), Is.True); Assert.That(result.Any(r => r.RewardType == (int)UserGoodsType.Skin && r.RewardId == testSkinId), Is.True); } [Test] public async Task SpotCard_still_throws_NotSupported() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId); var svc = scope.ServiceProvider.GetRequiredService(); Assert.ThrowsAsync(async () => await svc.ApplyAsync(viewer, UserGoodsType.SpotCard, 1L, 1)); Assert.ThrowsAsync(async () => await svc.ApplyAsync(viewer, UserGoodsType.SpotCardOnlyLatestCardPack, 1L, 1)); } [Test] public async Task OwnedCardEntry_unique_index_blocks_duplicate_viewer_card_row() { // Schema-level safety net: any code that forgets to .Include(v => v.Cards) before doing // a find-or-add OwnedCardEntry would silently insert a duplicate row otherwise. The // unique index on (ViewerId, CardId) makes that crash loudly at SaveChanges instead. using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var ctx = scope.ServiceProvider.GetRequiredService(); const long testCardId = 999_003_001L; var card = new ShadowverseCardEntry { Id = testCardId, Name = "UniqueIdxTest", Rarity = Rarity.Bronze }; ctx.Cards.Add(card); var viewer = await ctx.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = 1, IsProtected = false }); await ctx.SaveChangesAsync(); // Simulate the bug: a fresh viewer load WITHOUT .Include(v => v.Cards), then a manual // Add of a second row for the same (Viewer, Card). The unique index must reject this. using var scope2 = factory.Services.CreateScope(); var ctx2 = scope2.ServiceProvider.GetRequiredService(); var unloadedViewer = await ctx2.Viewers.FirstAsync(v => v.Id == viewerId); var sameCard = await ctx2.Cards.FirstAsync(c => c.Id == testCardId); unloadedViewer.Cards.Add(new OwnedCardEntry { Card = sameCard, Count = 1, IsProtected = false }); Assert.ThrowsAsync(async () => await ctx2.SaveChangesAsync()); } }