using System.Buffers;
using System.Text.Json;
using PkmnLib.Dynamic.Libraries;
using PkmnLib.Dynamic.Plugins;
using PkmnLib.Dynamic.ScriptHandling;
using PkmnLib.Plugin.Gen7.Common;
using PkmnLib.Static.Moves;
using PkmnLib.Static.Utils;

namespace PkmnLib.Plugin.Gen7.Tests.DataTests;

public class MoveDataTests
{
    public record TestCaseData(IDynamicLibrary Library, IMoveData Move)
    {
        /// <inheritdoc />
        public override string ToString() => Move.Name + " has valid scripts";
    }

    public static IEnumerable<Func<TestCaseData>> AllMovesHaveValidScriptsData()
    {
        var library = LibraryHelpers.LoadLibrary();
        var moveLibrary = library.StaticLibrary.Moves;
        foreach (var move in moveLibrary)
        {
            if (move.SecondaryEffect == null)
                continue;
            yield return () => new TestCaseData(library, move);
        }
    }

    [Test, MethodDataSource(nameof(AllMovesHaveValidScriptsData))]
    public async Task AllMoveEffectsHaveValidScripts(TestCaseData test)
    {
        if (test.Move.SecondaryEffect == null)
            return;
        var scriptName = test.Move.SecondaryEffect.Name;

        try
        {
            await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Move, scriptName,
                test.Move.SecondaryEffect.Parameters, out _)).IsTrue();
        }
        catch (Exception e)
        {
            // Helper method to find the line number of the effect in the JSON file
            var file = Path.GetFullPath("../../../../Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc");
            var json = await File.ReadAllLinesAsync(file);
            var moveLineNumber = json.Select((line, index) => new { line, index })
                .FirstOrDefault(x => x.line.Contains($"\"name\": \"{test.Move.Name}\""))?.index + 1;
            var effectLineNumber = moveLineNumber + json.Skip(moveLineNumber ?? 0)
                    .Select((line, index) => new { line, index }).FirstOrDefault(x => x.line.Contains("effect"))
                    ?.index +
                1 ?? 0;

            await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{file}:{effectLineNumber}");
            throw new AggregateException($"Failed to resolve script for move {test.Move.Name} with effect {scriptName}",
                e);
        }
    }

    public record SetStatusTestCaseData(IDynamicLibrary Library, IMoveData Move)
    {
        /// <inheritdoc />
        public override string ToString() => Move.Name + " has valid status: " +
                                             Move.SecondaryEffect?.Parameters.GetValueOrDefault("status");
    }

    public static IEnumerable<Func<SetStatusTestCaseData>> SetStatusMovesHaveValidStatusData()
    {
        var library = LibraryHelpers.LoadLibrary();
        var moveLibrary = library.StaticLibrary.Moves;
        foreach (var move in moveLibrary)
        {
            if (move.SecondaryEffect?.Name != "set_status")
                continue;
            yield return () => new SetStatusTestCaseData(library, move);
        }
    }

    [Test, MethodDataSource(nameof(SetStatusMovesHaveValidStatusData))]
    public async Task SetStatusMovesHaveValidStatus(SetStatusTestCaseData test)
    {
        if (test.Move.SecondaryEffect == null)
            return;
        var status = test.Move.SecondaryEffect.Parameters["status"]?.ToString();
        if (status == null)
            throw new Exception("Missing required parameter 'status'");

        await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out _)).IsTrue();
    }

    public static IEnumerable<Func<StringKey>> AllMoveFlagsAreIncludedInMoveFlagsData()
    {
        var library = LibraryHelpers.LoadLibrary();
        var moveLibrary = library.StaticLibrary.Moves;
        foreach (var flag in moveLibrary.SelectMany(x => x.Flags).Distinct())
        {
            yield return () => flag;
        }
    }

    private static readonly HashSet<StringKey> CompileTimeMoveFlags = typeof(MoveFlags).GetFields()
        .Where(x => x is { IsStatic: true, IsPublic: true } && x.FieldType == typeof(StringKey))
        .Select(x => (StringKey)x.GetValue(null)!).ToHashSet();

    [Test, MethodDataSource(nameof(AllMoveFlagsAreIncludedInMoveFlagsData))]
    public async Task AllMoveFlagsAreIncludedInMoveFlagsClass(StringKey flag)
    {
        await Assert.That(CompileTimeMoveFlags.Contains(flag)).IsTrue();
    }

    public record HasEitherEffectOrComment(string MoveName, bool HasEffect, bool HasComment)
    {
        /// <inheritdoc />
        public override string ToString() => MoveName + " has effect: " + HasEffect + ", comment: " + HasComment;
    }

    public static IEnumerable<Func<HasEitherEffectOrComment>> EveryMoveHasEitherEffectOrCommentData()
    {
        IResourceProvider plugin = new Gen7Plugin();
        using var movesJson = plugin.GetResource(ResourceFileType.Moves)!.Open();
        var json = new BinaryReader(movesJson);
        var moves = json.ReadBytes((int)movesJson.Length);
        var reader = new Utf8JsonReader(new ReadOnlySequence<byte>(moves), new JsonReaderOptions
        {
            CommentHandling = JsonCommentHandling.Allow,
            AllowTrailingCommas = true,
        });
        Console.WriteLine("Reading moves.json");

        // The JSON is an object { "data": [] }.
        // Each move is an object in the "data" array.
        // Each object needs to have either an "effect" or a "comment" property.

        // Read the first object
        reader.Read();
        // Read the properties, until we find the "data" property
        while (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "data")
        {
            reader.Read();
        }
        // Read the "data" property
        reader.Read();
        // Read the start of the array
        reader.Read();

        var results = new List<HasEitherEffectOrComment>();
        // Read each move object
        while (reader.TokenType != JsonTokenType.EndArray)
        {
            // Read the start of the object
            if (!reader.Read())
                break;
            // Read each property
            var name = "";
            var hasEffect = false;
            var hasComment = false;
            while (reader.TokenType != JsonTokenType.EndObject)
            {
                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    var propertyName = reader.GetString();
                    if (propertyName == "name")
                    {
                        reader.Read();
                        name = reader.GetString()!;
                    }
                    else if (propertyName == "effect")
                    {
                        hasEffect = true;
                        reader.Read();
                        // Read the effect object
                        while (reader.TokenType != JsonTokenType.EndObject)
                        {
                            reader.Read();
                            if (reader.TokenType != JsonTokenType.StartObject)
                                continue;
                            while (reader.TokenType != JsonTokenType.EndObject)
                            {
                                reader.Read();
                            }
                            reader.Read();
                        }
                    }
                }
                else if (reader.TokenType == JsonTokenType.Comment)
                {
                    // Read the comment
                    var comment = reader.GetComment();
                    hasComment = comment.Trim() == "No secondary effect";
                }

                reader.Read();
            }
            results.Add(new HasEitherEffectOrComment(name, hasEffect, hasComment));
        }
        return results.Where(x => !string.IsNullOrWhiteSpace(x.MoveName))
            .Select<HasEitherEffectOrComment, Func<HasEitherEffectOrComment>>(x => () => x).ToList();
    }

    /// <summary>
    /// To ensure that all moves have been properly implemented, we require that each move has either an "effect" property,
    /// or a comment with the exact text "No secondary effect". This ensures that we have not missed any moves.
    ///
    /// The implementation of this test is a bit tricky, as we need to read the JSON file and parse it ourselves,
    /// as System.Text.Json does not support reading comments in JSON files beyond the low-level API.
    /// </summary>
    /// <param name="test"></param>
    [Test, MethodDataSource(nameof(EveryMoveHasEitherEffectOrCommentData))]
    public async Task AllMovesHaveEitherEffectOrComment(HasEitherEffectOrComment test) =>
        // Check that each move has either an "effect" or a "comment" property
        await Assert.That(test.HasEffect || test.HasComment).IsTrue();
}