using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

using AIRuntime.Editor;

using NUnit.Framework;

using UnityEngine;


namespace AIRuntime.Tests
{
    public sealed class KanbanGraphLayoutTests
    {
        private Type _nodeType;

        private Type _layoutType;


        [SetUp]
        public void SetUp()
        {
            Assembly editorAssembly =
                typeof(AIRuntimeKanbanSession).Assembly;

            _nodeType =
                editorAssembly.GetType(
                    "AIRuntime.Editor.AIRuntimeKanbanGraphNode",
                    throwOnError: true);

            _layoutType =
                editorAssembly.GetType(
                    "AIRuntime.Editor.AIRuntimeKanbanGraphLayout",
                    throwOnError: true);
        }


        [Test]
        public void CalculatePositions_ConnectedTriangleUsesTwoDimensions()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "parent",
                    "Parent"));

            nodes.Add(
                CreateNode(
                    "implementation",
                    "Implementation",
                    parentTaskId: "parent"));

            nodes.Add(
                CreateNode(
                    "verification",
                    "Verification",
                    parentTaskId: "parent",
                    dependencyIds: new[]
                    {
                        "implementation"
                    }));

            IDictionary positions =
                CalculatePositions(
                    nodes);

            Vector2 parent =
                (Vector2)positions[
                    "parent"];

            Vector2 implementation =
                (Vector2)positions[
                    "implementation"];

            Vector2 verification =
                (Vector2)positions[
                    "verification"];

            Assert.Greater(Vector2.Distance(parent, implementation), 70f);
            Assert.Greater(Vector2.Distance(implementation, verification), 70f);
            Assert.Greater(Mathf.Abs(Vector3.Cross(implementation - parent, verification - parent).z), 1000f,
                "A connected triangle must occupy two dimensions rather than a dependency row.");
        }

        [Test]
        public void CalculatePositions_ToleratesCyclicLinks()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "first",
                    "First",
                    dependencyIds: new[]
                    {
                        "second"
                    }));

            nodes.Add(
                CreateNode(
                    "second",
                    "Second",
                    dependencyIds: new[]
                    {
                        "first"
                    }));

            IDictionary positions =
                CalculatePositions(
                    nodes);

            Assert.AreEqual(
                2,
                positions.Count);

            Assert.IsTrue(
                IsFinite(
                    (Vector2)positions[
                        "first"]));

            Assert.IsTrue(
                IsFinite(
                    (Vector2)positions[
                        "second"]));
        }


        [Test]
        public void AutoExpand_NewParallelChildReflowsExistingLayerWithoutOverlap()
        {
            IList initialNodes =
                CreateNodeList();

            initialNodes.Add(
                CreateNode(
                    "root",
                    "Root"));

            initialNodes.Add(
                CreateNode(
                    "first",
                    "First",
                    parentTaskId: "root"));

            IDictionary initialPositions =
                CalculatePositions(
                    initialNodes);

            IList expandedNodes =
                CreateNodeList();

            expandedNodes.Add(
                CreateNode(
                    "root",
                    "Root"));

            expandedNodes.Add(
                CreateNode(
                    "first",
                    "First",
                    parentTaskId: "root"));

            expandedNodes.Add(
                CreateNode(
                    "second",
                    "Second",
                    parentTaskId: "root"));

            object layoutUpdate =
                ReconcileTopologyLayout(
                    BuildTopologySignature(
                        initialNodes),
                    BuildTopologySignature(
                        expandedNodes),
                    initialPositions,
                    expandedNodes,
                    requiresPolicyUpgrade: false);

            Type updateType =
                layoutUpdate.GetType();

            Assert.IsTrue(
                (bool)updateType.GetField(
                        "DidAutoLayout")
                    .GetValue(
                        layoutUpdate));

            Assert.IsTrue(
                (bool)updateType.GetField(
                        "ShouldFit")
                    .GetValue(
                        layoutUpdate));

            IDictionary expandedPositions =
                (IDictionary)updateType.GetField(
                        "Positions")
                    .GetValue(
                        layoutUpdate);

            Vector2 initialFirst =
                (Vector2)initialPositions[
                    "first"];

            Vector2 expandedFirst =
                (Vector2)expandedPositions[
                    "first"];

            Vector2 expandedSecond =
                (Vector2)expandedPositions[
                    "second"];

            Assert.AreNotEqual(
                initialFirst.y,
                expandedFirst.y,
                "The existing sibling must move into the expanded full-layout slot.");

            Assert.GreaterOrEqual(
                Vector2.Distance(expandedSecond, expandedFirst),
                100f,
                "Expanded node labels must have space.");
        }


        [Test]
        public void AutoExpand_FirstRefreshPreservesCompleteSavedLayout()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "root",
                    "Root"));

            nodes.Add(
                CreateNode(
                    "child",
                    "Child",
                    parentTaskId: "root"));

            var manualPositions =
                new Dictionary<string, Vector2>(
                    StringComparer.Ordinal)
                {
                    ["root"] =
                        new Vector2(
                            25f,
                            40f),

                    ["child"] =
                        new Vector2(
                            480f,
                            275f)
                };

            object layoutUpdate =
                ReconcileTopologyLayout(
                    string.Empty,
                    BuildTopologySignature(
                        nodes),
                    manualPositions,
                    nodes,
                    requiresPolicyUpgrade: false);

            Type updateType =
                layoutUpdate.GetType();

            Assert.IsFalse(
                (bool)updateType.GetField(
                        "DidAutoLayout")
                    .GetValue(
                        layoutUpdate),
                "A domain reload must not erase a complete manual layout.");

            Assert.IsFalse(
                (bool)updateType.GetField(
                        "ShouldFit")
                    .GetValue(
                        layoutUpdate));

            IDictionary reconciled =
                (IDictionary)updateType.GetField(
                        "Positions")
                    .GetValue(
                        layoutUpdate);

            Assert.AreEqual(
                manualPositions[
                    "root"],
                (Vector2)reconciled[
                    "root"]);

            Assert.AreEqual(
                manualPositions[
                    "child"],
                (Vector2)reconciled[
                    "child"]);
        }


        [Test]
        public void AutoExpand_PolicyUpgradeRepairsExistingCompressedLayout()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "root",
                    "Root"));

            nodes.Add(
                CreateNode(
                    "first",
                    "First",
                    parentTaskId: "root"));

            nodes.Add(
                CreateNode(
                    "second",
                    "Second",
                    parentTaskId: "root"));

            var compressedPositions =
                new Dictionary<string, Vector2>(
                    StringComparer.Ordinal)
                {
                    ["root"] =
                        new Vector2(
                            60f,
                            60f),

                    ["first"] =
                        new Vector2(
                            380f,
                            60f),

                    ["second"] =
                        new Vector2(
                            380f,
                            138f)
                };

            string signature =
                BuildTopologySignature(
                    nodes);

            object layoutUpdate =
                ReconcileTopologyLayout(
                    signature,
                    signature,
                    compressedPositions,
                    nodes,
                    requiresPolicyUpgrade: true);

            Type updateType =
                layoutUpdate.GetType();

            Assert.IsTrue(
                (bool)updateType.GetField(
                        "DidAutoLayout")
                    .GetValue(
                        layoutUpdate));

            Assert.IsTrue(
                (bool)updateType.GetField(
                        "ShouldFit")
                    .GetValue(
                        layoutUpdate));

            IDictionary repaired =
                (IDictionary)updateType.GetField(
                        "Positions")
                    .GetValue(
                        layoutUpdate);

            Assert.GreaterOrEqual(
                Vector2.Distance((Vector2)repaired["second"], (Vector2)repaired["first"]),
                100f);
        }


        [Test]
        public void TopologySignature_BlockerChangeTriggersAutoExpand()
        {
            IList unblocked =
                CreateNodeList();

            unblocked.Add(
                CreateNode(
                    "producer",
                    "Producer"));

            unblocked.Add(
                CreateNode(
                    "consumer",
                    "Consumer"));

            IList blocked =
                CreateNodeList();

            blocked.Add(
                CreateNode(
                    "producer",
                    "Producer"));

            blocked.Add(
                CreateNode(
                    "consumer",
                    "Consumer",
                    blockedByTaskId: "producer"));

            string previousSignature =
                BuildTopologySignature(
                    unblocked);

            string currentSignature =
                BuildTopologySignature(
                    blocked);

            Assert.AreNotEqual(
                previousSignature,
                currentSignature);

            Assert.IsTrue(
                ShouldAutoLayoutTopologyChange(
                    previousSignature,
                    currentSignature,
                    new[]
                    {
                        "producer",
                        "consumer"
                    },
                    blocked));
        }


        [Test]
        public void TopologySignature_DuplicateDependencyBlockerDoesNotResetLayout()
        {
            IList dependencyOnly =
                CreateNodeList();

            dependencyOnly.Add(
                CreateNode(
                    "producer",
                    "Producer"));

            dependencyOnly.Add(
                CreateNode(
                    "consumer",
                    "Consumer",
                    dependencyIds: new[]
                    {
                        "producer"
                    }));

            IList duplicateBlocker =
                CreateNodeList();

            duplicateBlocker.Add(
                CreateNode(
                    "producer",
                    "Producer"));

            duplicateBlocker.Add(
                CreateNode(
                    "consumer",
                    "Consumer",
                    dependencyIds: new[]
                    {
                        "producer"
                    },
                    blockedByTaskId: "producer"));

            string previousSignature =
                BuildTopologySignature(
                    dependencyOnly);

            string currentSignature =
                BuildTopologySignature(
                    duplicateBlocker);

            Assert.AreEqual(
                previousSignature,
                currentSignature);

            Assert.IsFalse(
                ShouldAutoLayoutTopologyChange(
                    previousSignature,
                    currentSignature,
                    new[]
                    {
                        "producer",
                        "consumer"
                    },
                    duplicateBlocker));
        }


        [Test]
        public void Associations_ChangeTopologyAndPullNodesTogether()
        {
            IList withoutAssociation =
                CreateNodeList();

            withoutAssociation.Add(
                CreateNode(
                    "first",
                    "First"));

            withoutAssociation.Add(
                CreateNode(
                    "second",
                    "Second"));

            IList withAssociation =
                CreateNodeList();

            withAssociation.Add(
                CreateNode(
                    "first",
                    "First",
                    associationIds: new[]
                    {
                        "second"
                    }));

            withAssociation.Add(
                CreateNode(
                    "second",
                    "Second",
                    associationIds: new[]
                    {
                        "first"
                    }));

            Assert.AreNotEqual(
                BuildTopologySignature(
                    withoutAssociation),
                BuildTopologySignature(
                    withAssociation));

            IDictionary withoutPositions =
                CalculatePositions(
                    withoutAssociation);

            IDictionary withPositions =
                CalculatePositions(
                    withAssociation);

            Assert.Less(Vector2.Distance((Vector2)withPositions["first"], (Vector2)withPositions["second"]),
                Vector2.Distance((Vector2)withoutPositions["first"], (Vector2)withoutPositions["second"]));
        }

        [Test]
        public void VisibleAssociationEdges_DrawsPairOnceAndSuppressesStructuralLinks()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "parent",
                    "Parent",
                    associationIds: new[]
                    {
                        "child"
                    }));

            nodes.Add(
                CreateNode(
                    "child",
                    "Child",
                    parentTaskId: "parent"));

            nodes.Add(
                CreateNode(
                    "dependency",
                    "Dependency",
                    associationIds: new[]
                    {
                        "consumer"
                    }));

            nodes.Add(
                CreateNode(
                    "consumer",
                    "Consumer",
                    dependencyIds: new[]
                    {
                        "dependency"
                    }));

            nodes.Add(
                CreateNode(
                    "blocker",
                    "Blocker",
                    associationIds: new[]
                    {
                        "blocked"
                    }));

            nodes.Add(
                CreateNode(
                    "blocked",
                    "Blocked",
                    blockedByTaskId: "blocker"));

            nodes.Add(
                CreateNode(
                    "first",
                    "First",
                    associationIds: new[]
                    {
                        "second",
                        "missing",
                        "first"
                    }));

            nodes.Add(
                CreateNode(
                    "second",
                    "Second",
                    associationIds: new[]
                    {
                        "first"
                    }));

            MethodInfo method =
                typeof(AIRuntimeKanbanGraphWindow).GetMethod(
                    "GetVisibleAssociationEdges",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                method);

            IList edges =
                (IList)method.Invoke(
                    null,
                    new object[]
                    {
                        nodes
                    });

            Assert.AreEqual(
                1,
                edges.Count);

            object edge =
                edges[0];

            Assert.AreEqual(
                "first",
                edge.GetType()
                    .GetField("FirstTaskId")
                    .GetValue(edge));

            Assert.AreEqual(
                "second",
                edge.GetType()
                    .GetField("SecondTaskId")
                    .GetValue(edge));
        }


        [Test]
        public void CalculatePositions_ClustersConnectedBranches()
        {
            IList nodes =
                CreateNodeList();

            nodes.Add(
                CreateNode(
                    "z-parent",
                    "Alpha parent"));

            nodes.Add(
                CreateNode(
                    "a-parent",
                    "Zulu parent"));

            nodes.Add(
                CreateNode(
                    "alpha-child",
                    "Zulu child title",
                    parentTaskId: "z-parent"));

            nodes.Add(
                CreateNode(
                    "zulu-child",
                    "Alpha child title",
                    parentTaskId: "a-parent"));

            IDictionary positions =
                CalculatePositions(
                    nodes);

            float linked = Vector2.Distance((Vector2)positions["z-parent"], (Vector2)positions["alpha-child"])
                + Vector2.Distance((Vector2)positions["a-parent"], (Vector2)positions["zulu-child"]);
            float unrelated = Vector2.Distance((Vector2)positions["z-parent"], (Vector2)positions["zulu-child"])
                + Vector2.Distance((Vector2)positions["a-parent"], (Vector2)positions["alpha-child"]);
            Assert.Less(linked, unrelated, "Connected branches should cluster together.");
        }

        [Test]
        public void EnsureStyles_RebuildsAnyMissingStyleAfterHotReload()
        {
            AIRuntimeKanbanGraphWindow window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanGraphWindow>();

            try
            {
                MethodInfo ensureStyles =
                    typeof(AIRuntimeKanbanGraphWindow).GetMethod(
                        "EnsureStyles",
                        BindingFlags.Instance |
                        BindingFlags.NonPublic);

                Assert.NotNull(
                    ensureStyles);

                ensureStyles.Invoke(
                    window,
                    null);

                foreach (string fieldName in new[]
                         {
                             "_nodeTitleStyle",
                             "_nodeMetaStyle",
                             "_nodeStatusStyle",
                             "_nodePriorityStyle"
                         })
                {
                    FieldInfo styleField =
                        typeof(AIRuntimeKanbanGraphWindow).GetField(
                            fieldName,
                            BindingFlags.Instance |
                            BindingFlags.NonPublic);

                    Assert.NotNull(
                        styleField,
                        fieldName);

                    styleField.SetValue(
                        window,
                        null);

                    Assert.DoesNotThrow(
                        () =>
                            ensureStyles.Invoke(
                                window,
                                null),
                        fieldName);

                    Assert.NotNull(
                        styleField.GetValue(
                            window),
                        fieldName);
                }
            }
            finally
            {
                ScriptableObject.DestroyImmediate(
                    window);
            }
        }

        [Test]
        public void ForceLayout_NineTaskProjectIsCompactAndLabelsDoNotOverlap()
        {
            IList nodes = CreateNodeList();
            nodes.Add(CreateNode("root", "Build game"));
            for (int i = 1; i < 9; i++)
                nodes.Add(CreateNode("task-" + i, "Task " + i, parentTaskId: "root",
                    dependencyIds: i > 1 ? new[] { "task-" + (i - 1) } : null));
            IDictionary layout = CalculatePositions(nodes);
            var points = new List<Vector2>();
            foreach (Vector2 point in layout.Values) points.Add(point);
            float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
            for (int i = 0; i < points.Count; i++)
            {
                Assert.IsTrue(IsFinite(points[i]));
                minX = Mathf.Min(minX, points[i].x); maxX = Mathf.Max(maxX, points[i].x);
                minY = Mathf.Min(minY, points[i].y); maxY = Mathf.Max(maxY, points[i].y);
                for (int j = i + 1; j < points.Count; j++)
                    Assert.IsTrue(Mathf.Abs(points[i].x - points[j].x) >= 180 || Mathf.Abs(points[i].y - points[j].y) >= 76,
                        "Node label footprints must not overlap.");
            }
            Assert.Greater(maxY - minY, 200, "The project must not collapse into a horizontal row.");
            Assert.Less((maxX - minX) / (maxY - minY), 3f, "Use the canvas height as well as its width.");
            IList reversed = CreateNodeList();
            for (int i = nodes.Count - 1; i >= 0; i--) reversed.Add(nodes[i]);
            IDictionary again = CalculatePositions(reversed);
            foreach (string id in layout.Keys) Assert.AreEqual(layout[id], again[id], "Input order must not shuffle the graph.");
        }


        private IList CreateNodeList()
        {
            Type listType =
                typeof(List<>).MakeGenericType(
                    _nodeType);

            return (IList)Activator.CreateInstance(
                listType);
        }


        private object CreateNode(
            string id,
            string title,
            string parentTaskId = "",
            IEnumerable<string> dependencyIds = null,
            string blockedByTaskId = "",
            IEnumerable<string> associationIds = null)
        {
            object node =
                Activator.CreateInstance(
                    _nodeType);

            SetField(
                node,
                "Id",
                id);

            SetField(
                node,
                "Title",
                title);

            SetField(
                node,
                "ParentTaskId",
                parentTaskId);

            SetField(
                node,
                "DependsOnTaskIds",
                new List<string>(
                    dependencyIds ??
                    Array.Empty<string>()));

            SetField(
                node,
                "BlockedByTaskId",
                blockedByTaskId);

            SetField(
                node,
                "AssociatedTaskIds",
                new List<string>(
                    associationIds ??
                    Array.Empty<string>()));

            return node;
        }


        private IDictionary CalculatePositions(
            IList nodes)
        {
            MethodInfo method =
                _layoutType.GetMethod(
                    "CalculatePositions",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                method);

            return (IDictionary)method.Invoke(
                null,
                new object[]
                {
                    nodes
                });
        }


        private string BuildTopologySignature(
            IList nodes)
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanGraphWindow).GetMethod(
                    "BuildTopologySignature",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                method);

            return (string)method.Invoke(
                null,
                new object[]
                {
                    nodes
                });
        }


        private bool ShouldAutoLayoutTopologyChange(
            string previousSignature,
            string currentSignature,
            IEnumerable<string> savedTaskIds,
            IList nodes)
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanGraphWindow).GetMethod(
                    "ShouldAutoLayoutTopologyChange",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                method);

            return (bool)method.Invoke(
                null,
                new object[]
                {
                    previousSignature,
                    currentSignature,
                    new List<string>(
                        savedTaskIds ??
                        Array.Empty<string>()),
                    nodes
                });
        }


        private object ReconcileTopologyLayout(
            string previousSignature,
            string currentSignature,
            IDictionary savedPositions,
            IList nodes,
            bool requiresPolicyUpgrade)
        {
            var typedSavedPositions =
                new Dictionary<string, Vector2>(
                    StringComparer.Ordinal);

            foreach (DictionaryEntry entry in savedPositions)
            {
                typedSavedPositions[
                        (string)entry.Key] =
                    (Vector2)entry.Value;
            }

            MethodInfo method =
                typeof(AIRuntimeKanbanGraphWindow).GetMethod(
                    "ReconcileTopologyLayout",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                method);

            return method.Invoke(
                null,
                new object[]
                {
                    previousSignature,
                    currentSignature,
                    typedSavedPositions,
                    nodes,
                    requiresPolicyUpgrade
                });
        }


        private void SetField(
            object target,
            string fieldName,
            object value)
        {
            FieldInfo field =
                _nodeType.GetField(
                    fieldName,
                    BindingFlags.Instance |
                    BindingFlags.Public);

            Assert.NotNull(
                field);

            field.SetValue(
                target,
                value);
        }


        private static bool IsFinite(
            Vector2 value)
        {
            return !float.IsNaN(
                       value.x) &&
                   !float.IsInfinity(
                       value.x) &&
                   !float.IsNaN(
                       value.y) &&
                   !float.IsInfinity(
                       value.y);
        }
    }
}
