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

namespace NeonSprawl.Server.Tests.Game.Mastery;

public class MasteryCatalogLoaderTests
{
    private static readonly FrozenSet<string> PrototypeSkillIds = FrozenSet.ToFrozenSet(
        ["salvage", "refine", "intrusion"],
        StringComparer.Ordinal);

    private const string ValidSalvageMasteryJson =
        """
        {
          "schemaVersion": 1,
          "perks": {
            "salvage_scrap_efficiency_1": {
              "id": "salvage_scrap_efficiency_1",
              "displayName": "Scrap Sifter",
              "effectKind": "gather_yield_modifier"
            },
            "salvage_bulk_haul_1": {
              "id": "salvage_bulk_haul_1",
              "displayName": "Haul Frame",
              "effectKind": "gather_carry_modifier"
            }
          },
          "tracks": [
            {
              "skillId": "salvage",
              "tiers": [
                {
                  "tierIndex": 1,
                  "requiredLevel": 2,
                  "branches": [
                    { "branchId": "scrap_efficiency", "displayName": "Scrap Efficiency", "perkIds": [] },
                    { "branchId": "bulk_haul", "displayName": "Bulk Haul", "perkIds": [] }
                  ]
                },
                {
                  "tierIndex": 2,
                  "requiredLevel": 4,
                  "branches": [
                    { "branchId": "scrap_efficiency", "perkIds": ["salvage_scrap_efficiency_1"] },
                    { "branchId": "bulk_haul", "perkIds": ["salvage_bulk_haul_1"] }
                  ]
                }
              ]
            }
          ]
        }
        """;

    private sealed class TempContentLayout : IDisposable
    {
        public string MasteryDir { get; }
        public string SchemaPath { get; }
        private readonly string _root;

        private TempContentLayout(string root, string masteryDir, string schemaPath)
        {
            _root = root;
            MasteryDir = masteryDir;
            SchemaPath = schemaPath;
        }

        public static TempContentLayout Create()
        {
            var root = Directory.CreateTempSubdirectory("neon-sprawl-masterycat-");
            var masteryDir = Path.Combine(root.FullName, "content", "mastery");
            var schemaDir = Path.Combine(root.FullName, "content", "schemas");
            Directory.CreateDirectory(masteryDir);
            Directory.CreateDirectory(schemaDir);
            var schemaPath = Path.Combine(schemaDir, "mastery-catalog.schema.json");
            File.Copy(MasteryCatalogTestPaths.DiscoverRepoMasteryCatalogSchemaPath(), schemaPath, overwrite: true);
            return new TempContentLayout(root.FullName, masteryDir, schemaPath);
        }

        public void Dispose()
        {
            try
            {
                if (Directory.Exists(_root))
                    Directory.Delete(_root, recursive: true);
            }
            catch (IOException)
            {
                // Best-effort cleanup for parallel test runs.
            }
        }
    }

    [Fact]
    public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        File.WriteAllText(
            Path.Combine(layout.MasteryDir, "prototype_salvage_mastery.json"),
            ValidSalvageMasteryJson,
            Encoding.UTF8);
        // Act
        var catalog = MasteryCatalogLoader.Load(
            layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance);
        // Assert
        Assert.Equal(1, catalog.TrackCount);
        Assert.Equal(2, catalog.PerkCount);
        Assert.True(catalog.TracksBySkillId.ContainsKey("salvage"));
    }

    [Fact]
    public void Load_ShouldThrow_WhenJsonIsMalformed()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        File.WriteAllText(Path.Combine(layout.MasteryDir, "bad_mastery.json"), "{ not json", Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_mastery.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("invalid JSON", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenTracksIsNotArray()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        File.WriteAllText(
            Path.Combine(layout.MasteryDir, "bad_mastery.json"),
            """{"schemaVersion":1,"perks":{},"tracks":"nope"}""",
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("bad_mastery.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("Mastery catalog validation failed", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenRowViolatesSchema()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var bad = ValidSalvageMasteryJson.Replace(
            "\"displayName\": \"Scrap Sifter\"",
            "\"displayName\": \"\"",
            StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "schema_bad_mastery.json"), bad, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("schema_bad_mastery.json", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("Mastery catalog validation failed", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenUnknownSkillId()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var onlyRefineSkills = FrozenSet.ToFrozenSet(["refine"], StringComparer.Ordinal);
        File.WriteAllText(
            Path.Combine(layout.MasteryDir, "prototype_salvage_mastery.json"),
            ValidSalvageMasteryJson,
            Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, onlyRefineSkills, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("not a known SkillDef id", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("salvage", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicatePerkIdAcrossFiles()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        File.WriteAllText(Path.Combine(layout.MasteryDir, "a_mastery.json"), ValidSalvageMasteryJson, Encoding.UTF8);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "b_mastery.json"), ValidSalvageMasteryJson, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate perk id", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("salvage_scrap_efficiency_1", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenDuplicatePerkIdReferenceAcrossBranches()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var duplicateRef = ValidSalvageMasteryJson.Replace(
            "\"perkIds\": [\"salvage_bulk_haul_1\"]",
            "\"perkIds\": [\"salvage_scrap_efficiency_1\"]",
            StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "dup_ref_mastery.json"), duplicateRef, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate perk id reference", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("salvage_scrap_efficiency_1", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenUnreferencedPerkInMap()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var orphanPerk = ValidSalvageMasteryJson.Replace(
            "\"salvage_bulk_haul_1\":",
            "\"salvage_orphan_1\": { \"id\": \"salvage_orphan_1\", \"displayName\": \"Orphan\" },\n    \"salvage_bulk_haul_1\":",
            StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "orphan_mastery.json"), orphanPerk, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("not referenced by any branch perkIds", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenBranchIdSetDiffersBetweenTiers()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var mismatch = ValidSalvageMasteryJson.Replace(
            "\"branchId\": \"bulk_haul\", \"perkIds\": [\"salvage_bulk_haul_1\"]",
            "\"branchId\": \"other_branch\", \"perkIds\": [\"salvage_bulk_haul_1\"]",
            StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "branch_mismatch_mastery.json"), mismatch, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("branchId set", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("must match previous tier", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenRequiredLevelNotStrictlyIncreasing()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var flatLevel = ValidSalvageMasteryJson.Replace("\"requiredLevel\": 4", "\"requiredLevel\": 2", StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "flat_level_mastery.json"), flatLevel, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("requiredLevel must be strictly greater", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice4TrackCountWrong()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        const string twoTracksJson = """
                                     {
                                       "schemaVersion": 1,
                                       "perks": {},
                                       "tracks": [
                                         {
                                           "skillId": "salvage",
                                           "tiers": [
                                             {
                                               "tierIndex": 1,
                                               "requiredLevel": 2,
                                               "branches": [
                                                 { "branchId": "a", "perkIds": [] },
                                                 { "branchId": "b", "perkIds": [] }
                                               ]
                                             }
                                           ]
                                         },
                                         {
                                           "skillId": "refine",
                                           "tiers": [
                                             {
                                               "tierIndex": 1,
                                               "requiredLevel": 2,
                                               "branches": [
                                                 { "branchId": "a", "perkIds": [] },
                                                 { "branchId": "b", "perkIds": [] }
                                               ]
                                             }
                                           ]
                                         }
                                       ]
                                     }
                                     """;
        File.WriteAllText(Path.Combine(layout.MasteryDir, "two_tracks_mastery.json"), twoTracksJson, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype Slice 4 expects exactly one MasteryTrack", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenSlice4SoleTrackSkillIdWrong()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var refineTrack = ValidSalvageMasteryJson.Replace("\"skillId\": \"salvage\"", "\"skillId\": \"refine\"", StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "refine_track_mastery.json"), refineTrack, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("prototype Slice 4 expects the sole track skillId", ioe.Message, StringComparison.Ordinal);
        Assert.Contains("refine", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenTierIndexDuplicate()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var dupTier = ValidSalvageMasteryJson.Replace("\"tierIndex\": 2", "\"tierIndex\": 1", StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "dup_tier_mastery.json"), dupTier, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("duplicate tierIndex", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Load_ShouldThrow_WhenTierIndexHasGap()
    {
        // Arrange
        using var layout = TempContentLayout.Create();
        var gapTier = ValidSalvageMasteryJson.Replace("\"tierIndex\": 2", "\"tierIndex\": 3", StringComparison.Ordinal);
        File.WriteAllText(Path.Combine(layout.MasteryDir, "gap_tier_mastery.json"), gapTier, Encoding.UTF8);
        // Act
        var ex = Record.Exception(() =>
            MasteryCatalogLoader.Load(layout.MasteryDir, layout.SchemaPath, PrototypeSkillIds, NullLogger.Instance));
        // Assert
        var ioe = Assert.IsType<InvalidOperationException>(ex);
        Assert.Contains("tierIndex values must be unique and sequential", ioe.Message, StringComparison.Ordinal);
    }

    [Fact]
    public void Host_ShouldFailStartup_WhenMasteryDirectoryInvalid()
    {
        // Arrange
        var badDir = Path.Combine(Path.GetTempPath(), "neon-sprawl-empty-mastery-" + Guid.NewGuid().ToString("n"));
        try
        {
            Directory.CreateDirectory(badDir);
            var skillsDir = SkillCatalogTestPaths.DiscoverRepoSkillsDirectory();
            // Act
            var ex = Record.Exception(() =>
            {
                using var factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
                {
                    b.UseSetting("Content:SkillsDirectory", skillsDir);
                    b.UseSetting("Content:MasteryDirectory", badDir);
                });
                factory.CreateClient();
            });
            // Assert
            Assert.NotNull(ex);
            Assert.Contains("Mastery catalog validation failed", ex.ToString(), StringComparison.Ordinal);
        }
        finally
        {
            try
            {
                if (Directory.Exists(badDir))
                    Directory.Delete(badDir, recursive: true);
            }
            catch (IOException)
            {
                // Best-effort cleanup.
            }
        }
    }
}
