using System.Linq;
using System.Net;
using System.Net.Http.Json;
using NeonSprawl.Server.Game.Gathering;
using NeonSprawl.Server.Game.Interaction;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Game.World;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Interaction;

/// <summary>NEO-63: <c>resource_node</c> interact delegates to <see cref="GatherOperations"/>.</summary>
public sealed class InteractionResourceNodeGatherIntegrationTests
{
    [Fact]
    public async Task PostInteract_ResourceNode_WhenInRange_ShouldGrantInventoryAndSalvageXp()
    {
        // Arrange — node at (12, -6), radius 3; stand at (10, -6) → horizontal distance 2
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await MoveNearAlphaAsync(client);

        // Act
        var interact = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/interact",
            new InteractionRequest
            {
                SchemaVersion = InteractionRequest.CurrentSchemaVersion,
                InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
            });
        var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
        var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");

        // Assert
        Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
        var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
        Assert.NotNull(envelope);
        Assert.True(envelope!.Allowed);
        Assert.Equal(PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, envelope.InteractableId);

        Assert.Equal(HttpStatusCode.OK, inventory.StatusCode);
        var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
        Assert.NotNull(bag);
        Assert.Equal(1, CountItem(bag!, "scrap_metal_bulk"));

        Assert.Equal(HttpStatusCode.OK, progression.StatusCode);
        var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
        Assert.NotNull(skills);
        var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
        Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, salvage.Xp);
    }

    [Fact]
    public async Task PostInteract_ResourceNode_WhenDepleted_ShouldDenyWithNodeDepletedWithoutGrants()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await MoveNearAlphaAsync(client);
        var request = new InteractionRequest
        {
            SchemaVersion = InteractionRequest.CurrentSchemaVersion,
            InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
        };

        // Act — exhaust capacity, then one more interact
        var successfulGathers = new InteractionResponse[10];
        for (var i = 0; i < successfulGathers.Length; i++)
        {
            var gather = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request);
            successfulGathers[i] = (await gather.Content.ReadFromJsonAsync<InteractionResponse>())!;
        }

        var depletedInteract = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request);
        var inventoryAfter = await client.GetAsync("/game/players/dev-local-1/inventory");
        var progressionAfter = await client.GetAsync("/game/players/dev-local-1/skill-progression");

        // Assert
        Assert.All(successfulGathers, static r => Assert.True(r.Allowed));
        Assert.Equal(HttpStatusCode.OK, depletedInteract.StatusCode);
        var envelope = await depletedInteract.Content.ReadFromJsonAsync<InteractionResponse>();
        Assert.NotNull(envelope);
        Assert.False(envelope!.Allowed);
        Assert.Equal(GatherReasonCodes.NodeDepleted, envelope.ReasonCode);
        Assert.Equal(10, CountItem(
            (await inventoryAfter.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>())!,
            "scrap_metal_bulk"));
        Assert.Equal(
            10 * GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
            (await progressionAfter.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>())!
                .Skills!.Single(static s => s.Id == "salvage").Xp);
    }

    [Fact]
    public async Task PostInteract_ResourceNode_WhenBagFull_ShouldDenyWithInventoryFullWithoutDepletingNode()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
        {
            await client.PostAsJsonAsync(
                "/game/players/dev-local-1/inventory",
                new PlayerInventoryMutationRequest
                {
                    SchemaVersion = PlayerInventoryMutationRequest.CurrentSchemaVersion,
                    MutationKind = "add",
                    ItemId = "survey_drone_kit",
                    Quantity = 1,
                });
        }

        await MoveNearAlphaAsync(client);
        var inventoryBefore = await client.GetAsync("/game/players/dev-local-1/inventory");
        var progressionBefore = await client.GetAsync("/game/players/dev-local-1/skill-progression");
        var scrapBefore = CountItem(
            (await inventoryBefore.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>())!,
            "scrap_metal_bulk");
        var salvageBefore = (await progressionBefore.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>())!
            .Skills!.Single(static s => s.Id == "salvage").Xp;

        // Act
        var interact = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/interact",
            new InteractionRequest
            {
                SchemaVersion = InteractionRequest.CurrentSchemaVersion,
                InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
            });

        // Assert
        Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
        var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
        Assert.NotNull(envelope);
        Assert.False(envelope!.Allowed);
        Assert.Equal(GatherReasonCodes.InventoryFull, envelope.ReasonCode);
        Assert.Equal(
            scrapBefore,
            CountItem(
                (await (await client.GetAsync("/game/players/dev-local-1/inventory")).Content
                    .ReadFromJsonAsync<PlayerInventorySnapshotResponse>())!,
                "scrap_metal_bulk"));
        Assert.Equal(
            salvageBefore,
            (await (await client.GetAsync("/game/players/dev-local-1/skill-progression")).Content
                .ReadFromJsonAsync<SkillProgressionSnapshotResponse>())!
                .Skills!.Single(static s => s.Id == "salvage").Xp);
    }

    [Fact]
    public async Task PostInteract_PrototypeTerminal_ShouldNotGrantInventoryOrSalvageXp()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var move = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 0.5, Y = 0.9, Z = 0.5 },
        };
        await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);

        // Act
        var interact = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/interact",
            new InteractionRequest
            {
                SchemaVersion = InteractionRequest.CurrentSchemaVersion,
                InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
            });
        var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
        var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");

        // Assert
        Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
        var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
        Assert.NotNull(envelope);
        Assert.True(envelope!.Allowed);

        var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
        Assert.NotNull(bag);
        Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));

        var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
        Assert.NotNull(skills);
        var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
        Assert.Equal(0, salvage.Xp);
    }

    [Fact]
    public async Task PostInteract_ResourceNode_WhenSalvageDisallowsActivity_ShouldDenyWithoutGrants()
    {
        // Arrange
        await using var factory = new SalvageActivityDeniedRegistryWebApplicationFactory();
        var client = factory.CreateClient();
        await MoveNearAlphaAsync(client);

        // Act
        var interact = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/interact",
            new InteractionRequest
            {
                SchemaVersion = InteractionRequest.CurrentSchemaVersion,
                InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
            });
        var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
        var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");

        // Assert
        Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
        var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
        Assert.NotNull(envelope);
        Assert.False(envelope!.Allowed);
        Assert.Equal(SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed, envelope.ReasonCode);

        var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
        Assert.NotNull(bag);
        Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));

        var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
        Assert.NotNull(skills);
        var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
        Assert.Equal(0, salvage.Xp);
    }

    private static async Task MoveNearAlphaAsync(HttpClient client)
    {
        var move = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
        };
        await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
    }

    private static int CountItem(PlayerInventorySnapshotResponse 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;
    }
}
