using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Items;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Items;

public sealed class PlayerInventoryOperationsTests
{
    [Fact]
    public void TryAddStack_ToEmptyBag_ShouldPlaceInFirstSlot()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "scrap_metal_bulk",
            quantity: 10,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
        Assert.NotNull(outcome.Snapshot);
        Assert.Equal("scrap_metal_bulk", outcome.Snapshot!.BagSlots[0].ItemId);
        Assert.Equal(10, outcome.Snapshot.BagSlots[0].Quantity);
    }

    [Fact]
    public void TryAddStack_ShouldMergeIntoPartialStackRespectingStackMax()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 15, registry, store);

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
        Assert.Equal(20, CountItem(outcome.Snapshot!, "field_stim_mk0"));
        Assert.Equal(1, OccupiedBagSlotCount(outcome.Snapshot!));
    }

    [Fact]
    public void TryAddStack_WhenBagFull_ShouldDenyWithInventoryFull()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
        {
            var add = PlayerInventoryOperations.TryAddStack(
                "dev-local-1",
                "survey_drone_kit",
                quantity: 1,
                registry,
                store);
            Assert.Equal(PlayerInventoryMutationKind.Applied, add.Kind);
        }

        store.TryGetSnapshot("dev-local-1", out var before);

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "scrap_metal_bulk",
            quantity: 1,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode);
        store.TryGetSnapshot("dev-local-1", out var after);
        Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!));
    }

    [Fact]
    public void TryAddStack_ToEquipment_ShouldUseEquipmentContainer()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "prototype_armor_shell",
            quantity: 1,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
        Assert.Equal("prototype_armor_shell", outcome.Snapshot!.EquipmentSlots[0].ItemId);
        Assert.Equal(1, outcome.Snapshot.EquipmentSlots[0].Quantity);
        Assert.All(outcome.Snapshot.BagSlots, static s => Assert.True(s.IsEmpty));
    }

    [Fact]
    public void TryAddStack_WhenEquipmentOccupied_ShouldDenyWithInventoryFull()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        _ = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "prototype_armor_shell",
            quantity: 1,
            registry,
            store);

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "prototype_armor_shell",
            quantity: 1,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InventoryFull, outcome.ReasonCode);
    }

    [Fact]
    public void TryAddStack_ForUnknownItem_ShouldDenyWithInvalidItem()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "nonexistent_item_id",
            quantity: 1,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InvalidItem, outcome.ReasonCode);
    }

    [Fact]
    public void TryAddStack_ForNonPositiveQuantity_ShouldDenyWithInvalidQuantity()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "scrap_metal_bulk",
            quantity: 0,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode);
    }

    [Fact]
    public void TryRemoveStack_ShouldDrainLowestSlotIndexFirst()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 20, registry, store);
        _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "field_stim_mk0", 5, registry, store);

        // Act
        var outcome = PlayerInventoryOperations.TryRemoveStack(
            "dev-local-1",
            "field_stim_mk0",
            quantity: 20,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
        Assert.True(outcome.Snapshot!.BagSlots[0].IsEmpty);
        Assert.Equal("field_stim_mk0", outcome.Snapshot.BagSlots[1].ItemId);
        Assert.Equal(5, outcome.Snapshot.BagSlots[1].Quantity);
    }

    [Fact]
    public void TryRemoveStack_WhenInsufficientQuantity_ShouldDenyWithoutMutation()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        _ = PlayerInventoryOperations.TryAddStack("dev-local-1", "scrap_metal_bulk", 5, registry, store);
        store.TryGetSnapshot("dev-local-1", out var before);

        // Act
        var outcome = PlayerInventoryOperations.TryRemoveStack(
            "dev-local-1",
            "scrap_metal_bulk",
            quantity: 6,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InsufficientQuantity, outcome.ReasonCode);
        store.TryGetSnapshot("dev-local-1", out var after);
        Assert.Equal(TotalBagQuantity(before!), TotalBagQuantity(after!));
    }

    [Fact]
    public void TryAddStack_WhenQuantityExceedsStackMax_ShouldSpanMultipleEmptySlots()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "dev-local-1",
            "field_stim_mk0",
            quantity: 41,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
        Assert.Equal(41, CountItem(outcome.Snapshot!, "field_stim_mk0"));
        Assert.Equal(3, OccupiedBagSlotCount(outcome.Snapshot!));
        Assert.Equal(20, outcome.Snapshot!.BagSlots[0].Quantity);
        Assert.Equal(20, outcome.Snapshot.BagSlots[1].Quantity);
        Assert.Equal(1, outcome.Snapshot.BagSlots[2].Quantity);
    }

    [Fact]
    public void TryAddStack_ForUnknownPlayer_ShouldReturnStoreMissing()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "unknown-player-xyz",
            "scrap_metal_bulk",
            quantity: 1,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.StoreMissing, outcome.Kind);
        Assert.Null(outcome.ReasonCode);
        Assert.Null(outcome.Snapshot);
    }

    [Fact]
    public void TryAddStack_ForUnknownPlayer_WithInvalidQuantity_ShouldDenyWithReasonCode()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        var registry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var store = factory.Services.GetRequiredService<IPlayerInventoryStore>();

        // Act
        var outcome = PlayerInventoryOperations.TryAddStack(
            "unknown-player-xyz",
            "scrap_metal_bulk",
            quantity: 0,
            registry,
            store);

        // Assert
        Assert.Equal(PlayerInventoryMutationKind.Denied, outcome.Kind);
        Assert.Equal(PlayerInventoryReasonCodes.InvalidQuantity, outcome.ReasonCode);
        Assert.Null(outcome.Snapshot);
    }

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

        return total;
    }

    private static int OccupiedBagSlotCount(PlayerInventorySnapshot snapshot)
    {
        var count = 0;
        foreach (var slot in snapshot.BagSlots)
        {
            if (!slot.IsEmpty)
            {
                count++;
            }
        }

        return count;
    }

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

        return total;
    }
}
