using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.TestTools.Utils;
using Matrix4x4 = UnityEngine.Matrix4x4;
using Quaternion = UnityEngine.Quaternion;
using Vector3 = UnityEngine.Vector3;
using From = Unity.AI.Navigation.Editor.Tests.NavMeshLinkEditorTests.LinkEndType;
using To = Unity.AI.Navigation.Editor.Tests.NavMeshLinkEditorTests.LinkEndType;

// Note: To pause and inspect the state during these editor tests,
// run them in playmode from the NavMeshLinkEditorTestsInPlaymode class
// in the Unity.AI.Navigation.Editor.Tests.InPlaymode namespace.
namespace Unity.AI.Navigation.Editor.Tests
{
    public class NavMeshLinkEditorTests
    {
        List<UnityEngine.Object> m_TestObjects = new();
        GameObject m_LinkGameObject;
        GameObject m_Start;
        GameObject m_End;
        NavMeshLink m_Link;
        NavMeshLink m_LinkSibling1;
        NavMeshLink m_LinkSibling2;
        GameObject m_StartOnNavMesh;
        GameObject m_EndOnNavMesh;
        const int k_NotWalkable = 1;
        const int k_Walkable = 0;

        static readonly Vector3EqualityComparer k_DefaultThreshold = Vector3EqualityComparer.Instance;

        GameObject CreateTestObject(string name, params Type[] components)
        {
            var go = new GameObject(name, components);
            m_TestObjects.Add(go);
            return go;
        }

        [OneTimeSetUp]
        public void OneTimeSetup()
        {
            m_LinkGameObject = new GameObject("Link");
            m_Link = m_LinkGameObject.AddComponent<NavMeshLink>();
            m_Start = new GameObject("Start");
            m_End = new GameObject("End");

            m_StartOnNavMesh = GameObject.CreatePrimitive(PrimitiveType.Plane);
            m_StartOnNavMesh.gameObject.transform.position = new Vector3(20.0f, 0.0f, 20.0f);

            m_EndOnNavMesh = GameObject.CreatePrimitive(PrimitiveType.Plane);
            m_EndOnNavMesh.gameObject.transform.position = new Vector3(20.0f, 0.0f, 40.0f);

            var surface = m_StartOnNavMesh.gameObject.AddComponent<NavMeshSurface>();
            surface.BuildNavMesh();

            // To debug, add these components, only to show icons for them in the scene
            //m_Start.AddComponent<NavMeshSurface>().enabled = false;
            //m_End.AddComponent<NavMeshModifier>().enabled = false;
        }

        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            if (m_LinkGameObject != null)
                UnityEngine.Object.DestroyImmediate(m_LinkGameObject);
            if (m_Start != null)
                UnityEngine.Object.DestroyImmediate(m_Start);
            if (m_End != null)
                UnityEngine.Object.DestroyImmediate(m_End);
            if (m_StartOnNavMesh != null)
                UnityEngine.Object.DestroyImmediate(m_StartOnNavMesh);
            if (m_EndOnNavMesh != null)
                UnityEngine.Object.DestroyImmediate(m_EndOnNavMesh);
        }

        [SetUp]
        public void Setup()
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.area = k_Walkable;
                m_Link.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
                m_Link.transform.localScale = Vector3.one;
                m_Link.startPoint = Vector3.left;
                m_Link.endPoint = Vector3.right;
            }
        }

        [TearDown]
        public void TearDown()
        {
            if (m_LinkSibling1 != null)
                UnityEngine.Object.DestroyImmediate(m_LinkSibling1);

            if (m_LinkSibling2 != null)
                UnityEngine.Object.DestroyImmediate(m_LinkSibling2);

            foreach (var obj in m_TestObjects)
            {
                if (obj != null)
                    UnityEngine.Object.DestroyImmediate(obj);
            }

            m_TestObjects.Clear();
        }

        protected static readonly Vector3[] k_ReverseDirectionPositions =
            { Vector3.zero, new(1f, 2f, 3f), new(1f, -2f, 3f) };
        protected static readonly Quaternion[] k_ReverseDirectionOrientations =
            { Quaternion.identity, new(0f, 0.7071067812f, 0f, 0.7071067812f) };
        protected static readonly Vector3[] k_ReverseDirectionScales =
            { Vector3.one, new(0.5f, 1f, 2f), new(0.5f, -1f, 2f) };

        [Test]
        public void ReverseDirection_SwapsStartAndEndPoints(
            [ValueSource(nameof(k_ReverseDirectionPositions))]
            Vector3 position,
            [ValueSource(nameof(k_ReverseDirectionOrientations))]
            Quaternion orientation,
            [ValueSource(nameof(k_ReverseDirectionScales))]
            Vector3 scale
        )
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.transform.SetPositionAndRotation(position, orientation);
                m_Link.transform.localScale = scale;
                m_Link.startPoint = new Vector3(2f, 0f, 0f);
                m_Link.endPoint = new Vector3(0f, 0f, 2f);
            }

            NavMeshLinkEditor.ReverseDirection(m_Link);

            Assert.That(
                (m_Link.startPoint, m_Link.endPoint),
                Is.EqualTo((new Vector3(0f, 0f, 2f), new Vector3(2f, 0f, 0f))),
                "Start and end points did not swap."
            );
        }

        [Test]
        public void ReverseDirection_SwapsStartAndEndPoints_TargetTransformsDoNotAffect(
            [ValueSource(nameof(k_ReverseDirectionPositions))]
            Vector3 position,
            [ValueSource(nameof(k_ReverseDirectionOrientations))]
            Quaternion orientation,
            [ValueSource(nameof(k_ReverseDirectionScales))]
            Vector3 scale
        )
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startTransform = CreateTestObject("Start").transform;
                m_Link.endTransform = CreateTestObject("End").transform;
                m_Link.transform.SetPositionAndRotation(position, orientation);
                m_Link.transform.localScale = scale;
                m_Link.startPoint = new Vector3(2f, 0f, 0f);
                m_Link.endPoint = new Vector3(0f, 0f, 2f);
            }

            NavMeshLinkEditor.ReverseDirection(m_Link);

            Assert.That(
                (m_Link.startPoint, m_Link.endPoint),
                Is.EqualTo((new Vector3(0f, 0f, 2f), new Vector3(2f, 0f, 0f))),
                "Start and end points did not swap."
            );
        }

        protected static readonly TestCaseData[] k_LinkEnabledInitially =
        {
            new TestCaseData(true).SetName("Enabled before setting Not Walkable"),
            new TestCaseData(false).SetName("Disabled before setting Not Walkable")
        };

        [Test]
        [TestCaseSource(nameof(k_LinkEnabledInitially))]
        public void NavMeshLink_AfterSwitchingFromNonWalkableAreaType_BecomesWalkable(bool enabledInitially)
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startTransform = m_StartOnNavMesh.transform;
                m_Link.endTransform = m_EndOnNavMesh.transform;
                m_Link.area = k_Walkable;
            }

            var navMeshLinkObject = new SerializedObject(m_Link.GetComponent<NavMeshLink>());
            var navMeshLinkEnabled = navMeshLinkObject.FindProperty("m_Enabled");
            var areaTypeProperty = navMeshLinkObject.FindProperty("m_Area");

            var path = new NavMeshPath();

            NavMesh.CalculatePath(m_StartOnNavMesh.transform.position, m_EndOnNavMesh.transform.position, NavMesh.AllAreas, path);
            Assume.That(path.status, Is.EqualTo(NavMeshPathStatus.PathComplete));

            navMeshLinkEnabled.boolValue = enabledInitially;
            navMeshLinkObject.ApplyModifiedProperties();

            areaTypeProperty.intValue = k_NotWalkable;
            navMeshLinkObject.ApplyModifiedProperties();

            NavMesh.CalculatePath(m_StartOnNavMesh.transform.position, m_EndOnNavMesh.transform.position, NavMesh.AllAreas, path);
            Assume.That(path.status, Is.Not.EqualTo(NavMeshPathStatus.PathComplete));

            navMeshLinkEnabled.boolValue = true;
            navMeshLinkObject.ApplyModifiedProperties();

            areaTypeProperty.intValue = k_Walkable;
            navMeshLinkObject.ApplyModifiedProperties();

            NavMesh.CalculatePath(m_StartOnNavMesh.transform.position, m_EndOnNavMesh.transform.position, NavMesh.AllAreas, path);
            Assert.That(path.status, Is.EqualTo(NavMeshPathStatus.PathComplete));
        }

        [Test]
        public void ReverseDirection_SwapsStartAndEndTransforms()
        {
            var start = m_Link.startTransform = CreateTestObject("Start").transform;
            var end = m_Link.endTransform = CreateTestObject("End").transform;

            NavMeshLinkEditor.ReverseDirection(m_Link);

            Assert.That(
                (m_Link.startTransform, m_Link.endTransform),
                Is.EqualTo((end, start)),
                "Start and end transform did not swap."
            );
        }

        [Test]
        public void ReverseDirection_OneTransformIsNotSet_SwapsStartAndEndTransforms()
        {
            var start = m_Link.startTransform = CreateTestObject("Start").transform;
            var end = m_Link.endTransform;

            NavMeshLinkEditor.ReverseDirection(m_Link);

            Assert.That(
                (m_Link.startTransform, m_Link.endTransform),
                Is.EqualTo((end, start)),
                "Start and end transform did not swap."
            );
        }

        static readonly Vector3 k_Offset101 = new(1, 1, 1);
        static readonly Vector3 k_Offset103 = new(1, 1, 3);
        static readonly Quaternion k_DoNotRotate = Quaternion.identity;
        static readonly Quaternion k_FlipToRight = Quaternion.Euler(0, 0, -90);
        static readonly Quaternion k_UpSideDown = Quaternion.Euler(0, 0, 180);

        //TestCaseData( startType, endType, transformRotation,
        //      expectedPosition, expectedForward)
        protected static readonly TestCaseData[] k_PointsOnly =
        {
            new(From.Point, To.Point, k_DoNotRotate,
                new Vector3(1, 1, 2), Vector3.back),

            new(From.Point, To.Point, k_UpSideDown,
                new Vector3(-1, -1, 2), Vector3.back),
        };
        protected static readonly TestCaseData[] k_PointAndTransforms =
        {
            new(From.Point, To.Transform, k_DoNotRotate,
                new Vector3(1, 2, 2), Vector3.back),

            new(From.Point, To.Transform, k_FlipToRight,
                new Vector3(1, 1, 2), Quaternion.Euler(-116.565f, 0, -90) * Vector3.forward),

            new(From.Transform, To.Point, k_DoNotRotate,
                new Vector3(1, 2, 2), Vector3.back),

            new(From.Transform, To.Point, k_FlipToRight,
                new Vector3(1, 1, 2), Quaternion.Euler(116.565f, 0, -90) * Vector3.forward),

            new(From.Transform, To.Transform, k_DoNotRotate,
                new Vector3(1, 3, 2), Vector3.back),

            new(From.Transform, To.Transform, k_UpSideDown,
                new Vector3(1, 3, 2), Vector3.back),
        };
        protected static readonly TestCaseData[] k_ChildTransforms =
        {
            new(From.TransformChild, To.TransformChild, k_FlipToRight,
                new Vector3(3, -1, 2), Vector3.back),
        };

        void ConfigureLinkForTest(From startType, To endType, Quaternion transformRotation)
        {
            m_Start.transform.position = k_Offset103 + 2f * Vector3.up;
            m_End.transform.position = k_Offset101 + 2f * Vector3.up;
            m_Start.transform.parent = startType == From.TransformChild ? m_Link.transform : null;
            m_End.transform.parent = endType == To.TransformChild ? m_Link.transform : null;

            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startPoint = k_Offset103;
                m_Link.endPoint = k_Offset101;
                m_Link.startTransform = startType != From.Point ? m_Start.transform : null;
                m_Link.endTransform = endType != To.Point ? m_End.transform : null;

                m_Link.transform.rotation = transformRotation;
            }
        }

        [TestCaseSource(nameof(k_PointsOnly))]
        [TestCaseSource(nameof(k_PointAndTransforms))]
        [TestCaseSource(nameof(k_ChildTransforms))]
        public void AlignTransformToEndPoints_MovesTransformInTheMiddle(
            From startType, To endType, Quaternion transformRotation,
            Vector3 expectedPosition, Vector3 _)
        {
            ConfigureLinkForTest(startType, endType, transformRotation);

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assert.That(m_Link.transform.position, Is.EqualTo(expectedPosition).Using(k_DefaultThreshold),
                "The Link object should be in the middle between the endpoints.");
        }

        [TestCaseSource(nameof(k_PointsOnly))]
        [TestCaseSource(nameof(k_PointAndTransforms))]
        [Description("When the endpoints remain at the same world position it means that the local points must have been adjusted correctly.")]
        public void AlignTransformToEndPoints_EndpointsWorldPositionsRemainUnchanged(
            From startType, To endType, Quaternion transformRotation,
            Vector3 _, Vector3 __)
        {
            ConfigureLinkForTest(startType, endType, transformRotation);

            var initialEndpointsMatrix = LocalToWorldUnscaled(m_Link);
            var initialStartWorld = initialEndpointsMatrix.MultiplyPoint3x4(m_Link.startPoint);
            var initialEndWorld = initialEndpointsMatrix.MultiplyPoint3x4(m_Link.endPoint);
            var initialStartLocal = m_Link.startPoint;
            var initialEndLocal = m_Link.endPoint;

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assume.That(m_Link.startPoint, Is.Not.EqualTo(initialStartLocal).Using(k_DefaultThreshold),
                "The local position of Start point should have been adjusted.");

            Assert.That(m_Link.endPoint, Is.Not.EqualTo(initialEndLocal).Using(k_DefaultThreshold),
                "The local position of End point should have been adjusted.");

            var endpointsMatrix = LocalToWorldUnscaled(m_Link);
            var startWorld = endpointsMatrix.MultiplyPoint3x4(m_Link.startPoint);
            var endWorld = endpointsMatrix.MultiplyPoint3x4(m_Link.endPoint);

            Assert.That(startWorld, Is.EqualTo(initialStartWorld).Using(k_DefaultThreshold),
                "The world position of Start should remain unchanged.");

            Assert.That(endWorld, Is.EqualTo(initialEndWorld).Using(k_DefaultThreshold),
                "The world position of End should remain unchanged.");
        }

        static Matrix4x4 LocalToWorldUnscaled(NavMeshLink link)
        {
            return Matrix4x4.TRS(link.transform.position, link.transform.rotation, Vector3.one);
        }

        [TestCaseSource(nameof(k_PointAndTransforms))]
        [TestCaseSource(nameof(k_ChildTransforms))]
        public void AlignTransformToEndPoints_EndsTransformsRemainUnchanged(
            From startType, To endType, Quaternion transformRotation,
            Vector3 _, Vector3 __)
        {
            ConfigureLinkForTest(startType, endType, transformRotation);

            var initialStartPosition = m_Link.startTransform != null ? m_Link.startTransform.position : Vector3.negativeInfinity;
            var initialEndPosition = m_Link.endTransform != null ? m_Link.endTransform.position : Vector3.negativeInfinity;

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            if (m_Link.startTransform != null)
                Assert.That(m_Link.startTransform.position, Is.EqualTo(initialStartPosition).Using(k_DefaultThreshold),
                    "The Link start transform should not have moved.");

            if (m_Link.endTransform != null)
                Assert.That(m_Link.endTransform.position, Is.EqualTo(initialEndPosition).Using(k_DefaultThreshold),
                    "The Link end transform should not have moved.");
        }

        [Test]
        [Explicit("Functionality not implemented yet for child game objects")]
        public void AlignTransformToEndPoints_ChildGameObjectsRetainWorldPositions()
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startPoint = k_Offset103;
                m_Link.endPoint = k_Offset101;
                m_Link.startTransform = null;
                m_Link.endTransform = null;

                m_Link.transform.rotation = k_FlipToRight;
            }

            var child1 = CreateTestObject("Child 1").GetComponent<Transform>();
            var child2 = CreateTestObject("Child 2").GetComponent<Transform>();
            var grandchild = CreateTestObject("Grandchild").GetComponent<Transform>();
            child2.rotation = Quaternion.Euler(0, 90, 0);
            child1.parent = m_Link.transform;
            child2.parent = m_Link.transform;
            grandchild.parent = child2.transform;

            child1.position = new Vector3(2, 1, 3);
            child2.position = new Vector3(-0.5f, 0.6f, 0.7f);
            grandchild.position = new Vector3(-3, 2, 1);
            var child1Earlier = child1.position;
            var child2Earlier = child2.position;
            var grandchildEarlier = grandchild.position;

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assert.That(child1.position, Is.EqualTo(child1Earlier).Using(k_DefaultThreshold),
                "Child object 1 should not have moved.");
            Assert.That(child2.position, Is.EqualTo(child2Earlier).Using(k_DefaultThreshold),
                "Child object 2 should not have moved.");
            Assert.That(grandchild.position, Is.EqualTo(grandchildEarlier).Using(k_DefaultThreshold),
                "Grandchild object should not have moved.");
        }

        [Test]
        public void AlignTransformToEndPoints_EndpointsWorldPositionsRemainUnchanged_InSiblingLinks()
        {
            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startPoint = k_Offset103;
                m_Link.endPoint = k_Offset101;
                m_Link.startTransform = null;
                m_Link.endTransform = null;

                m_Link.transform.rotation = k_FlipToRight;
            }

            m_LinkSibling1 = m_Link.gameObject.AddComponent<NavMeshLink>();
            m_LinkSibling2 = m_Link.gameObject.AddComponent<NavMeshLink>();
            m_LinkSibling1.startPoint = m_Link.endPoint;
            m_LinkSibling1.endPoint = m_Link.startPoint;
            m_LinkSibling2.startPoint = m_Link.endPoint;
            m_LinkSibling2.endPoint = m_Link.startPoint;

            var initialStartLocal = m_Link.startPoint;

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assume.That(m_Link.startPoint, Is.Not.EqualTo(initialStartLocal).Using(k_DefaultThreshold));

            Assert.That(m_LinkSibling1.startPoint, Is.EqualTo(m_Link.endPoint).Using(k_DefaultThreshold),
                "The sibling 1 Start point should have been adjusted.");
            Assert.That(m_LinkSibling1.endPoint, Is.EqualTo(m_Link.startPoint).Using(k_DefaultThreshold),
                "The sibling 1 End point should have been adjusted.");

            Assert.That(m_LinkSibling2.startPoint, Is.EqualTo(m_Link.endPoint).Using(k_DefaultThreshold),
                "The sibling 2 Start point should have been adjusted.");
            Assert.That(m_LinkSibling2.endPoint, Is.EqualTo(m_Link.startPoint).Using(k_DefaultThreshold),
                "The sibling 2 End point should have been adjusted.");
        }

        [TestCaseSource(nameof(k_PointsOnly))]
        [TestCaseSource(nameof(k_PointAndTransforms))]
        [TestCaseSource(nameof(k_ChildTransforms))]
        public void AlignTransformToEndPoints_UpVectorRemainsUnchanged(
            From startType, To endType, Quaternion transformRotation,
            Vector3 _, Vector3 __)
        {
            ConfigureLinkForTest(startType, endType, transformRotation);

            var initialUp = m_Link.transform.up;

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assert.That(m_Link.transform.up, Is.EqualTo(initialUp).Using(k_DefaultThreshold),
                "The Link's up vector should remain unchanged.");
        }

        [TestCaseSource(nameof(k_PointsOnly))]
        [TestCaseSource(nameof(k_PointAndTransforms))]
        [TestCaseSource(nameof(k_ChildTransforms))]
        public void AlignTransformToEndPoints_OrientsForwardVectorFromStartToEndInXZPlane(
            From startType, To endType, Quaternion transformRotation,
            Vector3 _, Vector3 expectedForward)
        {
            ConfigureLinkForTest(startType, endType, transformRotation);

            NavMeshLinkEditor.AlignTransformToEndPoints(m_Link);

            Assert.That(m_Link.transform.forward, Is.EqualTo(expectedForward).Using(k_DefaultThreshold),
                "The Link's forward vector should point from the start towards the end of the link.");
        }

        static readonly Vector3 k_ForwardRightDiagonal = Quaternion.Euler(0, 45, 0) * Vector3.forward;
        static readonly Quaternion k_RotatedX90 = Quaternion.Euler(90, 0, 0);
        static readonly Quaternion k_RotatedY180 = Quaternion.Euler(0, 180, 0);
        static readonly Quaternion k_RotatedZ90 = Quaternion.Euler(0, 0, 90);
        static readonly Quaternion k_RotatedZ180 = Quaternion.Euler(0, 0, 180);
        protected static readonly TestCaseData[] k_RotateToChangeLinkDirectionLocally =
        {
            new TestCaseData(Quaternion.identity, Vector3.right, Vector3.one)
                .SetName("Link aligned to World axes"),

            new TestCaseData(k_RotatedX90, Vector3.zero, new Vector3(1, 1, -1))
                .SetName("Endpoints following the Up direction")
                .SetDescription("The right vector cannot be properly defined in this case"),

            new TestCaseData(k_RotatedY180, Vector3.forward, new Vector3(-1, 1, -1))
                .SetName("Endpoints following Right direction"),

            new TestCaseData(k_RotatedZ90, Vector3.right, new Vector3(1, -1, 1))
                .SetName("Link tipped to the left"),

            new TestCaseData(k_RotatedZ180, k_ForwardRightDiagonal, new Vector3(-1, -1, 1))
                .SetName("Link rotated freely")
                .SetDescription("The rotation has been chosen to produce a Right vector easy to identify and verify."),
        };

        [TestCaseSource(nameof(k_RotateToChangeLinkDirectionLocally))]
        [Description("The link's direction changes because the end transform moves in local space when the game object rotates.")]
        public void CalcLinkRight_ReturnsLocalRightIn2D(
            Quaternion transformRotation,
            Vector3 expectedLocalRight, Vector3 expectedEnd)
        {
            m_Link.transform.SetPositionAndRotation(Vector3.one, transformRotation);
            m_Link.transform.localScale = 2f * Vector3.one;
            m_End.transform.position = 2f * Vector3.one;

            using (new NavMeshLinkEditor.DeferredLinkUpdateScope(m_Link))
            {
                m_Link.startPoint = new Vector3(1, -1, -1);
                m_Link.endTransform = m_End.transform;

                m_Link.startTransform = null;
                m_Link.endPoint = Vector3.negativeInfinity;
            }

            var linkRight = NavMeshLinkEditor.GetLocalDirectionRight(m_Link, out var localStart, out var localEnd);

            Assume.That(localStart, Is.EqualTo(m_Link.startPoint).Using(k_DefaultThreshold), "Wrong local Start reported.");
            Assume.That(localEnd, Is.EqualTo(expectedEnd).Using(k_DefaultThreshold), "Wrong local End reported.");

            Assert.That(linkRight, Is.EqualTo(expectedLocalRight).Using(k_DefaultThreshold),
                "Wrong Right vector relative to the direction from start to end.");
        }

        public enum LinkEndType
        {
            Point,
            Transform,
            TransformChild
        }
    }
}
