using System;
using NUnit.Framework;
using Moq;
using NcProgramManager.Cnc.Fanuc;
using NcProgramManager.Cnc.Models;

namespace NcProgramManager.Tests.Unit
{
    /// <summary>
    /// FanucMachine calls real FOCAS DLL in ConnectCore and most methods guard
    /// via EnsureConnected. Tests that require an active FOCAS handle are marked Ignore.
    /// The tests here verify dialect call patterns by setting machine state via
    /// reflection (bypass EnsureConnected) after injecting a mock dialect.
    /// </summary>
    [TestFixture]
    public class FanucMachineTests
    {
        // Used in Returns() to modify the ref int len parameter — lambdas cannot have ref params.
        private delegate short UploadDataDelegate(ushort hndl, ref int len, char[] data);

        private FanucConnectionConfig EthernetConfig()
        {
            return new FanucConnectionConfig
            {
                Name = "TestFanuc",
                Transport = FanucTransport.Ethernet,
                IpAddress = "127.0.0.1",
                Port = 8193,
                ConnectTimeout = TimeSpan.FromSeconds(1)
            };
        }

        private static T GetRequiredField<T>(object target, string name)
        {
            var f = target.GetType().GetField(name,
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
                ?? throw new InvalidOperationException(
                    string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
            return (T)f.GetValue(target);
        }

        private static void SetRequiredField<T>(object target, string name, T value)
        {
            var f = target.GetType().GetField(name,
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
                ?? throw new InvalidOperationException(
                    string.Format("Field '{0}' not found on {1}. Field may have been renamed.", name, target.GetType().Name));
            f.SetValue(target, value);
        }

        private FanucMachine CreateConnectedWithMockDialect(Mock<IFocasDialect> dialectMock)
        {
            var machine = new FanucMachine(EthernetConfig());
            SetRequiredField(machine, "_state", ConnectionState.Connected);
            SetRequiredField(machine, "_dialect", dialectMock.Object);
            return machine;
        }

        [Test]
        [Ignore("Requires Fanuc FOCAS DLL — no mock injection path for ConnectCore")]
        public void ConnectAsync_FocasReturnsOk_ResultSuccess()
        {
            // Would need real FOCAS DLL or a way to intercept Focas1.cnc_allclibhndl3
        }

        [Test]
        [Ignore("Requires Fanuc FOCAS DLL")]
        public void ConnectAsync_FocasReturnsError_ResultFail()
        {
            // Same — Focas1 static calls cannot be intercepted without DLL
        }

        [Test]
        // Moq default: ref int len stays 0 after UploadData call, loop exits via else-branch (len == 0).
        // This test verifies call sequence only, not data content.
        // See ReadProgramAsync_UploadDataReturnsContent_ContentInResult for data-reading coverage.
        public void ReadProgramAsync_CallsUploadStartThenDataThenEnd()
        {
            var dialect = new Mock<IFocasDialect>();
            dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();
            }

            dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), 0), Times.Once);
            dialect.Verify(d => d.UploadEnd(It.IsAny<ushort>()), Times.Once);
        }

        [Test]
        public void ReadProgramAsync_UploadDataReturnsContent_ContentInResult()
        {
            var programContent = "O1234\nG0 X0\nM30\n%";
            var chars = programContent.ToCharArray();

            var dialect = new Mock<IFocasDialect>();
            dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
                   .Returns(new UploadDataDelegate(delegate(ushort h, ref int len, char[] buf)
                   {
                       Array.Copy(chars, buf, chars.Length);
                       len = chars.Length;
                       return Focas1.EW_OK;
                   }));
            dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();

                Assert.That(result.Success, Is.True);
                Assert.That(result.Value, Is.EqualTo(programContent));
            }
        }

        [Test]
        public void ReadToolDataAsync_CallsUploadStartWithFileType1()
        {
            var dialect = new Mock<IFocasDialect>();
            dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                machine.ReadToolDataAsync("O1234").GetAwaiter().GetResult();
            }

            dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)1), Times.Once);
        }

        [Test]
        public void ReadWorkZeroDataAsync_CallsUploadStartWithFileType5()
        {
            var dialect = new Mock<IFocasDialect>();
            dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadData(It.IsAny<ushort>(), ref It.Ref<int>.IsAny, It.IsAny<char[]>()))
                   .Returns(Focas1.EW_OK);
            dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                machine.ReadWorkZeroDataAsync("O1234").GetAwaiter().GetResult();
            }

            dialect.Verify(d => d.UploadStart(It.IsAny<ushort>(), "O1234", It.IsAny<long>(), (short)5), Times.Once);
        }

        [Test]
        public void ReadProgramAsync_UploadStartFails_ResultIsFail()
        {
            var dialect = new Mock<IFocasDialect>();
            dialect.Setup(d => d.UploadStart(It.IsAny<ushort>(), It.IsAny<string>(), It.IsAny<long>(), It.IsAny<short>()))
                   .Returns(-3); // EW_SOCKET or any error
            dialect.Setup(d => d.UploadEnd(It.IsAny<ushort>())).Returns(Focas1.EW_OK);

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                var result = machine.ReadProgramAsync("O1234").GetAwaiter().GetResult();

                Assert.That(result.Success, Is.False);
            }
        }

        [Test]
        public void WriteProgramAsync_NullContent_ResultFail()
        {
            var dialect = new Mock<IFocasDialect>();

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                var prog = new CncProgram { Name = "O1234", Content = null };
                var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();

                Assert.That(result.Success, Is.False);
            }
        }

        [Test]
        public void WriteProgramAsync_InvalidProgramName_ValidationFails()
        {
            var dialect = new Mock<IFocasDialect>();

            using (var machine = CreateConnectedWithMockDialect(dialect))
            {
                // "PROG" does not match Fanuc O#### name rule — validator rejects before any FOCAS call
                var prog = new CncProgram { Name = "PROG", Content = "O1234\nG00 X0\nM30" };
                var result = machine.WriteProgramAsync("//CNC_MEM/USER/PATH1/", prog).GetAwaiter().GetResult();

                Assert.That(result.Success, Is.False);
            }
        }

        [Test]
        public void State_InitialState_IsDisconnected()
        {
            using (var machine = new FanucMachine(EthernetConfig()))
            {
                Assert.That(machine.State, Is.EqualTo(ConnectionState.Disconnected));
            }
        }

        [Test]
        public void Name_ReturnsConfigName()
        {
            using (var machine = new FanucMachine(EthernetConfig()))
            {
                Assert.That(machine.Name, Is.EqualTo("TestFanuc"));
            }
        }
    }
}
