using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.EmulatedEntrypoint.Services;
using SVSim.UnitTests.Infrastructure;

namespace SVSim.UnitTests.Services;

public class CardAcquisitionServiceTests
{
    /// <summary>
    /// Seeds a viewer (via the factory's real RegisterViewer-backed helper) and gives it the
    /// given owned cards (key = card_id, value = count). Card rows are created on-demand if
    /// the test's card_id isn't already in the minimal seeded card set (matches the pattern
    /// used by SVSimTestFactory.SeedOwnedCardAsync, but inlined so multiple cards can be
    /// seeded in one viewer in one call). Returns the viewer's Id.
    /// </summary>
    private static async Task<long> SeedViewerWithCards(
        SVSimTestFactory factory,
        Dictionary<long, int> ownedCards,
        IEnumerable<long>? grantableCardIds = null)
    {
        long viewerId = await factory.SeedViewerAsync();

        using var scope = factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
        var viewer = await db.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card)
            .FirstAsync(v => v.Id == viewerId);

        foreach (var (cardId, count) in ownedCards)
        {
            var card = await EnsureCardAsync(db, cardId);
            viewer.Cards.Add(new OwnedCardEntry { Card = card, Count = count, IsProtected = false });
        }
        // Pre-seed bare Cards rows (no ownership) for any cardIds the test plans to grant via
        // the service. RewardGrantService.ApplyAsync does FirstOrDefaultAsync on _db.Cards;
        // without the row the grant throws InvalidOperationException("Card {id} not in catalog").
        if (grantableCardIds is not null)
        {
            foreach (var cardId in grantableCardIds)
            {
                await EnsureCardAsync(db, cardId);
            }
        }
        await db.SaveChangesAsync();
        return viewerId;
    }

    private static async Task<ShadowverseCardEntry> EnsureCardAsync(SVSimDbContext db, long cardId)
    {
        var card = await db.Cards.FirstOrDefaultAsync(c => c.Id == cardId);
        if (card is null)
        {
            // Foil twins follow the universal +1 convention (card_id ends in 1). Marking
            // IsFoil here keeps test setup tidy so foil-resolution tests don't have to
            // hand-patch the card row.
            var isFoil = cardId % 10 == 1;
            card = new ShadowverseCardEntry { Id = cardId, Name = $"SeededCard{cardId}", Rarity = Database.Enums.Rarity.Bronze, IsFoil = isFoil };
            db.Cards.Add(card);
            await db.SaveChangesAsync();
        }
        return card;
    }

    private static ICardAcquisitionService GetService(SVSimTestFactory factory)
    {
        var scope = factory.Services.CreateScope();
        return scope.ServiceProvider.GetRequiredService<ICardAcquisitionService>();
    }

    [Test]
    public async Task GrantManyAsync_NewBronzeCard_GrantsCardOnly()
    {
        // 101111010 is a synthetic test card (inserted ad-hoc via grantableCardIds) with no
        // CardCosmeticReward associations. Expectation: grant returns only the type=5 entry.
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 101111010L });

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 101111010L });

        Assert.That(result.RewardList, Has.Count.EqualTo(1));
        Assert.That(result.RewardList[0].RewardType, Is.EqualTo(5));   // Card
        Assert.That(result.RewardList[0].RewardId, Is.EqualTo(101111010L));
        Assert.That(result.RewardList[0].RewardNum, Is.EqualTo(1));    // post-state count
    }

    [Test]
    public async Task GrantManyAsync_LeaderCard_GrantsCardAndSkin()
    {
        // Card 704741010 (Aria leader-card variant) has 3 cosmetic associations in the seed:
        // skin 407, sleeve 704741010, emblem 704741010.
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });

        // Since SqliteFriendlyModelCustomizer strips CardCosmeticReward seed in tests, insert
        // the specific mappings we need for this test.
        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            db.CardCosmeticRewards.AddRange(
                new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin,   CosmeticId = 407L,        Quantity = 1 },
                new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Sleeve, CosmeticId = 704741010L,  Quantity = 1 },
                new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Emblem, CosmeticId = 704741010L,  Quantity = 1 }
            );
            // Ensure master rows exist for the cosmetics we'll grant
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
            if (await db.Sleeves.FindAsync(704741010) is null)
                db.Sleeves.Add(new SleeveEntry { Id = 704741010 });
            if (await db.Emblems.FindAsync(704741010) is null)
                db.Emblems.Add(new EmblemEntry { Id = 704741010 });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });

        var skinEntry = result.RewardList.SingleOrDefault(r => r.RewardType == 10);
        Assert.That(skinEntry, Is.Not.Null, "expected a Skin reward entry");
        Assert.That(skinEntry!.RewardId, Is.EqualTo(407L));

        // Verify viewer ownership was actually written to DB
        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            var viewer = await db.Viewers
                .Include(v => v.LeaderSkins)
                .FirstAsync(v => v.Id == viewerId);
            Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True);
        }
    }

    [Test]
    public async Task GrantManyAsync_AlreadyOwnedSkin_OmitsFromRewardList()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            // Pre-grant the skin to this viewer
            var viewer = await db.Viewers.Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
            var skin = await db.LeaderSkins.FindAsync(407) ?? db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" }).Entity;
            if (!viewer.LeaderSkins.Any(s => s.Id == 407))
                viewer.LeaderSkins.Add(skin);
            // Seed the card→skin mapping
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });

        Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.False,
            "skin entry should be omitted since viewer already owns it");
        Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 704741010L), Is.True,
            "card grant entry should still be emitted");
    }

    [Test]
    public async Task GrantManyAsync_FoilLeaderCard_ResolvesToNonFoilCosmetics()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741011L });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            // CardCosmeticReward.CardId has a FK→Cards.Id; ensure the non-foil row exists
            // even though we never grant it directly (the foil twin is the granted card).
            await EnsureCardAsync(db, 704741010L);
            // Map cosmetics to the NON-FOIL card_id (704741010), as the seed convention requires
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 704741011L });

        var skinEntry = result.RewardList.SingleOrDefault(r => r.RewardType == 10);
        Assert.That(skinEntry, Is.Not.Null, "expected skin entry via foil resolution");
        Assert.That(skinEntry!.RewardId, Is.EqualTo(407L));

        using var scope2 = factory.Services.CreateScope();
        var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
        var viewer = await db2.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
        Assert.That(viewer.Cards.Any(c => c.Card.Id == 704741011L), Is.True, "card is the foil");
        Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True);
    }

    [Test]
    public async Task GrantManyAsync_MultipleCopiesOfSameLeader_GrantsCosmeticOnce()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 704741010L, 704741010L, 704741010L });

        Assert.That(result.RewardList.Count(r => r.RewardType == 10), Is.EqualTo(1),
            "skin should appear exactly once in reward_list");
        var cardEntry = result.RewardList.Single(r => r.RewardType == 5 && r.RewardId == 704741010L);
        Assert.That(cardEntry.RewardNum, Is.EqualTo(3), "card count should reflect all 3 copies");
    }

    [Test]
    public async Task GrantManyAsync_RecentLeaderCard_GrantsAllFiveCosmeticTypes()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 721141010L });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            // All 5 cosmetic types for this card. Exact ids: from data_dumps captures.
            db.CardCosmeticRewards.AddRange(
                new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Sleeve,   CosmeticId = 721141010L, Quantity = 1 },
                new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Emblem,   CosmeticId = 721141010L, Quantity = 1 },
                new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Degree,   CosmeticId = 120021L,    Quantity = 1 },
                new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.Skin,     CosmeticId = 4601L,      Quantity = 1 },
                new CardCosmeticReward { CardId = 721141010L, Type = CosmeticType.MyPageBG, CosmeticId = 721141010L, Quantity = 1 }
            );
            // Ensure master rows
            if (await db.LeaderSkins.FindAsync(4601) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 4601, Name = "TestSkin4601" });
            if (await db.Sleeves.FindAsync(721141010) is null)
                db.Sleeves.Add(new SleeveEntry { Id = 721141010 });
            if (await db.Emblems.FindAsync(721141010) is null)
                db.Emblems.Add(new EmblemEntry { Id = 721141010 });
            if (await db.Degrees.FindAsync(120021) is null)
                db.Degrees.Add(new DegreeEntry { Id = 120021 });
            if (await db.MyPageBackgrounds.FindAsync(721141010) is null)
                db.MyPageBackgrounds.Add(new MyPageBackgroundEntry { Id = 721141010 });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 721141010L });

        Assert.Multiple(() =>
        {
            Assert.That(result.RewardList.Any(r => r.RewardType == 6), Is.True,  "Sleeve");
            Assert.That(result.RewardList.Any(r => r.RewardType == 7), Is.True,  "Emblem");
            Assert.That(result.RewardList.Any(r => r.RewardType == 8), Is.True,  "Degree");
            Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.True, "Skin");
            Assert.That(result.RewardList.Any(r => r.RewardType == 15), Is.True, "MyPageBG");
        });
    }

    [Test]
    public async Task BackfillCosmeticsAsync_DoesNotIncrementCardCount()
    {
        using var factory = new SVSimTestFactory();
        // Pre-seed viewer with card 704741010 count=5, no skin
        var viewerId = await SeedViewerWithCards(factory, new() { [704741010L] = 5 });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.BackfillCosmeticsAsync(viewerId);

        using var scope2 = factory.Services.CreateScope();
        var db2 = scope2.ServiceProvider.GetRequiredService<SVSimDbContext>();
        var viewer = await db2.Viewers.Include(v => v.Cards).ThenInclude(c => c.Card).Include(v => v.LeaderSkins).FirstAsync(v => v.Id == viewerId);
        var owned = viewer.Cards.Single(c => c.Card.Id == 704741010L);

        Assert.That(owned.Count, Is.EqualTo(5), "card count should be unchanged in backfill mode");
        Assert.That(viewer.LeaderSkins.Any(s => s.Id == 407), Is.True, "skin should be backfilled");
        Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 407L), Is.True,
            "skin entry returned even in backfill mode");
        Assert.That(result.RewardList.Any(r => r.RewardType == 5), Is.False,
            "no type=5 card entries in backfill mode");
    }

    [Test]
    public async Task BackfillCosmeticsAsync_CalledTwice_SecondCallIsNoOp()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { [704741010L] = 1 });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });
            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var first = await service.BackfillCosmeticsAsync(viewerId);
        var second = await service.BackfillCosmeticsAsync(viewerId);

        Assert.That(first.RewardList, Is.Not.Empty, "first call should grant cosmetics");
        Assert.That(second.RewardList, Is.Empty, "second call should be a no-op");
    }

    [Test]
    public async Task GrantManyAsync_LeaderCardWithMissingMapping_GrantsCardSilently()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 701141010L });

        // NO CardCosmeticReward rows inserted for this card — simulates the 83 missing-mapping cases.

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 701141010L });

        Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 701141010L), Is.True);
        Assert.That(result.RewardList.Any(r => r.RewardType == 10), Is.False);
        // No exception means it handled the missing mapping gracefully.
    }

    [Test]
    public async Task GrantManyAsync_OrphanCosmeticReward_LogsWarningAndSkips()
    {
        using var factory = new SVSimTestFactory();
        var viewerId = await SeedViewerWithCards(factory, new() { }, grantableCardIds: new[] { 704741010L });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
            // Real skin association
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 407L, Quantity = 1 });
            if (await db.LeaderSkins.FindAsync(407) is null)
                db.LeaderSkins.Add(new LeaderSkinEntry { Id = 407, Name = "TestSkin407" });

            // ORPHAN: points to non-existent skin_id
            db.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = 704741010L, Type = CosmeticType.Skin, CosmeticId = 9999999L, Quantity = 1 });

            await db.SaveChangesAsync();
        }

        var service = GetService(factory);
        var result = await service.GrantManyAsync(viewerId, new[] { 704741010L });

        Assert.That(result.RewardList.Any(r => r.RewardType == 5 && r.RewardId == 704741010L), Is.True);
        Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 407L), Is.True,
            "real skin should still be granted");
        Assert.That(result.RewardList.Any(r => r.RewardType == 10 && r.RewardId == 9999999L), Is.False,
            "orphan cosmetic should not appear in reward_list");
    }
}
