using System.Collections.Frozen;
using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.Crafting;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Crafting;

public class RecipeDefinitionRegistryTests
{
    private static readonly FrozenSet<string> KnownItemIds = PrototypeSlice1ItemCatalogRules.ExpectedItemIds;

    private static readonly FrozenSet<string> KnownSkillIds = FrozenSet.ToFrozenSet(
        ["salvage", "refine", "intrusion"],
        StringComparer.Ordinal);

    private static RecipeDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, RecipeDefRow> byId)
    {
        var catalog = new RecipeDefinitionCatalog("/tmp/recipes", byId, catalogJsonFileCount: 1);
        return new RecipeDefinitionRegistry(catalog);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenRefineRecipeExists()
    {
        // Arrange
        var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
        {
            ["refine_scrap_standard"] = new RecipeDefRow(
                "refine_scrap_standard",
                "Refine Scrap (Standard)",
                "process",
                "refine",
                [new RecipeIoRow("scrap_metal_bulk", 5)],
                [new RecipeIoRow("refined_plate_stock", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition("refine_scrap_standard", out var def);
        // Assert
        Assert.True(found);
        Assert.NotNull(def);
        Assert.Equal("process", def.RecipeKind);
        Assert.Equal("refine", def.RequiredSkillId);
        Assert.Equal("Refine Scrap (Standard)", def.DisplayName);
        Assert.Single(def.Inputs);
        Assert.Equal("scrap_metal_bulk", def.Inputs[0].ItemId);
        Assert.Equal(5, def.Inputs[0].Quantity);
        Assert.Single(def.Outputs);
        Assert.Equal("refined_plate_stock", def.Outputs[0].ItemId);
        Assert.Equal(1, def.Outputs[0].Quantity);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenMakeRecipeExists()
    {
        // Arrange
        var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
        {
            ["make_field_stim_mk0"] = new RecipeDefRow(
                "make_field_stim_mk0",
                "Make Field Stim Mk0",
                "make",
                "refine",
                [
                    new RecipeIoRow("refined_plate_stock", 2),
                    new RecipeIoRow("scrap_metal_bulk", 1),
                ],
                [new RecipeIoRow("field_stim_mk0", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition("make_field_stim_mk0", out var def);
        // Assert
        Assert.True(found);
        Assert.NotNull(def);
        Assert.Equal("make", def.RecipeKind);
        Assert.Equal(2, def.Inputs.Count);
        Assert.Equal("refined_plate_stock", def.Inputs[0].ItemId);
        Assert.Equal("scrap_metal_bulk", def.Inputs[1].ItemId);
        Assert.Single(def.Outputs);
        Assert.Equal("field_stim_mk0", def.Outputs[0].ItemId);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnFalse_WhenRecipeIdIsNull()
    {
        // Arrange
        var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
        {
            ["refine_scrap_standard"] = new RecipeDefRow(
                "refine_scrap_standard",
                "Refine Scrap (Standard)",
                "process",
                "refine",
                [new RecipeIoRow("scrap_metal_bulk", 5)],
                [new RecipeIoRow("refined_plate_stock", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition(null, out var def);
        // Assert
        Assert.False(found);
        Assert.Null(def);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnFalse_WhenIdUnknown()
    {
        // Arrange
        var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
        {
            ["refine_scrap_standard"] = new RecipeDefRow(
                "refine_scrap_standard",
                "Refine Scrap (Standard)",
                "process",
                "refine",
                [new RecipeIoRow("scrap_metal_bulk", 5)],
                [new RecipeIoRow("refined_plate_stock", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition("not_a_real_recipe", out var def);
        // Assert
        Assert.False(found);
        Assert.Null(def);
    }

    [Fact]
    public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleRecipes()
    {
        // Arrange
        var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
        {
            ["make_field_stim_mk0"] = new RecipeDefRow(
                "make_field_stim_mk0",
                "Make Field Stim Mk0",
                "make",
                "refine",
                [new RecipeIoRow("refined_plate_stock", 2)],
                [new RecipeIoRow("field_stim_mk0", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
            ["refine_scrap_efficient"] = new RecipeDefRow(
                "refine_scrap_efficient",
                "Refine Scrap (Efficient)",
                "process",
                "refine",
                [new RecipeIoRow("scrap_metal_bulk", 10)],
                [new RecipeIoRow("refined_plate_stock", 2)],
                RequiredSkillLevel: null,
                StationTag: null),
            ["refine_scrap_standard"] = new RecipeDefRow(
                "refine_scrap_standard",
                "Refine Scrap (Standard)",
                "process",
                "refine",
                [new RecipeIoRow("scrap_metal_bulk", 5)],
                [new RecipeIoRow("refined_plate_stock", 1)],
                RequiredSkillLevel: null,
                StationTag: null),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var list = registry.GetDefinitionsInIdOrder();
        // Assert
        Assert.Equal(3, list.Count);
        Assert.Equal("make_field_stim_mk0", list[0].Id);
        Assert.Equal("refine_scrap_efficient", list[1].Id);
        Assert.Equal("refine_scrap_standard", list[2].Id);
    }

    [Fact]
    public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
    {
        // Arrange
        var root = Directory.CreateTempSubdirectory("neon-sprawl-recipe-registry-loader-");
        try
        {
            var recipesDir = Path.Combine(root.FullName, "content", "recipes");
            var schemaDir = Path.Combine(root.FullName, "content", "schemas");
            Directory.CreateDirectory(recipesDir);
            Directory.CreateDirectory(schemaDir);
            var recipeDefSchemaPath = Path.Combine(schemaDir, "recipe-def.schema.json");
            var recipeIoRowSchemaPath = Path.Combine(schemaDir, "recipe-io-row.schema.json");
            File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeDefSchemaPath(), recipeDefSchemaPath, overwrite: true);
            File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeIoRowSchemaPath(), recipeIoRowSchemaPath, overwrite: true);
            File.Copy(
                Path.Combine(RecipeCatalogTestPaths.DiscoverRepoRecipesDirectory(), "prototype_recipes.json"),
                Path.Combine(recipesDir, "prototype_recipes.json"),
                overwrite: true);
            var loaded = RecipeDefinitionCatalogLoader.Load(
                recipesDir,
                recipeDefSchemaPath,
                recipeIoRowSchemaPath,
                KnownItemIds,
                KnownSkillIds,
                NullLogger.Instance);
            var registry = new RecipeDefinitionRegistry(loaded);
            // Act
            var ok = registry.TryGetDefinition("refine_scrap_standard", out var refine);
            var list = registry.GetDefinitionsInIdOrder();
            // Assert
            Assert.True(ok);
            Assert.NotNull(refine);
            Assert.Equal("process", refine.RecipeKind);
            Assert.Equal("refine", refine.RequiredSkillId);
            Assert.Equal(8, list.Count);
            Assert.Equal("make_armor_quick", list[0].Id);
            Assert.Equal("refine_scrap_standard", list[^1].Id);
        }
        finally
        {
            try
            {
                Directory.Delete(root.FullName, recursive: true);
            }
            catch (IOException)
            {
                // Best-effort: transient lock or race on some hosts; temp dir is unique per run.
            }
        }
    }

    [Fact]
    public async Task Host_ShouldResolveRegistryFromDi_WhenStartupSucceeds()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        using var client = factory.CreateClient();
        _ = await client.GetAsync("/health");
        // Act
        var registry = factory.Services.GetRequiredService<IRecipeDefinitionRegistry>();
        var found = registry.TryGetDefinition("refine_scrap_standard", out var refine);
        var unknown = registry.TryGetDefinition("not_a_real_recipe", out var missing);
        var list = registry.GetDefinitionsInIdOrder();
        // Assert
        Assert.True(found);
        Assert.NotNull(refine);
        Assert.Equal("process", refine.RecipeKind);
        Assert.False(unknown);
        Assert.Null(missing);
        Assert.Equal(8, list.Count);
        Assert.Contains(list, r => r.Id == "make_field_stim_mk0" && r.RecipeKind == "make");
    }
}
