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.Combat;
using NeonSprawl.Server.Tests;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Combat;

public class AbilityDefinitionCatalogLoaderTests
{
    private const string ValidPrototypeCatalogJson =
        """
        {
          "schemaVersion": 1,
          "abilities": [
            {
              "id": "prototype_pulse",
              "displayName": "Prototype Pulse",
              "baseDamage": 25,
              "cooldownSeconds": 3.0,
              "abilityKind": "attack"
            },
            {
              "id": "prototype_guard",
              "displayName": "Prototype Guard",
              "baseDamage": 0,
              "cooldownSeconds": 6.0,
              "abilityKind": "utility"
            },
            {
              "id": "prototype_dash",
              "displayName": "Prototype Dash",
              "baseDamage": 0,
              "cooldownSeconds": 4.0,
              "abilityKind": "movement"
            },
            {
              "id": "prototype_burst",
              "displayName": "Prototype Burst",
              "baseDamage": 40,
              "cooldownSeconds": 5.0,
              "abilityKind": "attack"
            }
          ]
        }
        """;

    private static (string Root, string AbilitiesDir, string SchemaPath) CreateTempContentLayout()
    {
        var root = Directory.CreateTempSubdirectory("neon-sprawl-abilitycat-");
        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);
        return (root.FullName, abilitiesDir, schemaPath);
    }

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

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

        throw new InvalidOperationException($"ability id not found: {abilityId}");
    }

    private static AbilityDefinitionCatalog LoadCatalog(string abilitiesDir, string schemaPath) =>
        AbilityDefinitionCatalogLoader.Load(abilitiesDir, schemaPath, NullLogger.Instance);

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        WriteCatalog(abilitiesDir, ValidPrototypeCatalogJson);
        // Act
        var catalog = LoadCatalog(abilitiesDir, schemaPath);
        // Assert
        Assert.Equal(4, catalog.DistinctAbilityCount);
        Assert.Equal(1, catalog.CatalogJsonFileCount);
        Assert.True(catalog.TryGetAbility("prototype_pulse", out var pulse));
        Assert.NotNull(pulse);
        Assert.Equal(25, pulse!.BaseDamage);
        Assert.Equal(3.0, pulse.CooldownSeconds);
        Assert.Equal("attack", pulse.AbilityKind);
    }

    [Fact]
    public void Load_ShouldThrow_WhenAbilitiesIsNotArray()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(
            Path.Combine(abilitiesDir, "bad_abilities.json"),
            """{"schemaVersion": 1, "abilities": "nope"}""",
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_abilities.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("expected top-level 'abilities' array", ioe.Message, StringComparison.Ordinal);
    }

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

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateAbilityIdAcrossFiles()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        WriteCatalog(abilitiesDir, ValidPrototypeCatalogJson);
        File.WriteAllText(
            Path.Combine(abilitiesDir, "extra_abilities.json"),
            """
            {
              "schemaVersion": 1,
              "abilities": [
                {
                  "id": "prototype_pulse",
                  "displayName": "Duplicate Pulse",
                  "baseDamage": 1,
                  "cooldownSeconds": 1.0
                }
              ]
            }
            """,
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate ability id 'prototype_pulse'", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenE5M1GateFailsWithMissingId()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        var abilities = root["abilities"] as JsonArray
                        ?? throw new InvalidOperationException("expected abilities array");
        abilities.RemoveAt(abilities.Count - 1);
        WriteCatalog(abilitiesDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype E5M1 expects exactly ability ids", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenE5M1GateFailsWithExtraId()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetAbilityRow(root, "prototype_burst")["id"] = "prototype_extra";
        WriteCatalog(abilitiesDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype E5M1 expects exactly ability ids", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("prototype_extra", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenBaseDamageIsNegative()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetAbilityRow(root, "prototype_pulse")["baseDamage"] = -1;
        WriteCatalog(abilitiesDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("Ability catalog validation failed", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("baseDamage", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenCooldownSecondsIsZero()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetAbilityRow(root, "prototype_pulse")["cooldownSeconds"] = 0;
        WriteCatalog(abilitiesDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("Ability catalog validation failed", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("cooldownSeconds", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDisplayNameIsEmpty()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetAbilityRow(root, "prototype_pulse")["displayName"] = "";
        WriteCatalog(abilitiesDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("Ability catalog validation failed", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("displayName", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenNoAbilityCatalogFiles()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("no *_abilities.json files", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenJsonIsInvalid()
    {
        // Arrange
        var (_, abilitiesDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(
            Path.Combine(abilitiesDir, "bad_abilities.json"),
            "{not json",
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => LoadCatalog(abilitiesDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_abilities.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("invalid JSON", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenAbilitiesDirectoryMissing()
    {
        // Arrange
        var missingDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-no-abilities-" + Guid.NewGuid().ToString("n"));
        var schemaPath = AbilityCatalogTestPaths.DiscoverRepoAbilityDefSchemaPath();
        // Act
        var ex = Record.Exception(() =>
            AbilityDefinitionCatalogLoader.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 (_, abilitiesDir, _) = CreateTempContentLayout();
        var missingSchema = Path.Combine(abilitiesDir, "missing-ability-def.schema.json");
        // Act
        var ex = Record.Exception(() =>
            AbilityDefinitionCatalogLoader.Load(abilitiesDir, 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<AbilityDefinitionCatalog>();
        Assert.Equal(4, catalog.DistinctAbilityCount);
        Assert.True(catalog.TryGetAbility("prototype_burst", out var burst));
        Assert.Equal(40, burst!.BaseDamage);
        Assert.Equal(5.0, burst.CooldownSeconds);
    }

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