using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Gathering;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Gathering;

public sealed class GatherOperationsTests
{
    private const string PlayerId = "dev-local-1";
    private const string AlphaId = "prototype_resource_node_alpha";
    private const string DeltaId = "prototype_urban_bulk_delta";

    [Fact]
    public void TryGather_OnFreshAlphaNode_ShouldGrantItemXpAndDecrementCapacity()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            AlphaId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        var salvageXp = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");

        // Assert
        Assert.True(result.Success);
        Assert.Null(result.ReasonCode);
        Assert.Single(result.GrantsApplied);
        Assert.Equal("scrap_metal_bulk", result.GrantsApplied[0].ItemId);
        Assert.Equal(1, result.GrantsApplied[0].Quantity);
        Assert.Equal(9, result.RemainingGathers);
        Assert.NotNull(result.XpGrantSummary);
        Assert.Equal("salvage", result.XpGrantSummary!.Value.SkillId);
        Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, result.XpGrantSummary.Value.Amount);
        Assert.Equal(GatherSkillXpConstants.ActivitySourceKind, result.XpGrantSummary.Value.SourceKind);
        Assert.Equal(1, CountItem(inventory!, "scrap_metal_bulk"));
        Assert.Equal(10, salvageXp);
    }

    [Fact]
    public void TryGather_WhenBagFull_ShouldDenyWithInventoryFullWithoutDepletingNode()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);
        for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
        {
            var add = PlayerInventoryOperations.TryAddStack(
                PlayerId,
                "survey_drone_kit",
                quantity: 1,
                deps.ItemRegistry,
                deps.InventoryStore);
            Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
        }

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
        var salvageXpBefore = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            AlphaId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");
        deps.InstanceStore.TryGetRemainingGathers(AlphaId, out var nodeSnapshot);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(GatherReasonCodes.InventoryFull, result.ReasonCode);
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
        Assert.Equal(salvageXpBefore, salvageXpAfter);
        Assert.True(deps.InstanceStore.TryGetRemainingGathers(AlphaId, out nodeSnapshot));
        Assert.Equal(10, nodeSnapshot.RemainingGathers);
    }

    [Fact]
    public void TryGather_AfterCapacityExhausted_ShouldReturnNodeDepletedWithoutSideEffects()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);
        for (var i = 0; i < 10; i++)
        {
            var gather = GatherOperations.TryGather(
                PlayerId,
                AlphaId,
                deps.NodeRegistry,
                deps.ItemRegistry,
                deps.InventoryStore,
                deps.SkillRegistry,
                deps.XpStore,
                deps.LevelCurve,
                deps.PerkUnlockEngine,
                deps.InstanceStore);
            Assert.True(gather.Success);
        }

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
        var salvageXpBefore = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            AlphaId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        var salvageXpAfter = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage");

        // Assert
        Assert.False(result.Success);
        Assert.Equal(GatherReasonCodes.NodeDepleted, result.ReasonCode);
        Assert.Equal(0, result.RemainingGathers);
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
        Assert.Equal(salvageXpBefore, salvageXpAfter);
    }

    [Fact]
    public void TryGather_ForUnknownNode_ShouldDenyWithoutCreatingInstanceRow()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);
        const string unknownId = "not_a_real_node_def";

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            unknownId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(GatherReasonCodes.UnknownNode, result.ReasonCode);
        Assert.False(rowExists);
    }

    [Fact]
    public void TryGather_WhenProgressionStoreMissing_ShouldRollbackInventoryAndDeny()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);
        var failingXpStore = new ProgressionStoreAlwaysMissing();
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            AlphaId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            failingXpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        deps.InstanceStore.TryGetRemainingGathers(AlphaId, out var nodeSnapshot);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(GatherReasonCodes.ProgressionStoreMissing, result.ReasonCode);
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
        Assert.True(deps.InstanceStore.TryGetRemainingGathers(AlphaId, out nodeSnapshot));
        Assert.Equal(10, nodeSnapshot.RemainingGathers);
    }

    [Fact]
    public void TryGather_OnDeltaNode_ShouldGrantConfiguredYieldQuantity()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveGatherDependencies(factory);

        // Act
        var result = GatherOperations.TryGather(
            PlayerId,
            DeltaId,
            deps.NodeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine,
            deps.InstanceStore);

        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);

        // Assert
        Assert.True(result.Success);
        Assert.Single(result.GrantsApplied);
        Assert.Equal(5, result.GrantsApplied[0].Quantity);
        Assert.Equal(5, CountItem(inventory!, "scrap_metal_bulk"));
    }

    private static GatherTestDependencies ResolveGatherDependencies(InMemoryWebApplicationFactory factory)
    {
        var services = factory.Services;
        return new GatherTestDependencies(
            services.GetRequiredService<IResourceNodeDefinitionRegistry>(),
            services.GetRequiredService<IItemDefinitionRegistry>(),
            services.GetRequiredService<IPlayerInventoryStore>(),
            services.GetRequiredService<ISkillDefinitionRegistry>(),
            services.GetRequiredService<IPlayerSkillProgressionStore>(),
            services.GetRequiredService<ISkillLevelCurve>(),
            services.GetRequiredService<PerkUnlockEngine>(),
            services.GetRequiredService<IResourceNodeInstanceStore>());
    }

    private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
    {
        var total = 0;
        foreach (var slot in snapshot.BagSlots)
        {
            if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
            {
                total += slot.Quantity;
            }
        }

        return total;
    }

    private static int TotalBagQuantity(PlayerInventorySnapshot snapshot)
    {
        var total = 0;
        foreach (var slot in snapshot.BagSlots)
        {
            total += slot.Quantity;
        }

        return total;
    }

    private readonly record struct GatherTestDependencies(
        IResourceNodeDefinitionRegistry NodeRegistry,
        IItemDefinitionRegistry ItemRegistry,
        IPlayerInventoryStore InventoryStore,
        ISkillDefinitionRegistry SkillRegistry,
        IPlayerSkillProgressionStore XpStore,
        ISkillLevelCurve LevelCurve,
        PerkUnlockEngine PerkUnlockEngine,
        IResourceNodeInstanceStore InstanceStore);

    private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
    {
        public IReadOnlyDictionary<string, int> GetXpTotals(string playerId) =>
            new Dictionary<string, int>(StringComparer.Ordinal);

        public bool TryApplyXpDelta(
            string playerId,
            string skillId,
            int amount,
            out int previousXp,
            out int newXp)
        {
            previousXp = 0;
            newXp = 0;
            return false;
        }

        public bool TrySetSkillXpTotal(string playerId, string skillId, int xp) => false;

        public bool TrySetSkillXpTotals(string playerId, IReadOnlyDictionary<string, int> skillXpTotals) => false;
    }
}
