using System.Net;
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Items;

public class ItemDefinitionCatalogLoaderTests
{
    private const string ValidPrototypeCatalogJson =
        """
        {
          "schemaVersion": 1,
          "items": [
            {
              "id": "scrap_metal_bulk",
              "displayName": "Scrap Metal (Bulk)",
              "prototypeRole": "material",
              "stackMax": 999,
              "inventorySlotKind": "bag"
            },
            {
              "id": "refined_plate_stock",
              "displayName": "Refined Plate Stock",
              "prototypeRole": "intermediate",
              "stackMax": 999,
              "inventorySlotKind": "bag"
            },
            {
              "id": "field_stim_mk0",
              "displayName": "Field Stim Mk0",
              "prototypeRole": "consumable",
              "stackMax": 20,
              "inventorySlotKind": "bag"
            },
            {
              "id": "survey_drone_kit",
              "displayName": "Survey Drone Kit",
              "prototypeRole": "utility",
              "stackMax": 1,
              "inventorySlotKind": "bag"
            },
            {
              "id": "contract_handoff_token",
              "displayName": "Contract Handoff Token",
              "prototypeRole": "quest_token",
              "stackMax": 1,
              "inventorySlotKind": "bag"
            },
            {
              "id": "prototype_armor_shell",
              "displayName": "Prototype Armor Shell",
              "prototypeRole": "equip_stub",
              "stackMax": 1,
              "inventorySlotKind": "equipment"
            }
          ]
        }
        """;

    private static (string Root, string ItemsDir, string SchemaPath) CreateTempContentLayout()
    {
        var root = Directory.CreateTempSubdirectory("neon-sprawl-itemcat-");
        var itemsDir = Path.Combine(root.FullName, "content", "items");
        var schemaDir = Path.Combine(root.FullName, "content", "schemas");
        Directory.CreateDirectory(itemsDir);
        Directory.CreateDirectory(schemaDir);
        var schemaPath = Path.Combine(schemaDir, "item-def.schema.json");
        File.Copy(ItemCatalogTestPaths.DiscoverRepoItemDefSchemaPath(), schemaPath, overwrite: true);
        return (root.FullName, itemsDir, schemaPath);
    }

    private static void WriteCatalog(string itemsDir, string catalogJson) =>
        File.WriteAllText(Path.Combine(itemsDir, "prototype_items.json"), catalogJson, Encoding.UTF8);

    private static JsonObject GetItemRow(JsonObject catalogRoot, string itemId)
    {
        var items = catalogRoot["items"] as JsonArray
                    ?? throw new InvalidOperationException("expected items array");
        foreach (var node in items)
        {
            if (node is JsonObject row && row["id"]?.GetValue<string>() == itemId)
                return row;
        }

        throw new InvalidOperationException($"item id not found: {itemId}");
    }

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(Path.Combine(itemsDir, "prototype_items.json"), ValidPrototypeCatalogJson, Encoding.UTF8);
        // Act
        var catalog = ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance);
        // Assert
        Assert.Equal(6, catalog.DistinctItemCount);
        Assert.Equal(1, catalog.CatalogJsonFileCount);
        Assert.True(catalog.TryGetItem("scrap_metal_bulk", out var row));
        Assert.NotNull(row);
        Assert.Equal("material", row!.PrototypeRole);
        Assert.Equal(999, row.StackMax);
    }

    [Fact]
    public void Load_ShouldThrow_WhenItemsIsNotArray()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(
            Path.Combine(itemsDir, "bad_items.json"),
            """{"schemaVersion": 1, "items": "nope"}""",
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_items.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("expected top-level 'items' array", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSchemaVersionIsNotOne()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(
            Path.Combine(itemsDir, "bad_items.json"),
            """{"schemaVersion": 2, "items": []}""",
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expected schemaVersion 1", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenRowViolatesSchema()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        const string bad = """
                  {
                    "schemaVersion": 1,
                    "items": [
                      {
                        "id": "scrap_metal_bulk",
                        "displayName": "",
                        "prototypeRole": "material",
                        "stackMax": 999,
                        "inventorySlotKind": "bag"
                      }
                    ]
                  }
                  """;
        File.WriteAllText(Path.Combine(itemsDir, "bad_items.json"), bad, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_items.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("Item catalog validation failed", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateIdAcrossFiles()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        const string singleItem = """
                         {
                           "schemaVersion": 1,
                           "items": [
                             {
                               "id": "scrap_metal_bulk",
                               "displayName": "Scrap Metal (Bulk)",
                               "prototypeRole": "material",
                               "stackMax": 999,
                               "inventorySlotKind": "bag"
                             }
                           ]
                         }
                         """;
        File.WriteAllText(Path.Combine(itemsDir, "a_items.json"), singleItem, Encoding.UTF8);
        File.WriteAllText(Path.Combine(itemsDir, "b_items.json"), singleItem, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate item id", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("scrap_metal_bulk", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("a_items.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("b_items.json", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice1IdsIncomplete()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        const string twoOnly = """
                      {
                        "schemaVersion": 1,
                        "items": [
                          {
                            "id": "scrap_metal_bulk",
                            "displayName": "Scrap Metal (Bulk)",
                            "prototypeRole": "material",
                            "stackMax": 999,
                            "inventorySlotKind": "bag"
                          },
                          {
                            "id": "refined_plate_stock",
                            "displayName": "Refined Plate Stock",
                            "prototypeRole": "intermediate",
                            "stackMax": 999,
                            "inventorySlotKind": "bag"
                          }
                        ]
                      }
                      """;
        File.WriteAllText(Path.Combine(itemsDir, "partial_items.json"), twoOnly, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype Slice 1 expects exactly item ids", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenEquipStubUsesBagSlot()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        var badEquip = ValidPrototypeCatalogJson.Replace(
            "\"inventorySlotKind\": \"equipment\"",
            "\"inventorySlotKind\": \"bag\"",
            StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(itemsDir, "prototype_items.json"), badEquip, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("equip_stub", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("inventorySlotKind 'equipment'", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice1PrototypeRolesDoNotMatch()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetItemRow(root, "refined_plate_stock")["prototypeRole"] = "material";
        WriteCatalog(itemsDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("requires exactly one row per prototypeRole", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenEquipStubStackMaxIsNotOne()
    {
        // Arrange
        var (_, itemsDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetItemRow(root, "prototype_armor_shell")["stackMax"] = 5;
        WriteCatalog(itemsDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype_armor_shell", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("stackMax 1", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenItemsDirectoryMissing()
    {
        // Arrange
        var missingDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-no-items-" + Guid.NewGuid().ToString("n"));
        var schemaPath = ItemCatalogTestPaths.DiscoverRepoItemDefSchemaPath();
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(missingDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("missing directory", ioe.Message, StringComparison.Ordinal);
        Assert.Contains(missingDir, ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSchemaFileMissing()
    {
        // Arrange
        var (_, itemsDir, _) = CreateTempContentLayout();
        var missingSchema = Path.Combine(itemsDir, "missing-item-def.schema.json");
        // Act
        var ex = Record.Exception(() => ItemDefinitionCatalogLoader.Load(itemsDir, missingSchema, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
        Assert.Contains(missingSchema, ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        using var client = factory.CreateClient();
        // Act
        var response = await client.GetAsync("/health");
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var catalog = factory.Services.GetRequiredService<ItemDefinitionCatalog>();
        Assert.Equal(6, catalog.DistinctItemCount);
        Assert.True(catalog.TryGetItem("prototype_armor_shell", out var shell));
        Assert.Equal("equipment", shell!.InventorySlotKind);
    }

    [Fact]
    public void Host_ShouldFailStartup_WhenCatalogDirectoryInvalid()
    {
        // Arrange
        var badDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-empty-items-" + Guid.NewGuid().ToString("n"));
        Directory.CreateDirectory(badDir);
        try
        {
            // Act
            var ex = Record.Exception(() =>
            {
                using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
                    b.UseSetting("Content:ItemsDirectory", badDir));
                factory.CreateClient();
            });
            // Assert
            Assert.NotNull(ex);
            Assert.Contains("Item catalog validation failed", ex.ToString(), StringComparison.Ordinal);
        }
        finally
        {
            if (Directory.Exists(badDir))
                Directory.Delete(badDir);
        }
    }
}
