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

namespace NeonSprawl.Server.Tests.Game.Npc;

public class NpcBehaviorDefinitionCatalogLoaderTests
{
    private const string ValidPrototypeCatalogJson =
        """
        {
          "schemaVersion": 1,
          "npcBehaviors": [
            {
              "id": "prototype_melee_pressure",
              "displayName": "Melee Pressure",
              "archetypeKind": "melee_pressure",
              "maxHp": 100,
              "aggroRadius": 8.0,
              "leashRadius": 16.0,
              "telegraphWindupSeconds": 1.5,
              "attackDamage": 15,
              "attackCooldownSeconds": 3.0
            },
            {
              "id": "prototype_ranged_control",
              "displayName": "Ranged Control",
              "archetypeKind": "ranged_control",
              "maxHp": 80,
              "aggroRadius": 10.0,
              "leashRadius": 20.0,
              "telegraphWindupSeconds": 2.0,
              "attackDamage": 12,
              "attackCooldownSeconds": 4.0
            },
            {
              "id": "prototype_elite_mini_boss",
              "displayName": "Elite Mini-Boss",
              "archetypeKind": "elite_mini_boss",
              "maxHp": 200,
              "aggroRadius": 8.0,
              "leashRadius": 18.0,
              "telegraphWindupSeconds": 2.5,
              "attackDamage": 25,
              "attackCooldownSeconds": 5.0
            }
          ]
        }
        """;

    private static (string Root, string NpcBehaviorsDir, string SchemaPath) CreateTempContentLayout()
    {
        var root = Directory.CreateTempSubdirectory("neon-sprawl-npcbehaviorcat-");
        var npcBehaviorsDir = Path.Combine(root.FullName, "content", "npc-behaviors");
        var schemaDir = Path.Combine(root.FullName, "content", "schemas");
        Directory.CreateDirectory(npcBehaviorsDir);
        Directory.CreateDirectory(schemaDir);
        var schemaPath = Path.Combine(schemaDir, "npc-behavior-def.schema.json");
        File.Copy(NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorDefSchemaPath(), schemaPath, overwrite: true);
        return (root.FullName, npcBehaviorsDir, schemaPath);
    }

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

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

        throw new InvalidOperationException($"npc behavior id not found: {behaviorId}");
    }

    private static NpcBehaviorDefinitionCatalog LoadCatalog(string npcBehaviorsDir, string schemaPath) =>
        NpcBehaviorDefinitionCatalogLoader.Load(npcBehaviorsDir, schemaPath, NullLogger.Instance);

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        var (_, npcBehaviorsDir, schemaPath) = CreateTempContentLayout();
        WriteCatalog(npcBehaviorsDir, ValidPrototypeCatalogJson);
        // Act
        var catalog = LoadCatalog(npcBehaviorsDir, schemaPath);
        // Assert
        Assert.Equal(3, catalog.DistinctBehaviorCount);
        Assert.Equal(1, catalog.CatalogJsonFileCount);
        Assert.True(catalog.TryGetBehavior("prototype_melee_pressure", out var melee));
        Assert.NotNull(melee);
        Assert.Equal("melee_pressure", melee!.ArchetypeKind);
        Assert.Equal(100, melee.MaxHp);
        Assert.Equal(8.0, melee.AggroRadius);
        Assert.Equal(16.0, melee.LeashRadius);
        Assert.Equal(15, melee.AttackDamage);
    }

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

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

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateBehaviorIdAcrossFiles()
    {
        // Arrange
        var (_, npcBehaviorsDir, schemaPath) = CreateTempContentLayout();
        WriteCatalog(npcBehaviorsDir, ValidPrototypeCatalogJson);
        File.WriteAllText(
            Path.Combine(npcBehaviorsDir, "extra_npc_behaviors.json"),
            """
            {
              "schemaVersion": 1,
              "npcBehaviors": [
                {
                  "id": "prototype_melee_pressure",
                  "displayName": "Duplicate Melee",
                  "archetypeKind": "melee_pressure",
                  "maxHp": 1,
                  "aggroRadius": 1.0,
                  "leashRadius": 2.0,
                  "telegraphWindupSeconds": 1.0,
                  "attackDamage": 1,
                  "attackCooldownSeconds": 1.0
                }
              ]
            }
            """,
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => LoadCatalog(npcBehaviorsDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate npc behavior id 'prototype_melee_pressure'", ioe.Message, StringComparison.Ordinal);
    }

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

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

    [Fact]
    public void Load_ShouldThrow_WhenLeashRadiusIsNotGreaterThanAggroRadius()
    {
        // Arrange
        var (_, npcBehaviorsDir, schemaPath) = CreateTempContentLayout();
        var root = JsonNode.Parse(ValidPrototypeCatalogJson) as JsonObject
                   ?? throw new InvalidOperationException("expected object root");
        GetBehaviorRow(root, "prototype_melee_pressure")["leashRadius"] = 8.0;
        WriteCatalog(npcBehaviorsDir, root.ToJsonString());
        // Act
        var ex = Record.Exception(() => LoadCatalog(npcBehaviorsDir, schemaPath));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("leashRadius 8 must be > aggroRadius 8", ioe.Message, StringComparison.Ordinal);
    }

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

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

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

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

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

    [Fact]
    public void Load_ShouldThrow_WhenNpcBehaviorsDirectoryMissing()
    {
        // Arrange
        var missingDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-no-npc-behaviors-" + Guid.NewGuid().ToString("n"));
        var schemaPath = NpcBehaviorCatalogTestPaths.DiscoverRepoNpcBehaviorDefSchemaPath();
        // Act
        var ex = Record.Exception(() =>
            NpcBehaviorDefinitionCatalogLoader.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 (_, npcBehaviorsDir, _) = CreateTempContentLayout();
        var missingSchema = Path.Combine(npcBehaviorsDir, "missing-npc-behavior-def.schema.json");
        // Act
        var ex = Record.Exception(() =>
            NpcBehaviorDefinitionCatalogLoader.Load(npcBehaviorsDir, 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<NpcBehaviorDefinitionCatalog>();
        Assert.Equal(3, catalog.DistinctBehaviorCount);
        Assert.True(catalog.TryGetBehavior("prototype_elite_mini_boss", out var elite));
        Assert.Equal(200, elite!.MaxHp);
        Assert.Equal(25, elite.AttackDamage);
        Assert.Equal(5.0, elite.AttackCooldownSeconds);
    }

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