using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Crafting;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Crafting;

/// <summary>Regression tests for <c>POST …/craft</c> (NEO-70).</summary>
public sealed class PlayerCraftApiTests
{
    private const string PlayerId = "dev-local-1";

    private static PlayerCraftRequest Request(
        string recipeId,
        int? quantity = null,
        int schemaVersion = PlayerCraftRequest.CurrentSchemaVersion) =>
        new()
        {
            SchemaVersion = schemaVersion,
            RecipeId = recipeId,
            Quantity = quantity,
        };

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

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/missing-player/craft",
            Request("refine_scrap_standard"));

        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

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

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("refine_scrap_standard", schemaVersion: 99));

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

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

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            new PlayerCraftRequest { SchemaVersion = PlayerCraftRequest.CurrentSchemaVersion, RecipeId = "   " });

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task PostCraft_RefineScrapStandard_ShouldReturnSuccessEnvelope()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        SeedStack(factory, "scrap_metal_bulk", 5);

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("refine_scrap_standard"));

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.Equal(CraftResponse.CurrentSchemaVersion, body!.SchemaVersion);
        Assert.True(body.Success);
        Assert.Null(body.ReasonCode);
        Assert.Single(body.InputsConsumed);
        Assert.Equal("scrap_metal_bulk", body.InputsConsumed[0].ItemId);
        Assert.Equal(5, body.InputsConsumed[0].Quantity);
        Assert.Single(body.OutputsGranted);
        Assert.Equal("refined_plate_stock", body.OutputsGranted[0].ItemId);
        Assert.Equal(1, body.OutputsGranted[0].Quantity);
        Assert.NotNull(body.XpGrantSummary);
        Assert.Equal("refine", body.XpGrantSummary!.SkillId);
        Assert.Equal(RefineSkillXpConstants.ActivityXpPerCraftRefineCompletion, body.XpGrantSummary.Amount);
        Assert.Equal(RefineSkillXpConstants.ActivitySourceKind, body.XpGrantSummary.SourceKind);
        var json = await response.Content.ReadAsStringAsync();
        Assert.Contains("\"reasonCode\":null", json, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PostCraft_WhenQuantityOmitted_ShouldDefaultToOneAndSucceed()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        SeedStack(factory, "scrap_metal_bulk", 5);

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            new { schemaVersion = PlayerCraftRequest.CurrentSchemaVersion, recipeId = "refine_scrap_standard" });

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.True(body!.Success);
        Assert.Single(body.InputsConsumed);
        Assert.Equal(5, body.InputsConsumed[0].Quantity);
        Assert.Single(body.OutputsGranted);
        Assert.Equal(1, body.OutputsGranted[0].Quantity);
    }

    [Fact]
    public async Task PostCraft_WhenMaterialsMissing_ShouldDenyWithInsufficientMaterials()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var beforeInventory = await client.GetFromJsonAsync<PlayerInventorySnapshotResponse>(
            $"/game/players/{PlayerId}/inventory");

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("refine_scrap_standard"));

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.False(body!.Success);
        Assert.Equal(CraftReasonCodes.InsufficientMaterials, body.ReasonCode);
        Assert.Empty(body.InputsConsumed);
        Assert.Empty(body.OutputsGranted);
        Assert.Null(body.XpGrantSummary);
        var afterInventory = await client.GetFromJsonAsync<PlayerInventorySnapshotResponse>(
            $"/game/players/{PlayerId}/inventory");
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
    }

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

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("not_a_real_recipe"));

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.False(body!.Success);
        Assert.Equal(CraftReasonCodes.UnknownRecipe, body.ReasonCode);
        Assert.Empty(body.InputsConsumed);
        Assert.Empty(body.OutputsGranted);
        Assert.Null(body.XpGrantSummary);
        var json = await response.Content.ReadAsStringAsync();
        Assert.Contains("\"reasonCode\":\"unknown_recipe\"", json, StringComparison.Ordinal);
        Assert.Contains("\"xpGrantSummary\":null", json, StringComparison.Ordinal);
    }

    [Fact]
    public async Task PostCraft_WhenQuantityInvalid_ShouldDenyWithInvalidQuantity()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        SeedStack(factory, "scrap_metal_bulk", 5);

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("refine_scrap_standard", quantity: 0));

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.False(body!.Success);
        Assert.Equal(CraftReasonCodes.InvalidQuantity, body.ReasonCode);
        Assert.Equal(5, CountBagItem(factory, "scrap_metal_bulk"));
    }

    [Fact]
    public async Task PostCraft_MakePrototypeArmor_WhenEquipmentOccupied_ShouldDenyWithInventoryFull()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        SeedStack(factory, "refined_plate_stock", 8);
        SeedStack(factory, "scrap_metal_bulk", 5);
        SeedStack(factory, "prototype_armor_shell", 1);
        var beforeInventory = await client.GetFromJsonAsync<PlayerInventorySnapshotResponse>(
            $"/game/players/{PlayerId}/inventory");

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("make_prototype_armor"));

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<CraftResponse>();
        Assert.NotNull(body);
        Assert.False(body!.Success);
        Assert.Equal(CraftReasonCodes.InventoryFull, body.ReasonCode);
        var afterInventory = await client.GetFromJsonAsync<PlayerInventorySnapshotResponse>(
            $"/game/players/{PlayerId}/inventory");
        Assert.Equal(TotalBagQuantity(beforeInventory!), TotalBagQuantity(afterInventory!));
    }

    [Fact]
    public async Task PostCraft_WhenInventoryStoreMissing_ShouldReturnNotFound()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory().WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                for (var i = services.Count - 1; i >= 0; i--)
                {
                    if (services[i].ServiceType == typeof(IPlayerInventoryStore))
                    {
                        services.RemoveAt(i);
                    }
                }

                services.AddSingleton<IPlayerInventoryStore, InventoryStoreAlwaysMissing>();
            });
        });
        var client = factory.CreateClient();

        // Act
        var response = await client.PostAsJsonAsync(
            $"/game/players/{PlayerId}/craft",
            Request("refine_scrap_standard"));

        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    private static void SeedStack(InMemoryWebApplicationFactory factory, string itemId, int quantity)
    {
        var itemRegistry = factory.Services.GetRequiredService<IItemDefinitionRegistry>();
        var inventoryStore = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        var outcome = PlayerInventoryOperations.TryAddStack(
            PlayerId,
            itemId,
            quantity,
            itemRegistry,
            inventoryStore);
        Assert.Equal(PlayerInventoryMutationKind.Applied, outcome.Kind);
    }

    private static int CountBagItem(InMemoryWebApplicationFactory factory, string itemId)
    {
        var inventoryStore = factory.Services.GetRequiredService<IPlayerInventoryStore>();
        inventoryStore.TryGetSnapshot(PlayerId, out var snapshot);
        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(PlayerInventorySnapshotResponse snapshot)
    {
        var total = 0;
        foreach (var slot in snapshot.BagSlots)
        {
            total += slot.Quantity;
        }

        return total;
    }

    private sealed class InventoryStoreAlwaysMissing : IPlayerInventoryStore
    {
        public bool TryGetSnapshot(string playerId, out PlayerInventorySnapshot snapshot)
        {
            snapshot = PlayerInventorySnapshot.Empty();
            return false;
        }

        public bool TryReplaceSnapshot(string playerId, PlayerInventorySnapshot snapshot) => false;

        public bool TryMutateSnapshot(
            string playerId,
            Func<PlayerInventorySnapshot, PlayerInventoryMutationWrite> mutator,
            out PlayerInventorySnapshot result)
        {
            result = PlayerInventorySnapshot.Empty();
            return false;
        }
    }
}
