using System.Collections.Generic;
using NUnit.Framework;
using OkonaPad.Input;
using UnityEngine;
using UnityEngine.InputSystem;

namespace OkonaPad.Tests
{
    [TestFixture]
    public class OkonaPadControllerDeviceTests : InputTestFixture
    {
        private List<OkonaPadControllerDevice> _createdDevices;

        public override void Setup()
        {
            base.Setup();
            _createdDevices = new List<OkonaPadControllerDevice>();
            // Reset and re-register layout after InputTestFixture resets the Input System
            OkonaPadControllerDevice.ResetLayoutRegistration();
            OkonaPadControllerDevice.RegisterLayout();
        }

        public override void TearDown()
        {
            // Clean up any devices created during tests
            foreach (var device in _createdDevices)
            {
                if (device != null && device.added)
                {
                    try
                    {
                        InputSystem.RemoveDevice(device);
                    }
                    catch (System.Exception)
                    {
                        // Ignore cleanup errors
                    }
                }
            }
            _createdDevices.Clear();
            base.TearDown();
        }

        private OkonaPadControllerDevice CreateTestDevice(int controllerId = 1)
        {
            // Use InputSystem.AddDevice<T> directly for testing (more reliable in test environment)
            var device = InputSystem.AddDevice<OkonaPadControllerDevice>();
            if (device != null)
            {
                device.ControllerId = controllerId;
                _createdDevices.Add(device);
            }
            return device;
        }

        #region Layout Registration Tests

        [Test]
        public void RegisterLayout_LayoutIsAvailable()
        {
            OkonaPadControllerDevice.RegisterLayout();

            var layout = InputSystem.LoadLayout("OkonaPad");
            Assert.IsNotNull(layout, "OkonaPad layout should be registered");
        }

        [Test]
        public void RegisterLayout_CalledMultipleTimes_DoesNotThrow()
        {
            // Should be idempotent
            Assert.DoesNotThrow(() =>
            {
                OkonaPadControllerDevice.RegisterLayout();
                OkonaPadControllerDevice.RegisterLayout();
                OkonaPadControllerDevice.RegisterLayout();
            });
        }

        #endregion

        #region Device Creation Tests

        [Test]
        public void Create_ReturnsValidDevice()
        {
            var device = CreateTestDevice(1);

            Assert.IsNotNull(device, "Device should be created");
            Assert.AreNotEqual(InputDevice.InvalidDeviceId, device.deviceId, "Device should have valid ID");
        }

        [Test]
        public void Create_SetsControllerId()
        {
            var device = CreateTestDevice(3);

            Assert.IsNotNull(device);
            Assert.AreEqual(3, device.ControllerId);
        }

        [Test]
        public void Create_MultipleDevices_HaveUniqueIds()
        {
            var device1 = CreateTestDevice(1);
            var device2 = CreateTestDevice(2);

            Assert.IsNotNull(device1);
            Assert.IsNotNull(device2);
            Assert.AreNotEqual(device1.deviceId, device2.deviceId, "Devices should have unique IDs");
            Assert.AreEqual(1, device1.ControllerId);
            Assert.AreEqual(2, device2.ControllerId);
        }

        [Test]
        public void Create_DeviceAppearsInInputSystem()
        {
            var device = CreateTestDevice(1);

            Assert.IsNotNull(device);
            Assert.Contains(device, new List<InputDevice>(InputSystem.devices));
        }

        [Test]
        public void Create_DeviceIsNotGamepad()
        {
            var device = CreateTestDevice(1);

            Assert.IsNotNull(device);
            Assert.IsInstanceOf<InputDevice>(device);
            Assert.IsNotInstanceOf<Gamepad>(device);
        }

        [Test]
        public void Create_DeviceDoesNotAppearInGamepadAll()
        {
            var device = CreateTestDevice(1);

            Assert.IsNotNull(device);
            bool foundInGamepadAll = false;
            foreach (var gamepad in Gamepad.all)
            {
                if (gamepad.deviceId == device.deviceId)
                {
                    foundInGamepadAll = true;
                    break;
                }
            }
            Assert.IsFalse(foundInGamepadAll, "Device should NOT appear in Gamepad.all");
        }

        #endregion

        #region Device State Tests

        [Test]
        public void UpdateState_ButtonA_UpdatesCorrectly()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.ButtonA, true);

            InputSystem.QueueStateEvent(device, state);
            InputSystem.Update();

            Assert.IsTrue(device.ButtonA.isPressed, "Button A should be pressed");
        }

        [Test]
        public void UpdateState_LeftStick_UpdatesCorrectly()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            // Use values at full range to avoid deadzone processing effects
            var state = new OkonaPadControllerState
            {
                leftStick = new Vector2(1f, -1f)
            };

            InputSystem.QueueStateEvent(device, state);
            InputSystem.Update();

            Assert.AreEqual(1f, device.LeftStick.x.ReadValue(), 0.01f);
            Assert.AreEqual(-1f, device.LeftStick.y.ReadValue(), 0.01f);
        }

        [Test]
        public void UpdateState_DpadButtons_UpdateCorrectly()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.DpadUp, true);
            state = state.WithButton(OkonaPadControllerState.DpadRight, true);

            InputSystem.QueueStateEvent(device, state);
            InputSystem.Update();

            Assert.IsTrue(device.DPad.up.isPressed, "D-pad up should be pressed");
            Assert.IsTrue(device.DPad.right.isPressed, "D-pad right should be pressed");
            Assert.IsFalse(device.DPad.down.isPressed, "D-pad down should not be pressed");
            Assert.IsFalse(device.DPad.left.isPressed, "D-pad left should not be pressed");
        }

        [Test]
        public void UpdateState_StartSelect_UpdateCorrectly()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.ButtonStart, true);
            state = state.WithButton(OkonaPadControllerState.ButtonSelect, true);

            InputSystem.QueueStateEvent(device, state);
            InputSystem.Update();

            Assert.IsTrue(device.StartButton.isPressed, "Start should be pressed");
            Assert.IsTrue(device.SelectButton.isPressed, "Select should be pressed");
        }

        [Test]
        public void UpdateState_AllFaceButtons_UpdateCorrectly()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.ButtonA, true);
            state = state.WithButton(OkonaPadControllerState.ButtonB, true);
            state = state.WithButton(OkonaPadControllerState.ButtonX, true);
            state = state.WithButton(OkonaPadControllerState.ButtonY, true);

            InputSystem.QueueStateEvent(device, state);
            InputSystem.Update();

            Assert.IsTrue(device.ButtonA.isPressed, "A should be pressed");
            Assert.IsTrue(device.ButtonB.isPressed, "B should be pressed");
            Assert.IsTrue(device.ButtonX.isPressed, "X should be pressed");
            Assert.IsTrue(device.ButtonY.isPressed, "Y should be pressed");
        }

        #endregion

        #region State Struct Tests

        [Test]
        public void OkonaPadControllerState_WithButton_SetsCorrectBit()
        {
            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.ButtonA, true);

            Assert.IsTrue(state.IsButtonPressed(OkonaPadControllerState.ButtonA));
            Assert.IsFalse(state.IsButtonPressed(OkonaPadControllerState.ButtonB));
        }

        [Test]
        public void OkonaPadControllerState_WithButton_ClearsCorrectBit()
        {
            var state = new OkonaPadControllerState();
            state = state.WithButton(OkonaPadControllerState.ButtonA, true);
            state = state.WithButton(OkonaPadControllerState.ButtonA, false);

            Assert.IsFalse(state.IsButtonPressed(OkonaPadControllerState.ButtonA));
        }

        [Test]
        public void OkonaPadControllerState_WithButton_OutOfRange_Throws()
        {
            var state = new OkonaPadControllerState();
            Assert.Throws<System.ArgumentOutOfRangeException>(() => state.WithButton(-1, true));
            Assert.Throws<System.ArgumentOutOfRangeException>(() => state.WithButton(16, true));
        }

        #endregion

        #region Haptics Tests

        [Test]
        public void SetMotorSpeeds_FiresRumbleEvent()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            int eventControllerId = -1;
            float eventLowFreq = 0;
            float eventHighFreq = 0;

            void Handler(int id, float low, float high)
            {
                eventControllerId = id;
                eventLowFreq = low;
                eventHighFreq = high;
            }

            try
            {
                OkonaPadControllerDevice.OnRumbleRequested += Handler;

                device.SetMotorSpeeds(0.5f, 0.75f);

                Assert.AreEqual(device.ControllerId, eventControllerId);
                Assert.AreEqual(0.5f, eventLowFreq, 0.001f);
                Assert.AreEqual(0.75f, eventHighFreq, 0.001f);
            }
            finally
            {
                OkonaPadControllerDevice.OnRumbleRequested -= Handler;
            }
        }

        [Test]
        public void PauseHaptics_SetsSpeedsToZero()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            float lastLowFreq = -1;
            float lastHighFreq = -1;

            void Handler(int id, float low, float high)
            {
                lastLowFreq = low;
                lastHighFreq = high;
            }

            try
            {
                OkonaPadControllerDevice.OnRumbleRequested += Handler;

                device.SetMotorSpeeds(0.5f, 0.75f);
                device.PauseHaptics();

                Assert.AreEqual(0f, lastLowFreq, 0.001f);
                Assert.AreEqual(0f, lastHighFreq, 0.001f);
            }
            finally
            {
                OkonaPadControllerDevice.OnRumbleRequested -= Handler;
            }
        }

        [Test]
        public void ResetHaptics_ClearsStoredSpeeds()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            float lastLowFreq = -1;
            float lastHighFreq = -1;

            void Handler(int id, float low, float high)
            {
                lastLowFreq = low;
                lastHighFreq = high;
            }

            try
            {
                OkonaPadControllerDevice.OnRumbleRequested += Handler;

                device.SetMotorSpeeds(0.5f, 0.75f);
                device.ResetHaptics();

                Assert.AreEqual(0f, lastLowFreq, 0.001f);
                Assert.AreEqual(0f, lastHighFreq, 0.001f);

                // ResumeHaptics after reset should send zeros
                device.ResumeHaptics();
                Assert.AreEqual(0f, lastLowFreq, 0.001f);
                Assert.AreEqual(0f, lastHighFreq, 0.001f);
            }
            finally
            {
                OkonaPadControllerDevice.OnRumbleRequested -= Handler;
            }
        }

        #endregion

        #region Device Removal Tests

        [Test]
        public void RemoveDevice_DeviceNoLongerInInputSystem()
        {
            var device = CreateTestDevice(1);
            Assert.IsNotNull(device);

            // Remove from our tracking list since we're manually removing
            _createdDevices.Remove(device);

            InputSystem.RemoveDevice(device);

            Assert.IsFalse(new List<InputDevice>(InputSystem.devices).Contains(device));
        }

        #endregion
    }
}
