using System.IO;
using System.Net;
using System.Net.Sockets;
using NUnit.Framework;
using MCPForUnity.Editor.Helpers;

namespace MCPForUnityTests.Editor.Services
{
    [TestFixture]
    public class PortManagerTests
    {
        private string _savedPortFileContent;
        private string _savedLegacyFileContent;
        private string _portFilePath;
        private string _legacyFilePath;

        [SetUp]
        public void SetUp()
        {
            // Snapshot the on-disk port config so DiscoverNewPort tests don't
            // permanently alter the running bridge's persisted port.
            string dir = Path.Combine(
                System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
                ".unity-mcp");
            _legacyFilePath = Path.Combine(dir, "unity-mcp-port.json");

            // The hashed file uses a private helper; approximate the same hash.
            // We snapshot every json file in the directory to be safe.
            _portFilePath = null;
            _savedPortFileContent = null;
            _savedLegacyFileContent = null;

            if (File.Exists(_legacyFilePath))
                _savedLegacyFileContent = File.ReadAllText(_legacyFilePath);

            // Find the hashed port file for this project
            if (Directory.Exists(dir))
            {
                foreach (var f in Directory.GetFiles(dir, "unity-mcp-port-*.json"))
                {
                    _portFilePath = f;
                    _savedPortFileContent = File.ReadAllText(f);
                    break; // one project at a time
                }
            }
        }

        [TearDown]
        public void TearDown()
        {
            // Restore the original port files
            if (_savedLegacyFileContent != null && _legacyFilePath != null)
                File.WriteAllText(_legacyFilePath, _savedLegacyFileContent);

            if (_savedPortFileContent != null && _portFilePath != null)
                File.WriteAllText(_portFilePath, _savedPortFileContent);
        }

        [Test]
        public void IsPortAvailable_ReturnsFalse_WhenPortIsOccupied()
        {
            var listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();
            int port = ((IPEndPoint)listener.LocalEndpoint).Port;

            try
            {
                Assert.IsFalse(PortManager.IsPortAvailable(port),
                    "IsPortAvailable should return false for a port that is already bound");
            }
            finally
            {
                listener.Stop();
            }
        }

        [Test]
        public void IsPortAvailable_ReturnsTrue_WhenPortIsFree()
        {
            var listener = new TcpListener(IPAddress.Loopback, 0);
            listener.Start();
            int port = ((IPEndPoint)listener.LocalEndpoint).Port;
            listener.Stop();

            Assert.IsTrue(PortManager.IsPortAvailable(port),
                "IsPortAvailable should return true for a port that is not bound");
        }

#if UNITY_EDITOR_OSX
        [Test]
        public void IsPortAvailable_ReturnsFalse_WhenPortHeldWithReuseAddr()
        {
            // Simulate what AssetImportWorkers do: bind with SO_REUSEADDR.
            // IsPortAvailable must still detect this as occupied.
            var holder = new TcpListener(IPAddress.Loopback, 0);
            holder.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            holder.Start();
            int port = ((IPEndPoint)holder.LocalEndpoint).Port;

            try
            {
                Assert.IsFalse(PortManager.IsPortAvailable(port),
                    "IsPortAvailable should detect ports held with SO_REUSEADDR on macOS");
            }
            finally
            {
                holder.Stop();
            }
        }
#endif

        [Test]
        public void ShouldAbandonBusyPort_KeepsSamePort_WithinReleaseWindow()
        {
            // A port busy for less than the fallback window is treated as our own
            // not-yet-released listener after a domain reload — keep retrying the same
            // port instead of silently switching and stranding the client (#1173).
            Assert.IsFalse(PortManager.ShouldAbandonBusyPort(0.0));
            Assert.IsFalse(PortManager.ShouldAbandonBusyPort(
                PortManager.BusyPortFallbackWindowSeconds - 0.5));
        }

        [Test]
        public void ShouldAbandonBusyPort_FallsBack_AfterReleaseWindow()
        {
            // A port that stays busy past the window is a foreign occupant — only then
            // does the bridge discover and switch to a new port.
            Assert.IsTrue(PortManager.ShouldAbandonBusyPort(
                PortManager.BusyPortFallbackWindowSeconds));
            Assert.IsTrue(PortManager.ShouldAbandonBusyPort(
                PortManager.BusyPortFallbackWindowSeconds + 5.0));
        }

        [Test]
        public void DiscoverNewPort_ReturnsAvailablePort()
        {
            int port = PortManager.DiscoverNewPort();
            Assert.Greater(port, 0, "DiscoverNewPort should return a positive port number");
            Assert.IsTrue(PortManager.IsPortAvailable(port),
                "The port returned by DiscoverNewPort should be available");
        }

        [Test]
        public void DiscoverNewPort_SkipsOccupiedDefaultPort()
        {
            // Hold the default port (6400) so DiscoverNewPort must find an alternative
            TcpListener holder = null;
            try
            {
                holder = new TcpListener(IPAddress.Loopback, 6400);
#if UNITY_EDITOR_OSX
                try { holder.Server.ExclusiveAddressUse = true; } catch { }
#endif
                holder.Start();
            }
            catch (SocketException)
            {
                // Port 6400 already occupied (e.g., by the running bridge) — that's fine,
                // the test still validates that DiscoverNewPort picks a different port.
                holder = null;
            }

            try
            {
                int port = PortManager.DiscoverNewPort();
                Assert.AreNotEqual(6400, port,
                    "DiscoverNewPort should not return the default port when it is occupied");
                Assert.Greater(port, 0);
            }
            finally
            {
                holder?.Stop();
            }
        }
    }
}
