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

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Filesystem;

using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEngine;
using UnityEngine.TestTools;


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

        private AIRuntime.Runtime.AIRuntime _runtime;


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

            Directory.CreateDirectory(
                _workspacePath);

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

            _runtime.RegisterCapability(
                new ListDirectoryTreeCapability());
        }


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


        [Test]
        public void ExposesReadOnlyContractAndBoundedSchema()
        {
            ListDirectoryTreeCapability capability =
                new ListDirectoryTreeCapability();


            Assert.AreEqual(
                "list_directory_tree",
                capability.Name);

            Assert.AreEqual(
                AIRuntime.Runtime.Capabilities.Permissions.CapabilityPermission.ReadOnly,
                capability.Permission);

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

            Assert.AreEqual(
                10,
                capability.Parameters["properties"]?["maxDepth"]?["default"]?
                    .Value<int>());

            Assert.AreEqual(
                10000,
                capability.Parameters["properties"]?["maxEntries"]?["default"]?
                    .Value<int>());

            Assert.AreEqual(
                true,
                capability.Parameters["properties"]?["directoriesOnly"]?["default"]?
                    .Value<bool>());

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

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

            Assert.AreEqual(
                "array",
                capability.Parameters["properties"]?["exclude"]?["type"]?
                    .Value<string>());
        }


        [UnityTest]
        public IEnumerator ReturnsRecursiveTypedEntriesAndHonorsExclusions()
        {
            return Await(
                RunRecursiveTraversalAsync());
        }


        [UnityTest]
        public IEnumerator EnforcesDepthAndEntryBounds()
        {
            return Await(
                RunBoundsAsync());
        }


        [UnityTest]
        public IEnumerator RejectsPathsOutsideWorkspace()
        {
            return Await(
                RunWorkspaceContainmentAsync());
        }


        [UnityTest]
        public IEnumerator EmptyPathUsesWorkspaceRoot()
        {
            return Await(
                RunEmptyPathAsync());
        }


        [UnityTest]
        public IEnumerator DefaultsReturnCompleteFolderTreeOnly()
        {
            return Await(
                RunDefaultFolderTreeAsync());
        }


        [UnityTest]
        public IEnumerator ExcludesUnityMetaSidecarsUnlessExplicitlyIncluded()
        {
            return Await(
                RunMetaFileFilteringAsync());
        }


        private async Task RunRecursiveTraversalAsync()
        {
            CreateFixture();


            string json =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\".\",\"maxDepth\":3," +
                    "\"directoriesOnly\":true," +
                    "\"exclude\":[\"Library\"]}");

            JObject result =
                JObject.Parse(
                    json);

            JArray entries =
                (JArray)result["Entries"];


            CollectionAssert.AreEqual(
                new[]
                {
                    "Assets",
                    "Assets/Editor",
                    "Assets/Scripts",
                    "Assets/Scripts/Runtime"
                },
                entries
                    .Select(
                        entry =>
                            (string)entry["Path"])
                    .ToArray());

            Assert.IsTrue(
                entries.All(
                    entry =>
                        (bool)entry["IsDirectory"]));

            Assert.IsFalse(
                entries.Any(
                    entry =>
                        ((string)entry["Path"])
                            .StartsWith(
                                "Library",
                                StringComparison.OrdinalIgnoreCase)));

            Assert.IsFalse(
                entries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "README.md",
                            StringComparison.Ordinal)));

            Assert.IsFalse(
                entries.Any(
                    entry =>
                        ((string)entry["Path"])
                            .StartsWith(
                                "Library",
                                StringComparison.OrdinalIgnoreCase)));

            Assert.IsFalse(
                (bool)result["Truncated"]);


            string generatedJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"includeGenerated\":true}");

            JArray generatedEntries =
                (JArray)JObject.Parse(
                    generatedJson)["Entries"];


            Assert.IsTrue(
                generatedEntries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "Library/Cache",
                            StringComparison.Ordinal)));
        }


        private async Task RunBoundsAsync()
        {
            CreateFixture();


            string depthJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\".\",\"maxDepth\":1," +
                    "\"maxEntries\":100,\"directoriesOnly\":false}");

            JObject depthResult =
                JObject.Parse(
                    depthJson);

            JArray depthEntries =
                (JArray)depthResult["Entries"];


            Assert.IsTrue(
                depthEntries.All(
                    entry =>
                        (int)entry["Depth"] ==
                        1));

            Assert.IsTrue(
                depthEntries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "README.md",
                            StringComparison.Ordinal) &&
                        !(bool)entry["IsDirectory"]));


            string entryJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\".\",\"maxDepth\":3," +
                    "\"maxEntries\":1,\"directoriesOnly\":false}");

            JObject entryResult =
                JObject.Parse(
                    entryJson);


            Assert.IsTrue(
                (bool)entryResult["Truncated"]);

            Assert.GreaterOrEqual(
                (int)entryResult["InspectedEntries"],
                2);

            Assert.AreEqual(
                1,
                ((JArray)entryResult["Entries"]).Count);
        }


        private async Task RunWorkspaceContainmentAsync()
        {
            LogAssert.Expect(
                LogType.Error,
                "[Capability] 'list_directory_tree' failed: Path '../outside' escapes the workspace.");


            try
            {
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\"../outside\"}");

                Assert.Fail(
                    "Expected the capability to reject a path outside the workspace.");
            }
            catch (ValidationException)
            {
                // Expected: workspace validation rejects parent traversal.
            }
        }


        private async Task RunEmptyPathAsync()
        {
            CreateFixture();


            string json =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\"\",\"maxDepth\":1}");

            JArray entries =
                (JArray)JObject.Parse(
                    json)["Entries"];


            Assert.IsTrue(
                entries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "Assets",
                            StringComparison.Ordinal)));
        }


        private async Task RunDefaultFolderTreeAsync()
        {
            CreateFixture();

            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "One",
                    "Two",
                    "Three",
                    "Four",
                    "Five"));


            string json =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{}");

            JObject result =
                JObject.Parse(
                    json);

            JArray entries =
                (JArray)result["Entries"];


            Assert.IsTrue(
                entries.All(
                    entry =>
                        (bool)entry["IsDirectory"]));

            Assert.IsTrue(
                entries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "Assets/One/Two/Three/Four/Five",
                            StringComparison.Ordinal)));

            Assert.IsFalse(
                entries.Any(
                    entry =>
                        string.Equals(
                            (string)entry["Path"],
                            "README.md",
                            StringComparison.Ordinal)));

            Assert.IsFalse(
                (bool)result["Truncated"]);
        }


        private async Task RunMetaFileFilteringAsync()
        {
            string docs =
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "Runtime",
                    "Docs");

            string nested =
                Path.Combine(
                    docs,
                    "FolderTest");

            Directory.CreateDirectory(
                nested);

            File.WriteAllText(
                Path.Combine(
                    docs,
                    "ARCHITECTURE.md"),
                "architecture");

            File.WriteAllText(
                Path.Combine(
                    docs,
                    "ARCHITECTURE.md.meta"),
                "file metadata");

            File.WriteAllText(
                Path.Combine(
                    docs,
                    "FolderTest.meta"),
                "folder metadata");


            string filteredJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\"Assets/Runtime/Docs\",\"maxDepth\":2," +
                    "\"directoriesOnly\":false,\"includeMetaFiles\":false}");

            JArray filteredEntries =
                (JArray)JObject.Parse(
                    filteredJson)["Entries"];

            string[] filteredPaths =
                filteredEntries
                    .Select(
                        entry =>
                            (string)entry["Path"])
                    .ToArray();

            CollectionAssert.Contains(
                filteredPaths,
                "Assets/Runtime/Docs/ARCHITECTURE.md");

            CollectionAssert.Contains(
                filteredPaths,
                "Assets/Runtime/Docs/FolderTest");

            Assert.IsFalse(
                filteredPaths.Any(
                    path =>
                        path.EndsWith(
                            ".meta",
                            StringComparison.OrdinalIgnoreCase)));


            string includedJson =
                await _runtime.ExecuteCapabilityAsync(
                    "list_directory_tree",
                    "{\"path\":\"Assets/Runtime/Docs\",\"maxDepth\":2," +
                    "\"directoriesOnly\":false,\"includeMetaFiles\":true}");

            string[] includedPaths =
                ((JArray)JObject.Parse(
                    includedJson)["Entries"])
                    .Select(
                        entry =>
                            (string)entry["Path"])
                    .ToArray();

            CollectionAssert.Contains(
                includedPaths,
                "Assets/Runtime/Docs/ARCHITECTURE.md.meta");

            CollectionAssert.Contains(
                includedPaths,
                "Assets/Runtime/Docs/FolderTest.meta");
        }


        private void CreateFixture()
        {
            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "Scripts",
                    "Runtime"));

            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Assets",
                    "Editor"));

            Directory.CreateDirectory(
                Path.Combine(
                    _workspacePath,
                    "Library",
                    "Cache"));

            File.WriteAllText(
                Path.Combine(
                    _workspacePath,
                    "README.md"),
                "project");
        }


        private static IEnumerator Await(
            Task task)
        {
            while (!task.IsCompleted)
            {
                yield return null;
            }


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


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