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.Gathering;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Gathering;

public class ResourceNodeCatalogLoaderTests
{
    private static readonly HashSet<string> ValidPrototypeItemIds =
    [
        "scrap_metal_bulk",
        "refined_plate_stock",
        "field_stim_mk0",
        "survey_drone_kit",
        "contract_handoff_token",
        "prototype_armor_shell",
    ];

    private const string ValidPrototypeNodeCatalogJson =
        """
        {
          "schemaVersion": 1,
          "nodes": [
            {
              "nodeDefId": "prototype_resource_node_alpha",
              "displayName": "Prototype Salvage Heap",
              "gatherLens": "consumer_salvage",
              "maxGathers": 10
            },
            {
              "nodeDefId": "prototype_subsurface_vein_beta",
              "displayName": "Prototype Subsurface Vein",
              "gatherLens": "subsurface",
              "maxGathers": 10
            },
            {
              "nodeDefId": "prototype_bio_mat_gamma",
              "displayName": "Prototype Bio Mat",
              "gatherLens": "bio",
              "maxGathers": 10
            },
            {
              "nodeDefId": "prototype_urban_bulk_delta",
              "displayName": "Prototype Urban Bulk",
              "gatherLens": "urban_bulk",
              "maxGathers": 10
            }
          ]
        }
        """;

    private const string ValidPrototypeYieldCatalogJson =
        """
        {
          "schemaVersion": 1,
          "yields": [
            {
              "nodeDefId": "prototype_resource_node_alpha",
              "itemId": "scrap_metal_bulk",
              "quantity": 1
            },
            {
              "nodeDefId": "prototype_subsurface_vein_beta",
              "itemId": "scrap_metal_bulk",
              "quantity": 2
            },
            {
              "nodeDefId": "prototype_bio_mat_gamma",
              "itemId": "scrap_metal_bulk",
              "quantity": 3
            },
            {
              "nodeDefId": "prototype_urban_bulk_delta",
              "itemId": "scrap_metal_bulk",
              "quantity": 5
            }
          ]
        }
        """;

    private static (string Root, string ResourceNodesDir, string NodeSchemaPath, string YieldSchemaPath) CreateTempContentLayout()
    {
        var root = Directory.CreateTempSubdirectory("neon-sprawl-resnodecat-");
        var resourceNodesDir = Path.Combine(root.FullName, "content", "resource-nodes");
        var schemaDir = Path.Combine(root.FullName, "content", "schemas");
        Directory.CreateDirectory(resourceNodesDir);
        Directory.CreateDirectory(schemaDir);
        var nodeSchemaPath = Path.Combine(schemaDir, "resource-node-def.schema.json");
        var yieldSchemaPath = Path.Combine(schemaDir, "resource-yield-row.schema.json");
        File.Copy(ResourceNodeCatalogTestPaths.DiscoverRepoResourceNodeDefSchemaPath(), nodeSchemaPath, overwrite: true);
        File.Copy(ResourceNodeCatalogTestPaths.DiscoverRepoResourceYieldRowSchemaPath(), yieldSchemaPath, overwrite: true);
        return (root.FullName, resourceNodesDir, nodeSchemaPath, yieldSchemaPath);
    }

    private static void WriteNodeCatalog(string resourceNodesDir, string catalogJson) =>
        File.WriteAllText(Path.Combine(resourceNodesDir, "prototype_resource_nodes.json"), catalogJson, Encoding.UTF8);

    private static void WriteYieldCatalog(string resourceNodesDir, string catalogJson) =>
        File.WriteAllText(Path.Combine(resourceNodesDir, "prototype_resource_yields.json"), catalogJson, Encoding.UTF8);

    private static void WriteValidCatalogs(string resourceNodesDir)
    {
        WriteNodeCatalog(resourceNodesDir, ValidPrototypeNodeCatalogJson);
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
    }

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

        throw new InvalidOperationException($"nodeDefId not found: {nodeDefId}");
    }

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        // Act
        var catalog = ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance);
        // Assert
        Assert.Equal(4, catalog.DistinctNodeCount);
        Assert.Equal(4, catalog.DistinctYieldCount);
        Assert.Equal(1, catalog.NodeCatalogJsonFileCount);
        Assert.Equal(1, catalog.YieldCatalogJsonFileCount);
        Assert.True(catalog.TryGetNode("prototype_resource_node_alpha", out var node));
        Assert.NotNull(node);
        Assert.Equal("consumer_salvage", node!.GatherLens);
        Assert.Equal(10, node.MaxGathers);
        Assert.True(catalog.TryGetYield("prototype_resource_node_alpha", out var yield));
        Assert.NotNull(yield);
        Assert.Equal("scrap_metal_bulk", yield!.ItemId);
        Assert.Equal(1, yield.Quantity);
        Assert.True(catalog.TryGetYield("prototype_urban_bulk_delta", out var deltaYield));
        Assert.Equal(5, deltaYield!.Quantity);
    }

    [Fact]
    public void Load_ShouldThrow_WhenNodesIsNotArray()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteNodeCatalog(resourceNodesDir, """{"schemaVersion": 1, "nodes": "nope"}""");
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expected top-level 'nodes' array", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenYieldsIsNotArray()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteNodeCatalog(resourceNodesDir, ValidPrototypeNodeCatalogJson);
        WriteYieldCatalog(resourceNodesDir, """{"schemaVersion": 1, "yields": "nope"}""");
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expected top-level 'yields' array", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSchemaVersionIsNotOne()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteNodeCatalog(resourceNodesDir, """{"schemaVersion": 2, "nodes": []}""");
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expected schemaVersion 1", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenNodeRowViolatesSchema()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        const string bad = """
                  {
                    "schemaVersion": 1,
                    "nodes": [
                      {
                        "nodeDefId": "prototype_resource_node_alpha",
                        "displayName": "",
                        "gatherLens": "consumer_salvage",
                        "maxGathers": 10
                      }
                    ]
                  }
                  """;
        WriteNodeCatalog(resourceNodesDir, bad);
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("Resource node catalog validation failed", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateNodeDefIdAcrossFiles()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        const string singleNode = """
                         {
                           "schemaVersion": 1,
                           "nodes": [
                             {
                               "nodeDefId": "prototype_resource_node_alpha",
                               "displayName": "A",
                               "gatherLens": "consumer_salvage",
                               "maxGathers": 10
                             }
                           ]
                         }
                         """;
        File.WriteAllText(Path.Combine(resourceNodesDir, "a_resource_nodes.json"), singleNode, Encoding.UTF8);
        File.WriteAllText(Path.Combine(resourceNodesDir, "b_resource_nodes.json"), singleNode, Encoding.UTF8);
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate nodeDefId", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("prototype_resource_node_alpha", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice2NodeIdsIncomplete()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        const string oneOnly = """
                      {
                        "schemaVersion": 1,
                        "nodes": [
                          {
                            "nodeDefId": "prototype_resource_node_alpha",
                            "displayName": "A",
                            "gatherLens": "consumer_salvage",
                            "maxGathers": 10
                          }
                        ]
                      }
                      """;
        WriteNodeCatalog(resourceNodesDir, oneOnly);
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype Slice 2 expects exactly nodeDefIds", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice2GatherLensesDoNotMatch()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeNodeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetNodeRow(root, "prototype_subsurface_vein_beta")["gatherLens"] = "consumer_salvage";
        WriteNodeCatalog(resourceNodesDir, root.ToJsonString());
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("requires exactly one row per gatherLens", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenYieldNodeDefIdNotInNodes()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        var yieldRoot = JsonNode.Parse(ValidPrototypeYieldCatalogJson) as JsonObject
                        ?? throw new InvalidOperationException("expected object root");
        var yields = yieldRoot["yields"] as JsonArray ?? throw new InvalidOperationException("expected yields");
        yields.Add(JsonNode.Parse("""{"nodeDefId": "unknown_node_xyz", "itemId": "scrap_metal_bulk", "quantity": 1}"""));
        WriteYieldCatalog(resourceNodesDir, yieldRoot.ToJsonString());
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("unknown_node_xyz", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("not defined in resource node catalogs", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenYieldItemIdUnknown()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        var yieldRoot = JsonNode.Parse(ValidPrototypeYieldCatalogJson) as JsonObject
                        ?? throw new InvalidOperationException("expected object root");
        var yields = yieldRoot["yields"] as JsonArray ?? throw new InvalidOperationException("expected yields");
        yields[0]!["itemId"] = "not_a_real_item";
        WriteYieldCatalog(resourceNodesDir, yieldRoot.ToJsonString());
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("not_a_real_item", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("not in item catalogs", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateYieldNodeDefId()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        const string dupYield = """
                         {
                           "schemaVersion": 1,
                           "yields": [
                             {
                               "nodeDefId": "prototype_resource_node_alpha",
                               "itemId": "scrap_metal_bulk",
                               "quantity": 1
                             }
                           ]
                         }
                         """;
        File.WriteAllText(Path.Combine(resourceNodesDir, "a_resource_yields.json"), dupYield, Encoding.UTF8);
        File.WriteAllText(Path.Combine(resourceNodesDir, "b_resource_yields.json"), dupYield, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate yield nodeDefId", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice2YieldRowsIncomplete()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        const string oneYield = """
                         {
                           "schemaVersion": 1,
                           "yields": [
                             {
                               "nodeDefId": "prototype_resource_node_alpha",
                               "itemId": "scrap_metal_bulk",
                               "quantity": 1
                             }
                           ]
                         }
                         """;
        WriteYieldCatalog(resourceNodesDir, oneYield);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expects exactly one yield row per nodeDefId", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice2YieldItemIdNotAllowed()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        var yieldRoot = JsonNode.Parse(ValidPrototypeYieldCatalogJson) as JsonObject
                        ?? throw new InvalidOperationException("expected object root");
        var yields = yieldRoot["yields"] as JsonArray ?? throw new InvalidOperationException("expected yields");
        yields[0]!["itemId"] = "refined_plate_stock";
        WriteYieldCatalog(resourceNodesDir, yieldRoot.ToJsonString());
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("expects yield itemIds", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("scrap_metal_bulk", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenResourceNodesDirectoryMissing()
    {
        // Arrange
        var missingDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-no-resnodes-" + Guid.NewGuid().ToString("n"));
        var nodeSchemaPath = ResourceNodeCatalogTestPaths.DiscoverRepoResourceNodeDefSchemaPath();
        var yieldSchemaPath = ResourceNodeCatalogTestPaths.DiscoverRepoResourceYieldRowSchemaPath();
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            missingDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            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_WhenNoResourceNodesJsonFiles()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteYieldCatalog(resourceNodesDir, ValidPrototypeYieldCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("no *_resource_nodes.json files", ioe.Message, StringComparison.Ordinal);
        Assert.Contains(resourceNodesDir, ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenNoResourceYieldsJsonFiles()
    {
        // Arrange
        var (_, resourceNodesDir, nodeSchemaPath, yieldSchemaPath) = CreateTempContentLayout();
        WriteNodeCatalog(resourceNodesDir, ValidPrototypeNodeCatalogJson);
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            nodeSchemaPath,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("no *_resource_yields.json files", ioe.Message, StringComparison.Ordinal);
        Assert.Contains(resourceNodesDir, ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenNodeSchemaFileMissing()
    {
        // Arrange
        var (_, resourceNodesDir, _, yieldSchemaPath) = CreateTempContentLayout();
        WriteValidCatalogs(resourceNodesDir);
        var missingSchema = Path.Combine(resourceNodesDir, "missing-node.schema.json");
        // Act
        var ex = Record.Exception(() => ResourceNodeCatalogLoader.Load(
            resourceNodesDir,
            missingSchema,
            yieldSchemaPath,
            ValidPrototypeItemIds,
            NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds()
    {
        // Arrange
        using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        // Act
        var catalog = factory.Services.GetRequiredService<ResourceNodeCatalog>();
        // Assert
        Assert.Equal(4, catalog.DistinctNodeCount);
        Assert.True(catalog.TryGetNode("prototype_bio_mat_gamma", out var node));
        Assert.Equal("bio", node!.GatherLens);
        Assert.True(catalog.TryGetYield("prototype_bio_mat_gamma", out var yield));
        Assert.Equal(3, yield!.Quantity);
    }

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