using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Crafting;
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.Crafting;

public sealed class CraftOperationsTests
{
    private const string PlayerId = "dev-local-1";

    [Fact]
    public void TryCraft_RefineScrapStandard_ShouldConsumeInputsGrantOutputAndRefineXp()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "scrap_metal_bulk", 5);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        var refineXp = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("refine");
        Assert.True(result.Success);
        Assert.Null(result.ReasonCode);
        Assert.Single(result.InputsConsumed);
        Assert.Equal("scrap_metal_bulk", result.InputsConsumed[0].ItemId);
        Assert.Equal(5, result.InputsConsumed[0].Quantity);
        Assert.Single(result.OutputsGranted);
        Assert.Equal("refined_plate_stock", result.OutputsGranted[0].ItemId);
        Assert.Equal(1, result.OutputsGranted[0].Quantity);
        Assert.NotNull(result.XpGrantSummary);
        Assert.Equal("refine", result.XpGrantSummary!.Value.SkillId);
        Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, result.XpGrantSummary.Value.Amount);
        Assert.Equal(RefineSkillXpConstants.ActivitySourceKind, result.XpGrantSummary.Value.SourceKind);
        Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
        Assert.Equal(1, CountBagItem(inventory!, "refined_plate_stock"));
        Assert.Equal(10, refineXp);
    }

    [Fact]
    public void TryCraft_RefineScrapStandard_AtQuantityTwo_ShouldScaleInputsAndOutputs()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "scrap_metal_bulk", 10);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity: 2,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        Assert.True(result.Success);
        Assert.Single(result.InputsConsumed);
        Assert.Equal(10, result.InputsConsumed[0].Quantity);
        Assert.Single(result.OutputsGranted);
        Assert.Equal(2, result.OutputsGranted[0].Quantity);
        Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
        Assert.Equal(2, CountBagItem(inventory!, "refined_plate_stock"));
    }

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

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        var refineXp = deps.XpStore.GetXpTotals(PlayerId).GetValueOrDefault("refine");
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.InsufficientMaterials, result.ReasonCode);
        Assert.Empty(result.InputsConsumed);
        Assert.Empty(result.OutputsGranted);
        Assert.Null(result.XpGrantSummary);
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
        Assert.Equal(0, refineXp);
    }

    [Fact]
    public void TryCraft_WhenBagFullWithScrapInputs_ShouldSucceedByFreeingInputSlots()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "scrap_metal_bulk", 5);
        FillBagExcept(deps, occupiedSlots: 1);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        Assert.True(result.Success);
        Assert.Equal(0, CountBagItem(inventory!, "scrap_metal_bulk"));
        Assert.Equal(1, CountBagItem(inventory!, "refined_plate_stock"));
    }

    [Fact]
    public void TryCraft_MakePrototypeArmor_WhenEquipmentOccupied_ShouldDenyWithInventoryFull()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "refined_plate_stock", 8);
        SeedStack(deps, "scrap_metal_bulk", 5);
        SeedStack(deps, "prototype_armor_shell", 1);
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "make_prototype_armor",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.InventoryFull, result.ReasonCode);
        Assert.Equal(8, CountBagItem(afterInventory!, "refined_plate_stock"));
        Assert.Equal(5, CountBagItem(afterInventory!, "scrap_metal_bulk"));
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
    }

    [Fact]
    public void TryCraft_MakePrototypeArmor_WhenBagFullButEquipmentEmpty_ShouldPlaceArmorInEquipmentSlot()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "refined_plate_stock", 8);
        SeedStack(deps, "scrap_metal_bulk", 5);
        FillBagExcept(deps, occupiedSlots: 2);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "make_prototype_armor",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        Assert.True(result.Success);
        Assert.Equal("prototype_armor_shell", inventory!.EquipmentSlots[0].ItemId);
        Assert.Equal(1, inventory.EquipmentSlots[0].Quantity);
        Assert.Equal(0, CountBagItem(inventory, "refined_plate_stock"));
        Assert.Equal(0, CountBagItem(inventory, "scrap_metal_bulk"));
    }

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

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "not_a_real_recipe",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.UnknownRecipe, result.ReasonCode);
    }

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

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "   ",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.UnknownRecipe, result.ReasonCode);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public void TryCraft_WhenQuantityInvalid_ShouldDenyWithInvalidQuantity(int quantity)
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "scrap_metal_bulk", 5);

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            deps.XpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.InvalidQuantity, result.ReasonCode);
        Assert.Equal(5, CountBagItem(inventory!, "scrap_metal_bulk"));
    }

    [Fact]
    public void TryCraft_WhenProgressionStoreMissing_ShouldRollbackInventoryAndDeny()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var deps = ResolveCraftDependencies(factory);
        SeedStack(deps, "scrap_metal_bulk", 5);
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
        var failingXpStore = new ProgressionStoreAlwaysMissing();

        // Act
        var result = CraftOperations.TryCraft(
            PlayerId,
            "refine_scrap_standard",
            quantity: 1,
            deps.RecipeRegistry,
            deps.ItemRegistry,
            deps.InventoryStore,
            deps.SkillRegistry,
            failingXpStore,
            deps.LevelCurve,
            deps.PerkUnlockEngine);

        // Assert
        deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
        Assert.False(result.Success);
        Assert.Equal(CraftReasonCodes.ProgressionStoreMissing, result.ReasonCode);
        Assert.Equal(5, CountBagItem(afterInventory!, "scrap_metal_bulk"));
        Assert.Equal(0, CountBagItem(afterInventory!, "refined_plate_stock"));
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
    }

    private static void SeedStack(CraftTestDependencies deps, string itemId, int quantity)
    {
        var outcome = PlayerInventoryOperations.TryAddStack(
            PlayerId,
            itemId,
            quantity,
            deps.ItemRegistry,
            deps.InventoryStore);
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
    }

    private static void FillBagExcept(CraftTestDependencies deps, int occupiedSlots)
    {
        for (var i = occupiedSlots; i < PlayerInventorySnapshot.BagSlotCount; i++)
        {
            var add = PlayerInventoryOperations.TryAddStack(
                PlayerId,
                "survey_drone_kit",
                quantity: 1,
                deps.ItemRegistry,
                deps.InventoryStore);
            Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
        }
    }

    private static CraftTestDependencies ResolveCraftDependencies(InMemoryWebApplicationFactory factory)
    {
        var services = factory.Services;
        return new CraftTestDependencies(
            services.GetRequiredService<IRecipeDefinitionRegistry>(),
            services.GetRequiredService<IItemDefinitionRegistry>(),
            services.GetRequiredService<IPlayerInventoryStore>(),
            services.GetRequiredService<ISkillDefinitionRegistry>(),
            services.GetRequiredService<IPlayerSkillProgressionStore>(),
            services.GetRequiredService<ISkillLevelCurve>(),
            services.GetRequiredService<PerkUnlockEngine>());
    }

    private static int CountBagItem(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 CraftTestDependencies(
        IRecipeDefinitionRegistry RecipeRegistry,
        IItemDefinitionRegistry ItemRegistry,
        IPlayerInventoryStore InventoryStore,
        ISkillDefinitionRegistry SkillRegistry,
        IPlayerSkillProgressionStore XpStore,
        ISkillLevelCurve LevelCurve,
        PerkUnlockEngine PerkUnlockEngine);

    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;
    }
}
