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<SVSimDbContext>();

        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<RewardGrantService>();

        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<SVSimDbContext>();
        var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
        viewer.Currency.Rupees = 100UL;
        await ctx.SaveChangesAsync();

        var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();

        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<SVSimDbContext>();

        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<RewardGrantService>();

        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<SVSimDbContext>();

        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<RewardGrantService>();

        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<SVSimDbContext>();

        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<RewardGrantService>();
        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<SVSimDbContext>();

        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<RewardGrantService>();
        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<SVSimDbContext>();

        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<RewardGrantService>();
        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<SVSimDbContext>();

        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<RewardGrantService>();
        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<SVSimDbContext>();
        var viewer = await ctx.Viewers.FirstAsync(v => v.Id == viewerId);
        var svc = scope.ServiceProvider.GetRequiredService<RewardGrantService>();

        Assert.ThrowsAsync<NotSupportedException>(async () =>
            await svc.ApplyAsync(viewer, UserGoodsType.SpotCard, 1L, 1));
        Assert.ThrowsAsync<NotSupportedException>(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<SVSimDbContext>();

        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<SVSimDbContext>();
        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<DbUpdateException>(async () => await ctx2.SaveChangesAsync());
    }
}
