using System.Reflection;
using NUnit.Framework;
using UnityEngine;

namespace Streamingle.Gaze.Tests
{
    public sealed class GazeProfileAuthoringTests
    {
        private static readonly string[] StandardChannels =
        {
            "eyeLookUpLeft",
            "eyeLookUpRight",
            "eyeLookDownLeft",
            "eyeLookDownRight",
            "eyeLookInLeft",
            "eyeLookInRight",
            "eyeLookOutLeft",
            "eyeLookOutRight"
        };

        [Test]
        public void SeedStandardEyeLook_CreatesFivePointsAndFourCorners()
        {
            var profile = CreateStandardProfile();
            try
            {
                Assert.That(profile.SeedStandardEyeLook(80f), Is.True);
                for (var index = 0; index < 9; index++)
                    Assert.That(profile.IsSampleCaptured((GazeCalibrationPoint)index), Is.True);

                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.Center),
                    Is.EqualTo(new[] { 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f }));
                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.CenterLeft),
                    Is.EqualTo(new[] { 0f, 0f, 0f, 0f, 0f, 80f, 80f, 0f }));
                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.UpRight),
                    Is.EqualTo(new[] { 80f, 80f, 0f, 0f, 80f, 0f, 0f, 80f }));
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void GenerateCorners_CombinesCardinalDeltasAndClampsByUsage()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft"),
                    new GazeBlendShapeChannel("eyeLidCorrective", GazeChannelUsage.Corrective)
                });
                profile.SetSample(GazeCalibrationPoint.Center, new[] { 80f, 0f });
                profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 100f, 100f });
                profile.SetSample(GazeCalibrationPoint.CenterRight, new[] { 0f, -100f });
                profile.SetSample(GazeCalibrationPoint.UpCenter, new[] { 100f, 100f });
                profile.SetSample(GazeCalibrationPoint.DownCenter, new[] { 0f, -100f });

                Assert.That(profile.CanGenerateCorners, Is.True);
                Assert.That(profile.GenerateCornersFromCardinals(), Is.True);
                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.UpLeft),
                    Is.EqualTo(new[] { 100f, 100f }));
                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.DownRight),
                    Is.EqualTo(new[] { 0f, -100f }));
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void GenerateCorners_UsesCardinalDeltasRelativeToCenter()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookTest")
                });
                profile.SetSample(GazeCalibrationPoint.Center, new[] { 10f });
                profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 30f });
                profile.SetSample(GazeCalibrationPoint.CenterRight, new[] { 10f });
                profile.SetSample(GazeCalibrationPoint.UpCenter, new[] { 40f });
                profile.SetSample(GazeCalibrationPoint.DownCenter, new[] { 10f });

                Assert.That(profile.GenerateCornersFromCardinals(), Is.True);
                Assert.That(
                    profile.GetSampleWeight(GazeCalibrationPoint.UpLeft, 0),
                    Is.EqualTo(60f));
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void DirectionLimits_SanitizeNonFiniteValuesAndClampToSupportedRanges()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.SetDirectionLimits(
                    float.NaN,
                    float.PositiveInfinity,
                    float.NegativeInfinity,
                    float.NaN);

                Assert.That(profile.MinYaw, Is.EqualTo(-35f));
                Assert.That(profile.MaxYaw, Is.EqualTo(35f));
                Assert.That(profile.MinPitch, Is.EqualTo(-25f));
                Assert.That(profile.MaxPitch, Is.EqualTo(20f));

                profile.SetDirectionLimits(-180f, -5f, 10f, 180f);

                Assert.That(profile.MinYaw, Is.EqualTo(-90f));
                Assert.That(profile.MaxYaw, Is.EqualTo(0.1f));
                Assert.That(profile.MinPitch, Is.EqualTo(-0.1f));
                Assert.That(profile.MaxPitch, Is.EqualTo(90f));

                SetPrivateField(profile, "minYaw", float.PositiveInfinity);
                SetPrivateField(profile, "maxYaw", float.NegativeInfinity);
                SetPrivateField(profile, "minPitch", float.NaN);
                SetPrivateField(profile, "maxPitch", float.NaN);
                InvokeOnValidate(profile);

                Assert.That(profile.MinYaw, Is.EqualTo(-35f));
                Assert.That(profile.MaxYaw, Is.EqualTo(35f));
                Assert.That(profile.MinPitch, Is.EqualTo(-25f));
                Assert.That(profile.MaxPitch, Is.EqualTo(20f));
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void CopySample_WithMirrorSwapsLeftAndRightChannels()
        {
            var profile = CreateStandardProfile();
            try
            {
                profile.SetSample(
                    GazeCalibrationPoint.CenterLeft,
                    new[] { 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f });

                Assert.That(
                    profile.CopySample(
                        GazeCalibrationPoint.CenterLeft,
                        GazeCalibrationPoint.CenterRight,
                        true),
                    Is.True);
                Assert.That(
                    profile.GetSampleWeightsCopy(GazeCalibrationPoint.CenterRight),
                    Is.EqualTo(new[] { 2f, 1f, 4f, 3f, 6f, 5f, 8f, 7f }));
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        [Test]
        public void SetSample_RejectsNonFiniteWeights()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            try
            {
                profile.ReplaceChannels(new[]
                {
                    new GazeBlendShapeChannel("eyeLookUpLeft")
                });

                Assert.Throws<System.ArgumentException>(() =>
                    profile.SetSample(GazeCalibrationPoint.Center, new[] { float.NaN }));
                Assert.Throws<System.ArgumentException>(() =>
                    profile.SetSample(GazeCalibrationPoint.Center, new[] { float.PositiveInfinity }));
                Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);
            }
            finally
            {
                Object.DestroyImmediate(profile);
            }
        }

        private static BlendshapeGazeProfile CreateStandardProfile()
        {
            var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
            var channels = new GazeBlendShapeChannel[StandardChannels.Length];
            for (var index = 0; index < channels.Length; index++)
                channels[index] = new GazeBlendShapeChannel(StandardChannels[index]);
            profile.ReplaceChannels(channels);
            return profile;
        }

        private static void SetPrivateField(
            BlendshapeGazeProfile profile,
            string fieldName,
            float value)
        {
            var field = typeof(BlendshapeGazeProfile).GetField(
                fieldName,
                BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.That(field, Is.Not.Null, $"Expected private field '{fieldName}'.");
            field.SetValue(profile, value);
        }

        private static void InvokeOnValidate(BlendshapeGazeProfile profile)
        {
            var method = typeof(BlendshapeGazeProfile).GetMethod(
                "OnValidate",
                BindingFlags.Instance | BindingFlags.NonPublic);
            Assert.That(method, Is.Not.Null, "Expected private OnValidate method.");
            method.Invoke(profile, null);
        }
    }
}
