using System.Reflection;
using NUnit.Framework;
using Streamingle.Gaze.Editor;
using UnityEngine;

namespace Streamingle.Gaze.Tests
{
    public sealed class GazeCalibrationWindowTests
    {
        [Test]
        public void AutoCreateOrigin_ParentsFaceGazeOriginUnderRendererHeadBone()
        {
            var root = new GameObject("GazeOriginParentTest");
            var head = new GameObject("Bip001 Head");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();

            try
            {
                head.transform.SetParent(root.transform, false);
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.bones = new[] { head.transform };
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;
                Invoke(window, "SetDriver", driver);
                Invoke(window, "AutoCreateOrRecalculateOrigin");

                Assert.That(driver.GazeOrigin, Is.Not.Null);
                Assert.That(driver.GazeOrigin.name, Is.EqualTo("Face_GazeOrigin"));
                Assert.That(driver.GazeOrigin.parent, Is.EqualTo(head.transform));
            }
            finally
            {
                if (window != null)
                    Object.DestroyImmediate(window);
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void AutoCreateOrigin_ReusesExistingHeadChildWhenDriverReferenceIsMissing()
        {
            var root = new GameObject("GazeOriginReuseTest");
            var head = new GameObject("Bip001 Head");
            var existingOrigin = new GameObject("Face_GazeOrigin");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();

            try
            {
                head.transform.SetParent(root.transform, false);
                existingOrigin.transform.SetParent(head.transform, false);
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.bones = new[] { head.transform };
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;
                Invoke(window, "SetDriver", driver);
                Invoke(window, "AutoCreateOrRecalculateOrigin");

                Assert.That(driver.GazeOrigin, Is.SameAs(existingOrigin.transform));
                Assert.That(head.transform.childCount, Is.EqualTo(1));
            }
            finally
            {
                if (window != null)
                    Object.DestroyImmediate(window);
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void SelectingMissingPointsDoesNotCaptureAndCancelRevertsProfile()
        {
            var root = new GameObject("GazeCalibrationWindowTest");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();

            try
            {
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.SetBlendShapeWeight(0, 12f);

                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                var origin = new GameObject("GazeOrigin").transform;
                origin.SetParent(root.transform, false);
                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;
                driver.GazeOrigin = origin;

                Invoke(window, "SetDriver", driver);
                Invoke(window, "StartCalibration");
                Invoke(window, "SelectPoint", GazeCalibrationPoint.CenterLeft);

                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);
                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);

                // Guided calibration edits an unsaved working copy. Cancel must
                // discard it without ever mutating the original profile asset.
                var session = GetPrivateField<GazeCalibrationPreviewSession>(window, "previewSession");
                Assert.That(session, Is.Not.Null);
                Assert.That(session.Profile, Is.Not.SameAs(profile));
                UnityEditor.Undo.RecordObject(session.Profile, "Test Gaze Calibration Edit");
                session.Profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 77f });
                renderer.SetBlendShapeWeight(0, 77f);
                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);

                // A mapping/play-mode/reload guard may stop the preview before the
                // window repaints. The stop callback must cancel immediately.
                session.Stop();
                Invoke(window, "FinishCalibration", false, false);

                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(12f).Within(0.001f));
            }
            finally
            {
                if (window != null)
                {
                    Invoke(window, "FinishCalibration", false, false);
                    Object.DestroyImmediate(window);
                }
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void ApplyCalibration_CommitsWorkingCopyAndRestoresRendererPose()
        {
            var root = new GameObject("GazeCalibrationApplyTest");
            var mesh = CreateMesh();
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();

            try
            {
                var renderer = root.AddComponent<SkinnedMeshRenderer>();
                renderer.sharedMesh = mesh;
                renderer.SetBlendShapeWeight(0, 14f);
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                var origin = new GameObject("GazeOrigin").transform;
                origin.SetParent(root.transform, false);
                var driver = root.AddComponent<BlendshapeGazeDriver>();
                driver.TargetRenderer = renderer;
                driver.Profile = profile;
                driver.GazeOrigin = origin;

                Invoke(window, "SetDriver", driver);
                Invoke(window, "StartCalibration");
                var session = GetPrivateField<GazeCalibrationPreviewSession>(window, "previewSession");
                Assert.That(session, Is.Not.Null);
                Assert.That(session.Profile, Is.Not.SameAs(profile));

                UnityEditor.Undo.RecordObject(session.Profile, "Test Apply Gaze Calibration");
                session.Profile.SetSample(GazeCalibrationPoint.Center, new[] { 66f });
                session.PreviewSample(GazeCalibrationPoint.Center);
                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);

                Invoke(window, "FinishCalibration", true, false);

                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.True);
                Assert.That(
                    profile.GetSampleWeight(GazeCalibrationPoint.Center, 0),
                    Is.EqualTo(66f).Within(0.001f));
                Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(14f).Within(0.001f));
            }
            finally
            {
                if (window != null)
                {
                    Invoke(window, "FinishCalibration", false, false);
                    Object.DestroyImmediate(window);
                }
                Object.DestroyImmediate(root);
                Object.DestroyImmediate(mesh);
                Object.DestroyImmediate(profile);
            }
        }

        private static void Invoke(object target, string methodName, params object[] arguments)
        {
            var method = target.GetType().GetMethod(
                methodName,
                BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.That(method, Is.Not.Null, $"Expected private method '{methodName}'.");
            method.Invoke(target, arguments);
        }

        private static T GetPrivateField<T>(object target, string fieldName)
            where T : class
        {
            var field = target.GetType().GetField(
                fieldName,
                BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.That(field, Is.Not.Null, $"Expected private field '{fieldName}'.");
            return field.GetValue(target) as T;
        }

        private static Mesh CreateMesh()
        {
            var mesh = new Mesh { name = "GazeCalibrationWindowMesh" };
            mesh.vertices = new[]
            {
                new Vector3(-0.1f, 0f, 0f),
                new Vector3(0.1f, 0f, 0f),
                new Vector3(0f, 0.1f, 0f)
            };
            mesh.triangles = new[] { 0, 1, 2 };
            mesh.AddBlendShapeFrame(
                "eyeLookUpLeft",
                100f,
                new[] { Vector3.up * 0.01f, Vector3.up * 0.01f, Vector3.up * 0.01f },
                new Vector3[3],
                new Vector3[3]);
            return mesh;
        }
    }
}
