using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Permissions;
using AIRuntime.Runtime.Filesystem;
using AIRuntime.Runtime.Filesystem.Models;

using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEngine;
using UnityEngine.TestTools;


namespace AIRuntime.Tests
{
    public sealed class FileSystemCapabilityTests
    {
        private string _workspacePath;

        private AIRuntime.Runtime.AIRuntime _runtime;


        [SetUp]
        public void SetUp()
        {
            _workspacePath =
                Path.Combine(
                    Path.GetTempPath(),
                    "AIRuntime.FileSystemCapabilityTests",
                    Guid.NewGuid().ToString("N"));


            Directory.CreateDirectory(
                _workspacePath);


            _runtime =
                new AIRuntime.Runtime.AIRuntime(
                    _workspacePath);


            foreach (ICapability capability in CreateCapabilities())
            {
                _runtime.RegisterCapability(
                    capability);
            }


            _runtime.AllowPermission(
                CapabilityPermission.Write);

            _runtime.AllowPermission(
                CapabilityPermission.Destructive);
        }


        [TearDown]
        public void TearDown()
        {
            if (!string.IsNullOrWhiteSpace(
                    _workspacePath) &&
                Directory.Exists(
                    _workspacePath))
            {
                Directory.Delete(
                    _workspacePath,
                    recursive: true);
            }
        }


        [Test]
        public void CapabilitiesExposeExpectedNamesAndPermissions()
        {
            Dictionary<string, CapabilityPermission> expected =
                new Dictionary<string, CapabilityPermission>
                {
                    ["copy_file"] =
                        CapabilityPermission.Write,

                    ["create_directory"] =
                        CapabilityPermission.Write,

                    ["delete_directory"] =
                        CapabilityPermission.Destructive,

                    ["delete_file"] =
                        CapabilityPermission.Destructive,

                    ["file_exists"] =
                        CapabilityPermission.ReadOnly,

                    ["get_file_info"] =
                        CapabilityPermission.ReadOnly,

                    ["list_directory"] =
                        CapabilityPermission.ReadOnly,

                    ["list_directory_tree"] =
                        CapabilityPermission.ReadOnly,

                    ["move_file"] =
                        CapabilityPermission.Destructive
                };


            ICapability[] capabilities =
                CreateCapabilities()
                    .ToArray();


            Assert.AreEqual(
                expected.Count,
                capabilities.Length);


            foreach (ICapability capability in capabilities)
            {
                Assert.IsTrue(
                    expected.ContainsKey(
                        capability.Name));

                Assert.AreEqual(
                    expected[capability.Name],
                    capability.Permission);

                Assert.IsFalse(
                    string.IsNullOrWhiteSpace(
                        capability.Description));
            }
        }


        [Test]
        public void CapabilitiesExposeGeneratedSchemas()
        {
            CopyFileCapability copy =
                new CopyFileCapability();

            DeleteDirectoryCapability deleteDirectory =
                new DeleteDirectoryCapability();

            CreateDirectoryCapability createDirectory =
                new CreateDirectoryCapability();

            ListDirectoryCapability listDirectory =
                new ListDirectoryCapability();

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "sourcePath",
                    "destinationPath"
                },
                copy.Parameters["required"]
                    .Values<string>()
                    .ToArray());

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "parentPath",
                    "directoryName"
                },
                createDirectory.Parameters["required"]
                    .Values<string>()
                    .ToArray());


            Assert.AreEqual(
                false,
                deleteDirectory.Parameters["properties"]?["recursive"]?["default"]?
                    .Value<bool>());

            Assert.AreEqual(
                false,
                createDirectory.Parameters["properties"]?["createParents"]?["default"]?
                    .Value<bool>());

            Assert.AreEqual(
                ".",
                listDirectory.Parameters["properties"]?["path"]?["default"]?
                    .Value<string>());

            Assert.AreEqual(
                "All",
                listDirectory.Parameters["properties"]?["entryType"]?["default"]?
                    .Value<string>());

            Assert.AreEqual(
                "Metadata",
                listDirectory.Parameters["properties"]?["outputFormat"]?["default"]?
                    .Value<string>());

            Assert.AreEqual(
                false,
                listDirectory.Parameters["properties"]?["includeMetaFiles"]?["default"]?
                    .Value<bool>());

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "All",
                    "Files",
                    "Directories"
                },
                listDirectory.Parameters["properties"]?["entryType"]?["enum"]?
                    .Values<string>()
                    .ToArray());

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "Metadata",
                    "PathsOnly"
                },
                listDirectory.Parameters["properties"]?["outputFormat"]?["enum"]?
                    .Values<string>()
                    .ToArray());
        }


        [UnityTest]
        public IEnumerator CapabilitiesPerformEndToEndFileOperations()
        {
            Task task =
                RunEndToEndAsync();


            while (!task.IsCompleted)
            {
                yield return null;
            }


            if (task.IsFaulted)
            {
                throw task.Exception?
                    .GetBaseException();
            }


            if (task.IsCanceled)
            {
                throw new OperationCanceledException(
                    "The filesystem capability test was unexpectedly cancelled.");
            }
        }


        [UnityTest]
        public IEnumerator CreateDirectoryRejectsMissingParent()
        {
            Task task =
                RunRejectMissingParentAsync();


            while (!task.IsCompleted)
            {
                yield return null;
            }


            if (task.IsFaulted)
            {
                throw task.Exception?
                    .GetBaseException();
            }
        }


        [UnityTest]
        public IEnumerator CreateDirectoryCreatesRequestedParentChain()
        {
            Task task =
                RunCreateParentChainAsync();


            while (!task.IsCompleted)
            {
                yield return null;
            }


            if (task.IsFaulted)
            {
                throw task.Exception?
                    .GetBaseException();
            }
        }


        [UnityTest]
        public IEnumerator CreateDirectoryAcceptsNestedRelativeChainWithParents()
        {
            Task task =
                RunCreateNestedRelativeChainAsync();

            while (!task.IsCompleted)
            {
                yield return null;
            }

            if (task.IsFaulted)
            {
                throw task.Exception?
                    .GetBaseException();
            }
        }


        [UnityTest]
        public IEnumerator CreateDirectoryRejectsTrailingPeriodName()
        {
            LogAssert.Expect(
                LogType.Warning,
                "[Capability] 'create_directory' failed: DirectoryName must be one directory name without path separators, leading or trailing whitespace, or a trailing period.");

            Task task =
                RunRejectTrailingPeriodNameAsync();

            while (!task.IsCompleted)
            {
                yield return null;
            }

            Assert.IsTrue(
                task.IsFaulted,
                "A trailing-period directory name must be rejected before reaching the filesystem.");

            Assert.IsInstanceOf<ValidationException>(
                task.Exception?.GetBaseException());

            Assert.IsFalse(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "Assets",
                        "JumpingBallGame.")));
        }


        private async Task RunEndToEndAsync()
        {
            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "documents"));


            string createJson =
                await _runtime.ExecuteCapabilityAsync(
                    "create_directory",
                    "{\"parentPath\":\"documents\"," +
                    "\"directoryName\":\"nested\"}");

            JObject createResult =
                JObject.Parse(
                    createJson);


            Assert.IsTrue(
                (bool)createResult["Succeeded"]);

            Assert.AreEqual(
                "documents/nested",
                (string)createResult["Path"]);

            StringAssert.Contains(
                "Copy the Path field exactly",
                (string)createResult["ResponseInstruction"]);


            foreach (
                string arguments
                in new[]
                {
                    "{}",
                    "{\"path\":\"\"}"
                })
            {
                string rootJson =
                    await _runtime.ExecuteCapabilityAsync(
                        "list_directory",
                        arguments);

                JToken documents =
                    JArray.Parse(
                            rootJson)
                        .Single(
                            entry =>
                                string.Equals(
                                    (string)entry["Name"],
                                    "documents",
                                    StringComparison.Ordinal));


                Assert.IsTrue(
                    (bool)documents["IsDirectory"]);

                Assert.AreEqual(
                    "documents",
                    ((string)documents["Path"])
                        .Replace(
                            '\\',
                            '/'));
            }


            Assert.IsTrue(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "documents",
                        "nested")));


            File.WriteAllText(
                Path.Combine(
                    _workspacePath,
                    "documents",
                    "source.txt"),
                "source contents");

            File.WriteAllText(
                Path.Combine(
                    _workspacePath,
                    "documents",
                    "source.txt.meta"),
                "unity metadata");


            string exists =
                await _runtime.ExecuteCapabilityAsync(
                    "file_exists",
                    "{\"path\":\"documents/source.txt\"}");


            Assert.AreEqual(
                "true",
                exists);


            string infoJson =
                await _runtime.ExecuteCapabilityAsync(
                    "get_file_info",
                    "{\"path\":\"documents/source.txt\"}");


            JObject info =
                JObject.Parse(
                    infoJson);


            Assert.AreEqual(
                true,
                (bool)info["Exists"]);

            Assert.AreEqual(
                "source.txt",
                (string)info["Name"]);


            await _runtime.ExecuteCapabilityAsync(
                "copy_file",
                "{\"sourcePath\":\"documents/source.txt\"," +
                "\"destinationPath\":\"documents/nested/copy.txt\"}");


            Assert.AreEqual(
                "source contents",
                File.ReadAllText(
                    Path.Combine(
                        _workspacePath,
                        "documents",
                        "nested",
                        "copy.txt")));


            await _runtime.ExecuteCapabilityAsync(
                "move_file",
                "{\"sourcePath\":\"documents/nested/copy.txt\"," +
                "\"destinationPath\":\"documents/moved.txt\"}");


            Assert.IsFalse(
                File.Exists(
                    Path.Combine(
                        _workspacePath,
                        "documents",
                        "nested",
                        "copy.txt")));


            string entriesJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory",
                    "{\"path\":\"documents\"}");


            JArray entries =
                JArray.Parse(
                    entriesJson);


            CollectionAssert.AreEqual(
                new[]
                {
                    "moved.txt",
                    "nested",
                    "source.txt"
                },
                entries
                    .Select(
                        entry =>
                            (string)entry["Name"])
                    .ToArray());

            string includingMetaJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory",
                    "{\"path\":\"documents\",\"entryType\":\"Files\",\"outputFormat\":\"PathsOnly\",\"includeMetaFiles\":true}");

            CollectionAssert.Contains(
                JArray.Parse(
                        includingMetaJson)
                    .Values<string>()
                    .ToArray(),
                "documents/source.txt.meta");

            CollectionAssert.AreEqual(
                new[]
                {
                    "documents/moved.txt",
                    "documents/nested",
                    "documents/source.txt"
                },
                entries
                    .Select(
                        entry =>
                            ((string)entry["Path"])
                                .Replace(
                                    '\\',
                                    '/'))
                    .ToArray());

            CollectionAssert.AreEqual(
                new[]
                {
                    false,
                    true,
                    false
                },
                entries
                    .Select(
                        entry =>
                            (bool)entry["IsDirectory"])
                    .ToArray());

            Assert.AreEqual(
                ".txt",
                (string)entries[0]["Extension"]);

            Assert.Greater(
                (long)entries[0]["Size"],
                0);

            Assert.IsFalse(
                (bool)entries[0]["IsSymbolicLink"]);

            Assert.IsNull(
                entries[0]["CreatedUtc"]);

            Assert.IsNull(
                entries[0]["ModifiedUtc"]);

            Assert.IsNull(
                entries[0]["Exists"]);


            string filesJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory",
                    "{\"path\":\"documents\",\"entryType\":\"Files\"}");

            CollectionAssert.AreEqual(
                new[]
                {
                    "moved.txt",
                    "source.txt"
                },
                JArray.Parse(
                        filesJson)
                    .Select(
                        entry =>
                            (string)entry["Name"])
                    .ToArray());


            string directoriesJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory",
                    "{\"path\":\"documents\",\"entryType\":\"Directories\"}");

            CollectionAssert.AreEqual(
                new[]
                {
                    "nested"
                },
                JArray.Parse(
                        directoriesJson)
                    .Select(
                        entry =>
                            (string)entry["Name"])
                    .ToArray());

            string pathsOnlyJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory",
                    "{\"path\":\"documents\",\"entryType\":\"Files\",\"outputFormat\":\"PathsOnly\"}");

            CollectionAssert.AreEqual(
                new[]
                {
                    "documents/moved.txt",
                    "documents/source.txt"
                },
                JArray.Parse(
                        pathsOnlyJson)
                    .Values<string>()
                    .ToArray());


            await _runtime.ExecuteCapabilityAsync(
                "delete_file",
                "{\"path\":\"documents/moved.txt\"}");


            Assert.IsFalse(
                File.Exists(
                    Path.Combine(
                        _workspacePath,
                        "documents",
                        "moved.txt")));


            await _runtime.ExecuteCapabilityAsync(
                "delete_directory",
                "{\"path\":\"documents\",\"recursive\":true}");


            Assert.IsFalse(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "documents")));
        }


        private async Task RunRejectMissingParentAsync()
        {
            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "Runtime",
                    "Docs"));


            const string requestedPath =
                "Runtime/Docs/FolderTest";

            ICapability capability =
                new CreateDirectoryCapability();

            IFileSystem fileSystem =
                new LocalFileSystem(
                    new Workspace(
                        _workspacePath));

            CapabilityContext context =
                new CapabilityContext
                {
                    Capability =
                        capability,

                    FileSystem =
                        fileSystem
                };


            try
            {
                await capability.ExecuteObjectAsync(
                    new CreateDirectoryArguments
                    {
                        ParentPath =
                            requestedPath,

                        DirectoryName =
                            "NewFolder"
                    },
                    context);

                Assert.Fail(
                    "The capability created a directory under a missing parent.");
            }
            catch (DirectoryNotFoundException exception)
            {
                StringAssert.Contains(
                    "Parent directory does not exist: Assets/Runtime/Docs/FolderTest",
                    exception.Message);
            }


            Assert.IsFalse(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "Assets",
                        "Runtime",
                        "Docs",
                        "NewFolder")));

            Assert.IsFalse(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "Assets",
                        "Runtime",
                        "Docs",
                        "FolderTest",
                        "NewFolder")));
        }


        private async Task RunCreateParentChainAsync()
        {
            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "Runtime"));


            string resultJson =
                await _runtime.ExecuteCapabilityAsync(
                    "create_directory",
                    "{\"parentPath\":\"Runtime/Docs/FolderTest\"," +
                    "\"directoryName\":\"NewFolder\"," +
                    "\"createParents\":true}");

            JObject result =
                JObject.Parse(
                    resultJson);


            Assert.AreEqual(
                "Assets/Runtime/Docs/FolderTest/NewFolder",
                (string)result["Path"]);

            Assert.IsTrue(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "Assets",
                        "Runtime",
                        "Docs",
                        "FolderTest",
                        "NewFolder")));
        }


        private async Task RunCreateNestedRelativeChainAsync()
        {
            string resultJson =
                await _runtime.ExecuteCapabilityAsync(
                    "create_directory",
                    "{\"parentPath\":\"Assets\"," +
                    "\"directoryName\":\"Assets/JumpingBallGame/Scripts\"," +
                    "\"createParents\":true}");

            JObject result =
                JObject.Parse(
                    resultJson);

            Assert.AreEqual(
                "Assets/JumpingBallGame/Scripts",
                (string)result["Path"]);

            Assert.IsTrue(
                Directory.Exists(
                    Path.Combine(
                        _workspacePath,
                        "Assets",
                        "JumpingBallGame",
                        "Scripts")));
        }


        private async Task RunRejectTrailingPeriodNameAsync()
        {
            await _runtime.ExecuteCapabilityAsync(
                "create_directory",
                "{\"parentPath\":\"Assets\"," +
                "\"directoryName\":\"JumpingBallGame.\"," +
                "\"createParents\":true}");
        }


        private static IEnumerable<ICapability> CreateCapabilities()
        {
            yield return new CopyFileCapability();
            yield return new CreateDirectoryCapability();
            yield return new DeleteDirectoryCapability();
            yield return new DeleteFileCapability();
            yield return new FileExistsCapability();
            yield return new GetFileInfoCapability();
            yield return new ListDirectoryCapability();
            yield return new ListDirectoryTreeCapability();
            yield return new MoveFileCapability();
        }
    }
}
