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

namespace NcProgramManager.Tests.Unit
{
    [TestFixture]
    public class MazakMachineTests
    {
        private MazakConnectionConfig _config;
        private Mock<IMazakFtpClient> _ftpMock;
        private Mock<INcProgramValidator> _validatorMock;
        private MazakMachine _machine;

        [SetUp]
        public void SetUp()
        {
            _config = new MazakConnectionConfig
            {
                Name = "TestMazak",
                IpAddress = "192.168.1.4",
                Port = 21,
                ProgramDirectory = "/PRG/"
            };
            _ftpMock = new Mock<IMazakFtpClient>(MockBehavior.Strict);
            // Always-ok validator: Mazak addresses files verbatim (passthrough),
            // so validation must not interfere with path/verbatim assertions.
            _validatorMock = new Mock<INcProgramValidator>();
            _validatorMock.Setup(v => v.Validate(It.IsAny<string>(), It.IsAny<string>()))
                          .Returns(ValidationResult.Ok());
            _machine = new MazakMachine(_config, _ftpMock.Object, _validatorMock.Object);
        }

        [TearDown]
        public void TearDown()
        {
            _machine?.Dispose();
        }

        // ConnectAsync

        [Test]
        public void ConnectAsync_PingTrue_StateConnectedAndResultOk()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);

            var result = _machine.ConnectAsync().GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            Assert.That(result.Value, Is.True);
            Assert.That(_machine.State, Is.EqualTo(ConnectionState.Connected));
        }

        [Test]
        public void ConnectAsync_PingFalse_StateFaultedAndResultFail()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(false);

            var result = _machine.ConnectAsync().GetAwaiter().GetResult();

            Assert.That(result.Success, Is.False);
            Assert.That(_machine.State, Is.EqualTo(ConnectionState.Faulted));
        }

        // ReadProgramAsync - verbatim filename, NO extension appended

        [Test]
        public void ReadProgramAsync_FullPath_CallsDownloadFileWithExactPath()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("EIA_DATA");
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var result = _machine.ReadProgramAsync("/PRG/1234").GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            Assert.That(result.Value, Is.EqualTo("EIA_DATA"));
            _ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
        }

        [Test]
        public void ReadProgramAsync_BareIdentifier_ResolvesWithDirectoryNoExtension()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            // Bare "1234" -> ProgramDirectory + "1234" verbatim, NO .eia / extension.
            _ftpMock.Setup(f => f.DownloadFile("/PRG/1234")).Returns("DATA");
            _machine.ConnectAsync().GetAwaiter().GetResult();

            _machine.ReadProgramAsync("1234").GetAwaiter().GetResult();

            _ftpMock.Verify(f => f.DownloadFile("/PRG/1234"), Times.Once);
            // Must NOT append any extension (e.g. .eia / .EIA).
            _ftpMock.Verify(f => f.DownloadFile(It.Is<string>(s => s.Contains("."))), Times.Never);
        }

        // WriteProgramAsync

        [Test]
        public void WriteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithRead()
        {
            // ISS-020 symmetry regression: write("1234") must upload to the SAME
            // resolved path that read("1234") downloads from ("/PRG/1234"), so the
            // roundtrip targets one location. Path comes from the identifier verbatim,
            // NOT from a directory combined with program.Name.
            const string readPath = "/PRG/1234"; // path used by ReadProgramAsync("1234")
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _ftpMock.Setup(f => f.UploadFile(readPath, "CONTENT"));
            _machine.ConnectAsync().GetAwaiter().GetResult();

            // program.Name deliberately differs from the identifier to prove the path
            // is built from the identifier ("1234"), not from program.Name.
            var prog = new CncProgram { Name = "O0001", Content = "CONTENT" };
            var result = _machine.WriteProgramAsync("1234", prog).GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            // write-path == read-path for identical input -> symmetry guard.
            _ftpMock.Verify(f => f.UploadFile(readPath, "CONTENT"), Times.Once);
        }

        [Test]
        public void WriteProgramAsync_FullPath_CallsUploadFileWithExactPath()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _ftpMock.Setup(f => f.UploadFile("/PRG/1234", "CONTENT"));
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var prog = new CncProgram { Name = "1234", Content = "CONTENT" };
            var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            _ftpMock.Verify(f => f.UploadFile("/PRG/1234", "CONTENT"), Times.Once);
        }

        [Test]
        public void WriteProgramAsync_LetterName_VerbatimPassthroughNoRename()
        {
            // Passthrough proof: a letter name "MYPART.NC" is uploaded to
            // ProgramDirectory + "MYPART.NC" UNCHANGED - no rename, no O#### enforcement,
            // no extension mangling. Validator is always-ok (Mazak passthrough).
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _ftpMock.Setup(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"));
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var prog = new CncProgram { Name = "MYPART.NC", Content = "CONTENT" };
            var result = _machine.WriteProgramAsync("MYPART.NC", prog).GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            _ftpMock.Verify(f => f.UploadFile("/PRG/MYPART.NC", "CONTENT"), Times.Once);
        }

        [Test]
        public void WriteProgramAsync_EmptyContent_FailsWithoutCallingUploadFile()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var prog = new CncProgram { Name = "1234", Content = "" };
            var result = _machine.WriteProgramAsync("/PRG/1234", prog).GetAwaiter().GetResult();

            Assert.That(result.Success, Is.False);
            _ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
        }

        [Test]
        public void WriteProgramAsync_NullProgram_FailsWithoutCallingUploadFile()
        {
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var result = _machine.WriteProgramAsync("/PRG/1234", null).GetAwaiter().GetResult();

            Assert.That(result.Success, Is.False);
            _ftpMock.Verify(f => f.UploadFile(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
        }

        // DeleteProgramAsync

        [Test]
        public void DeleteProgramAsync_BareIdentifier_ResolvesWithDirectory_SymmetricWithReadWrite()
        {
            // ISS-020 symmetry regression: delete("1234") must target the SAME
            // resolved path read/write use for "1234" ("/PRG/1234"). Path comes from
            // ResolvePath(identifier), NOT the raw identifier verbatim.
            _ftpMock.Setup(f => f.Ping()).Returns(true);
            _ftpMock.Setup(f => f.DeleteFile("/PRG/1234"));
            _machine.ConnectAsync().GetAwaiter().GetResult();

            var result = _machine.DeleteProgramAsync("1234").GetAwaiter().GetResult();

            Assert.That(result.Success, Is.True);
            _ftpMock.Verify(f => f.DeleteFile("/PRG/1234"), Times.Once);
        }

        // ReadActiveProgramAsync - not supported via FTP

        [Test]
        public void ReadActiveProgramAsync_ReturnsFailNotSupported()
        {
            var result = _machine.ReadActiveProgramAsync().GetAwaiter().GetResult();

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

        // SelectMainProgramAsync - not supported via FTP

        [Test]
        public void SelectMainProgramAsync_ReturnsFailNotSupported()
        {
            var result = _machine.SelectMainProgramAsync("/PRG/1234").GetAwaiter().GetResult();

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