using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.AbilityInput;
using NeonSprawl.Server.Game.Combat;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Combat;

public class AbilityDefinitionRegistryTests
{
    private static AbilityDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, AbilityDefRow> byId)
    {
        var catalog = new AbilityDefinitionCatalog("/tmp/abilities", byId, catalogJsonFileCount: 1);
        return new AbilityDefinitionRegistry(catalog);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenPulseExists()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var def);
        // Assert
        Assert.True(found);
        Assert.NotNull(def);
        Assert.Equal(25, def.BaseDamage);
        Assert.Equal(3.0, def.CooldownSeconds);
        Assert.Equal("attack", def.AbilityKind);
        Assert.Equal("Prototype Pulse", def.DisplayName);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenBurstExists()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypeBurst,
                "Prototype Burst",
                40,
                5.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var def);
        // Assert
        Assert.True(found);
        Assert.NotNull(def);
        Assert.Equal(40, def.BaseDamage);
        Assert.Equal(5.0, def.CooldownSeconds);
    }

    [Fact]
    public void TryGetDefinition_ShouldReturnFalse_WhenAbilityIdIsNull()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        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, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryGetDefinition("not_a_real_ability", out var def);
        // Assert
        Assert.False(found);
        Assert.Null(def);
    }

    [Fact]
    public void TryNormalizeKnown_ShouldReturnTrueAndLowercaseId_WhenInputHasWhitespaceAndMixedCase()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryNormalizeKnown("  Prototype_Pulse  ", out var normalized);
        // Assert
        Assert.True(found);
        Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, normalized);
    }

    [Fact]
    public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsNull()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryNormalizeKnown(null, out var normalized);
        // Assert
        Assert.False(found);
        Assert.Equal(string.Empty, normalized);
    }

    [Fact]
    public void TryNormalizeKnown_ShouldReturnFalse_WhenInputIsWhitespaceOnly()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryNormalizeKnown("   ", out var normalized);
        // Assert
        Assert.False(found);
        Assert.Equal(string.Empty, normalized);
    }

    [Fact]
    public void TryNormalizeKnown_ShouldReturnFalse_WhenIdNotInCatalog()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var found = registry.TryNormalizeKnown("prototype_unknown", out var normalized);
        // Assert
        Assert.False(found);
        Assert.Equal("prototype_unknown", normalized);
    }

    [Fact]
    public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleAbilities()
    {
        // Arrange
        var rows = new Dictionary<string, AbilityDefRow>(StringComparer.Ordinal)
        {
            [PrototypeAbilityRegistry.PrototypeBurst] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypeBurst,
                "Prototype Burst",
                40,
                5.0,
                "attack"),
            [PrototypeAbilityRegistry.PrototypeDash] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypeDash,
                "Prototype Dash",
                0,
                4.0,
                "movement"),
            [PrototypeAbilityRegistry.PrototypeGuard] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypeGuard,
                "Prototype Guard",
                0,
                6.0,
                "utility"),
            [PrototypeAbilityRegistry.PrototypePulse] = new AbilityDefRow(
                PrototypeAbilityRegistry.PrototypePulse,
                "Prototype Pulse",
                25,
                3.0,
                "attack"),
        };
        var registry = CreateRegistryFromRows(rows);
        // Act
        var list = registry.GetDefinitionsInIdOrder();
        // Assert
        Assert.Equal(4, list.Count);
        Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id);
        Assert.Equal(PrototypeAbilityRegistry.PrototypeDash, list[1].Id);
        Assert.Equal(PrototypeAbilityRegistry.PrototypeGuard, list[2].Id);
        Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, list[3].Id);
    }

    [Fact]
    public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
    {
        // Arrange
        var root = Directory.CreateTempSubdirectory("neon-sprawl-ability-registry-loader-");
        try
        {
            var abilitiesDir = Path.Combine(root.FullName, "content", "abilities");
            var schemaDir = Path.Combine(root.FullName, "content", "schemas");
            Directory.CreateDirectory(abilitiesDir);
            Directory.CreateDirectory(schemaDir);
            var schemaPath = Path.Combine(schemaDir, "ability-def.schema.json");
            File.Copy(AbilityCatalogTestPaths.DiscoverRepoAbilityDefSchemaPath(), schemaPath, overwrite: true);
            File.Copy(
                Path.Combine(AbilityCatalogTestPaths.DiscoverRepoAbilitiesDirectory(), "prototype_abilities.json"),
                Path.Combine(abilitiesDir, "prototype_abilities.json"),
                overwrite: true);
            var loaded = AbilityDefinitionCatalogLoader.Load(abilitiesDir, schemaPath, NullLogger.Instance);
            var registry = new AbilityDefinitionRegistry(loaded);
            // Act
            var pulseOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse);
            var guardOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeGuard, out var guard);
            var dashOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeDash, out var dash);
            var burstOk = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypeBurst, out var burst);
            var list = registry.GetDefinitionsInIdOrder();
            // Assert
            Assert.True(pulseOk);
            Assert.NotNull(pulse);
            Assert.Equal(25, pulse!.BaseDamage);
            Assert.Equal(3.0, pulse.CooldownSeconds);
            Assert.True(guardOk);
            Assert.NotNull(guard);
            Assert.Equal(0, guard!.BaseDamage);
            Assert.Equal(6.0, guard.CooldownSeconds);
            Assert.True(dashOk);
            Assert.NotNull(dash);
            Assert.Equal(0, dash!.BaseDamage);
            Assert.Equal(4.0, dash.CooldownSeconds);
            Assert.True(burstOk);
            Assert.NotNull(burst);
            Assert.Equal(40, burst!.BaseDamage);
            Assert.Equal(5.0, burst.CooldownSeconds);
            Assert.Equal(4, list.Count);
            Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, list[0].Id);
            Assert.Equal(PrototypeAbilityRegistry.PrototypePulse, 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<IAbilityDefinitionRegistry>();
        var pulseFound = registry.TryGetDefinition(PrototypeAbilityRegistry.PrototypePulse, out var pulse);
        var unknown = registry.TryGetDefinition("not_a_real_ability", out var missing);
        var normalized = registry.TryNormalizeKnown(" Prototype_Burst ", out var burstId);
        var list = registry.GetDefinitionsInIdOrder();
        // Assert
        Assert.True(pulseFound);
        Assert.NotNull(pulse);
        Assert.Equal(25, pulse!.BaseDamage);
        Assert.Equal(3.0, pulse.CooldownSeconds);
        Assert.False(unknown);
        Assert.Null(missing);
        Assert.True(normalized);
        Assert.Equal(PrototypeAbilityRegistry.PrototypeBurst, burstId);
        Assert.Equal(4, list.Count);
        Assert.Contains(list, a => a.Id == PrototypeAbilityRegistry.PrototypeGuard && a.BaseDamage == 0);
    }
}
