using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SVSim.Database; using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Models.Config; using SVSim.Database.Repositories.Card; using SVSim.Database.Repositories.Collectibles; using SVSim.Database.Services; using SVSim.EmulatedEntrypoint.Services; using SVSim.UnitTests.Infrastructure; namespace SVSim.UnitTests.Services; public class ViewerEntitlementsTests { /// /// FreeplayConfig is in SVSim.Database so EnsureSeedDataAsync seeds a DB row with /// Enabled=false (ShippedDefaults). Since tier 1 (DB) wins, we mutate the seeded row /// to activate freeplay rather than relying on an IConfiguration override. /// private static void SetFreeplayEnabled(SVSimDbContext db, bool enabled, ulong currencyAmount = 99999, int cardCopies = 3) { var row = db.GameConfigs.First(s => s.SectionName == "Freeplay"); var cfg = JsonSerializer.Deserialize(row.ValueJson)!; cfg.Enabled = enabled; cfg.CurrencyAmount = currencyAmount; cfg.CardCopies = cardCopies; row.ValueJson = JsonSerializer.Serialize(cfg); db.SaveChanges(); } private static ViewerEntitlements Build(IServiceScope scope) { var db = scope.ServiceProvider.GetRequiredService(); return new ViewerEntitlements( new GameConfigService(db, new ConfigurationBuilder().Build()), new CardRepository(db), new CollectionRepository(db)); } [Test] public async Task Freeplay_off_reflects_real_balance_and_ownership() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Freeplay is seeded as Enabled=false by default — no mutation needed. var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); viewer.Currency.Crystals = 7; var ent = Build(scope); Assert.That(ent.IsFreeplay, Is.False); Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Crystal), Is.EqualTo(7)); Assert.That(ent.OwnsCard(viewer, 99_999_999L), Is.False); Assert.That(ent.OwnsCosmetic(viewer, CosmeticType.Skin, 99_999), Is.False); } [Test] public async Task Freeplay_on_inflates_main_currencies_but_not_spot_points() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); SetFreeplayEnabled(db, enabled: true, currencyAmount: 99999); var viewer = await db.Viewers.FirstAsync(v => v.Id == viewerId); viewer.Currency.SpotPoints = 5; var ent = Build(scope); Assert.That(ent.IsFreeplay, Is.True); Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Crystal), Is.EqualTo(99999)); Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.Rupee), Is.EqualTo(99999)); Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.RedEther), Is.EqualTo(99999)); Assert.That(ent.EffectiveBalance(viewer, SpendCurrency.SpotPoint), Is.EqualTo(5), "spot points are not a freeplay-inflated currency"); } [Test] public async Task Freeplay_on_treats_all_cards_and_cosmetics_as_owned() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); SetFreeplayEnabled(db, enabled: true); var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).FirstAsync(v => v.Id == viewerId); var ent = Build(scope); Assert.That(ent.OwnsCard(viewer, 99_999_999L), Is.True); Assert.That(ent.OwnsCosmetic(viewer, CosmeticType.Skin, 99_999), Is.True); } // ------------------------------------------------------------------------- // EffectiveOwnedCardsAsync // ------------------------------------------------------------------------- [Test] public async Task EffectiveOwnedCards_freeplay_on_returns_all_collectible_cards_at_card_copies() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); // Seed one collectible card owned by this viewer (gives it a CollectionInfo). await factory.SeedOwnedCardAsync(viewerId, 50001001L, count: 1); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); SetFreeplayEnabled(db, enabled: true, cardCopies: 3); var viewer = await db.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .FirstAsync(v => v.Id == viewerId); var ent = Build(scope); var result = await ent.EffectiveOwnedCardsAsync(viewer); // Freeplay returns the whole collectible catalog — card 50001001 must be present. Assert.That(result.Any(e => e.Card.Id == 50001001L), Is.True, "seeded collectible card must appear in freeplay result"); // Every returned entry must have Count == CardCopies (3). Assert.That(result.All(e => e.Count == 3), Is.True, "every freeplay entry should have Count == CardCopies (3)"); // The full set == every collectible card in the DB. int collectibleCount = db.Cards.Count(c => c.CollectionInfo != null); Assert.That(result.Count, Is.EqualTo(collectibleCount), "freeplay result should contain exactly all collectible cards"); } [Test] public async Task EffectiveOwnedCards_freeplay_off_returns_only_owned() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); // Seed card 50001002 owned at count 2. await factory.SeedOwnedCardAsync(viewerId, 50001002L, count: 2); // Seed a second collectible card (50001003) NOT owned by the viewer — insert card row // only (with CollectionInfo so it's collectible) but do not link it to the viewer. using (var setupScope = factory.Services.CreateScope()) { var setupDb = setupScope.ServiceProvider.GetRequiredService(); if (!setupDb.Cards.Any(c => c.Id == 50001003L)) { setupDb.Cards.Add(new ShadowverseCardEntry { Id = 50001003L, Name = "UnownedCollectible", Rarity = SVSim.Database.Enums.Rarity.Bronze, CollectionInfo = new SVSim.Database.Models.CardCollectionInfo { CraftCost = 200, DustReward = 50 }, }); await setupDb.SaveChangesAsync(); } } using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Freeplay is off by default — no mutation needed. var viewer = await db.Viewers .Include(v => v.Cards).ThenInclude(c => c.Card) .FirstAsync(v => v.Id == viewerId); var ent = Build(scope); var result = await ent.EffectiveOwnedCardsAsync(viewer); // The owned card must be present at the right count. var owned = result.FirstOrDefault(e => e.Card.Id == 50001002L); Assert.That(owned, Is.Not.Null, "owned card should appear in result"); Assert.That(owned!.Count, Is.EqualTo(2)); // The unowned collectible card must NOT appear. Assert.That(result.Any(e => e.Card.Id == 50001003L), Is.False, "card not owned by viewer must not appear when freeplay is off"); } // ------------------------------------------------------------------------- // EffectiveCosmeticsAsync // ------------------------------------------------------------------------- [Test] public async Task EffectiveCosmetics_leader_skins_always_full_catalog_owned_set_differs() { using var factory = new SVSimTestFactory(); long viewerId = await factory.SeedViewerAsync(); // --- freeplay OFF: fresh viewer owns no cosmetics --- { using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Freeplay off by default. var viewer = await db.Viewers .Include(v => v.LeaderSkins) .Include(v => v.Sleeves) .Include(v => v.Emblems) .Include(v => v.Degrees) .Include(v => v.MyPageBackgrounds) .FirstAsync(v => v.Id == viewerId); var ent = Build(scope); var cosmetics = await ent.EffectiveCosmeticsAsync(viewer); int masterSkinCount = db.LeaderSkins.Count(); Assert.That(masterSkinCount, Is.GreaterThan(0), "leaderskins.csv must have been imported — master table must be non-empty"); Assert.That(cosmetics.AllLeaderSkins.Count, Is.EqualTo(masterSkinCount), "AllLeaderSkins should always be the full catalog regardless of freeplay"); // A fresh viewer owns one default skin per class (granted at registration). // Assert the owned set matches what the viewer actually has — don't assume empty. var expectedOwnedSkinIds = viewer.LeaderSkins.Select(s => s.Id).ToHashSet(); Assert.That(cosmetics.OwnedLeaderSkinIds, Is.EquivalentTo(expectedOwnedSkinIds), "OwnedLeaderSkinIds should match the viewer's actual owned skins when freeplay is off"); // OwnedLeaderSkinIds must be a strict subset of AllLeaderSkins (not all of them). Assert.That(cosmetics.OwnedLeaderSkinIds.Count, Is.LessThan(masterSkinCount), "fresh viewer should own fewer skins than the full catalog"); // The four id-lists reflect what the viewer actually owns. Assert.That(cosmetics.SleeveIds.Count, Is.EqualTo(viewer.Sleeves.Count)); Assert.That(cosmetics.EmblemIds.Count, Is.EqualTo(viewer.Emblems.Count)); Assert.That(cosmetics.DegreeIds.Count, Is.EqualTo(viewer.Degrees.Count)); Assert.That(cosmetics.MyPageBackgroundIds.Count, Is.EqualTo(viewer.MyPageBackgrounds.Count)); } // --- freeplay ON: all catalogs become owned --- { using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); SetFreeplayEnabled(db, enabled: true); var viewer = await db.Viewers .Include(v => v.LeaderSkins) .Include(v => v.Sleeves) .Include(v => v.Emblems) .Include(v => v.Degrees) .Include(v => v.MyPageBackgrounds) .FirstAsync(v => v.Id == viewerId); var ent = Build(scope); var cosmetics = await ent.EffectiveCosmeticsAsync(viewer); int masterSkinCount = db.LeaderSkins.Count(); Assert.That(cosmetics.AllLeaderSkins.Count, Is.EqualTo(masterSkinCount), "AllLeaderSkins count unchanged when freeplay is on"); Assert.That(cosmetics.OwnedLeaderSkinIds.Count, Is.EqualTo(masterSkinCount), "freeplay: every skin id must be in OwnedLeaderSkinIds"); // All four id-lists should equal the full catalog counts. Assert.That(cosmetics.SleeveIds.Count, Is.EqualTo(db.Sleeves.Count()), "freeplay: SleeveIds should equal full sleeve catalog"); Assert.That(cosmetics.EmblemIds.Count, Is.EqualTo(db.Emblems.Count()), "freeplay: EmblemIds should equal full emblem catalog"); Assert.That(cosmetics.DegreeIds.Count, Is.EqualTo(db.Degrees.Count()), "freeplay: DegreeIds should equal full degree catalog"); Assert.That(cosmetics.MyPageBackgroundIds.Count, Is.EqualTo(db.MyPageBackgrounds.Count()), "freeplay: MyPageBackgroundIds should equal full my-page-background catalog"); } } }