using System.Net;
using System.Text;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Tests;
using Microsoft.Extensions.Logging.Abstractions;
using NeonSprawl.Server.Game.Skills;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Skills;

public class SkillDefinitionCatalogLoaderTests
{
    private const string ValidPrototypeCatalogJson =
        """
        {
          "schemaVersion": 1,
          "skills": [
            {
              "id": "salvage",
              "category": "gather",
              "displayName": "Salvage",
              "allowedXpSourceKinds": ["activity", "mission_reward"]
            },
            {
              "id": "refine",
              "category": "process",
              "displayName": "Refine",
              "allowedXpSourceKinds": ["activity", "mission_reward", "trainer"]
            },
            {
              "id": "intrusion",
              "category": "tech",
              "displayName": "Intrusion",
              "allowedXpSourceKinds": ["activity", "mission_reward", "book_or_item"]
            }
          ]
        }
        """;

    private static (string Root, string SkillsDir, string SchemaPath) CreateTempContentLayout()
    {
        var root = Directory.CreateTempSubdirectory("neon-sprawl-skillcat-");
        var skillsDir = Path.Combine(root.FullName, "content", "skills");
        var schemaDir = Path.Combine(root.FullName, "content", "schemas");
        Directory.CreateDirectory(skillsDir);
        Directory.CreateDirectory(schemaDir);
        var schemaPath = Path.Combine(schemaDir, "skill-def.schema.json");
        File.Copy(SkillCatalogTestPaths.DiscoverRepoSkillDefSchemaPath(), schemaPath, overwrite: true);
        return (root.FullName, skillsDir, schemaPath);
    }

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        var (_, skillsDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(Path.Combine(skillsDir, "prototype_skills.json"), ValidPrototypeCatalogJson, Encoding.UTF8);
        // Act
        var catalog = SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance);
        // Assert
        Assert.Equal(3, catalog.DistinctSkillCount);
        Assert.Equal(1, catalog.CatalogJsonFileCount);
        Assert.True(catalog.ById.ContainsKey("salvage"));
        Assert.Equal("gather", catalog.ById["salvage"].Category);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSkillsIsNotArray()
    {
        // Arrange
        var (_, skillsDir, schemaPath) = CreateTempContentLayout();
        File.WriteAllText(Path.Combine(skillsDir, "bad_skills.json"), """{"skills": "nope"}""", Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_skills.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("expected top-level 'skills' array", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenRowViolatesSchema()
    {
        // Arrange
        var (_, skillsDir, schemaPath) = CreateTempContentLayout();
        var bad = """
                  {
                    "skills": [
                      {
                        "id": "salvage",
                        "category": "gather",
                        "displayName": "",
                        "allowedXpSourceKinds": ["activity"]
                      }
                    ]
                  }
                  """;
        File.WriteAllText(Path.Combine(skillsDir, "bad_skills.json"), bad, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_skills.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("Skill catalog validation failed", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicateIdAcrossFiles()
    {
        // Arrange
        var (_, skillsDir, schemaPath) = CreateTempContentLayout();
        var singleSalvage = """
                            {
                              "skills": [
                                {
                                  "id": "salvage",
                                  "category": "gather",
                                  "displayName": "Salvage",
                                  "allowedXpSourceKinds": ["activity"]
                                }
                              ]
                            }
                            """;
        File.WriteAllText(Path.Combine(skillsDir, "a_skills.json"), singleSalvage, Encoding.UTF8);
        File.WriteAllText(Path.Combine(skillsDir, "b_skills.json"), singleSalvage, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate skill id", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("salvage", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("a_skills.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("b_skills.json", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice1IdsIncomplete()
    {
        // Arrange
        var (_, skillsDir, schemaPath) = CreateTempContentLayout();
        var twoOnly = """
                      {
                        "skills": [
                          {
                            "id": "salvage",
                            "category": "gather",
                            "displayName": "Salvage",
                            "allowedXpSourceKinds": ["activity"]
                          },
                          {
                            "id": "refine",
                            "category": "process",
                            "displayName": "Refine",
                            "allowedXpSourceKinds": ["activity"]
                          }
                        ]
                      }
                      """;
        File.WriteAllText(Path.Combine(skillsDir, "partial_skills.json"), twoOnly, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() => SkillDefinitionCatalogLoader.Load(skillsDir, schemaPath, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype Slice 1 expects exactly skill ids", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Host_ShouldResolveCatalogFromDi_WhenStartupSucceeds()
    {
        // Arrange — InMemoryWebApplicationFactory strips Postgres and pins Content:SkillsDirectory to the repo catalog.
        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<SkillDefinitionCatalog>();
        Assert.Equal(3, catalog.DistinctSkillCount);
    }

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