using System.Diagnostics;
using System.IO.Pipes;
using VRio.Core.Device;
using VRio.Core.Protocol;
using Xunit;

namespace VRio.Core.Tests;

public class PipeFrameDecoderTests
{
    [Fact]
    public void Data_frame_delivers_payload()
    {
        var decoder = new PipeFrameDecoder();
        byte[]? got = null;
        decoder.Data += (buf, count) => got = buf.Take(count).ToArray();

        Assert.True(decoder.Feed(new byte[] { 0x00, 0x03, 0xAA, 0xBB, 0xCC }, 5));
        Assert.Equal(new byte[] { 0xAA, 0xBB, 0xCC }, got);
    }

    [Fact]
    public void Frames_survive_any_split_across_reads()
    {
        var decoder = new PipeFrameDecoder();
        var data = new List<byte>();
        var lines = new List<byte>();
        decoder.Data += (buf, count) => data.AddRange(buf.Take(count));
        decoder.Lines += lines.Add;

        // A lines frame, a 2-byte data frame, a 1-byte data frame — one byte at a time.
        byte[] stream = { 0x01, 0x03, 0x00, 0x02, 0x81, 0x01, 0x00, 0x01, 0xFC };
        foreach (byte b in stream)
            Assert.True(decoder.Feed(new[] { b }, 1));

        Assert.Equal(new byte[] { 0x03 }, lines);
        Assert.Equal(new byte[] { 0x81, 0x01, 0xFC }, data);
    }

    [Fact]
    public void Multiple_frames_in_one_read_all_deliver()
    {
        var decoder = new PipeFrameDecoder();
        var data = new List<byte>();
        var lines = new List<byte>();
        decoder.Data += (buf, count) => data.AddRange(buf.Take(count));
        decoder.Lines += lines.Add;

        byte[] chunk = { 0x00, 0x01, 0x42, 0x01, 0x00, 0x00, 0x01, 0x43 };
        Assert.True(decoder.Feed(chunk, chunk.Length));

        Assert.Equal(new byte[] { 0x42, 0x43 }, data);
        Assert.Equal(new byte[] { 0x00 }, lines);
    }

    [Fact]
    public void Unknown_frame_type_poisons_the_decoder()
    {
        var decoder = new PipeFrameDecoder();

        Assert.False(decoder.Feed(new byte[] { 0x7F }, 1));
        Assert.Contains("0x7F", decoder.Violation);
        // Poisoned: even a healthy frame is rejected until Reset.
        Assert.False(decoder.Feed(new byte[] { 0x00, 0x01, 0x42 }, 3));

        decoder.Reset();
        Assert.Null(decoder.Violation);
        Assert.True(decoder.Feed(new byte[] { 0x00, 0x01, 0x42 }, 3));
    }

    [Fact]
    public void Zero_length_data_frame_is_a_violation()
    {
        var decoder = new PipeFrameDecoder();
        Assert.False(decoder.Feed(new byte[] { 0x00, 0x00 }, 2));
        Assert.Contains("zero-length", decoder.Violation);
    }
}

public class VRioPipeServiceTests
{
    /// <summary>
    /// A contract-speaking client: connects to the service's pipe, deframes
    /// everything it receives (timestamping data bytes for the pacing test),
    /// and sends raw frame bytes.
    /// </summary>
    private sealed class TestClient : IDisposable
    {
        private readonly NamedPipeClientStream _pipe;
        private readonly Thread _reader;
        private readonly object _sync = new();
        private readonly List<byte> _data = new();
        private readonly List<long> _dataTicks = new();
        private readonly List<byte> _lines = new();
        private volatile bool _closed;

        public TestClient(string pipeName)
        {
            _pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut,
                PipeOptions.Asynchronous);

            // The server's listener may still be spinning up; retry briefly.
            var connectClock = Stopwatch.StartNew();
            while (true)
            {
                try
                {
                    _pipe.Connect(200);
                    break;
                }
                catch (Exception ex) when (ex is IOException or TimeoutException)
                {
                    if (connectClock.ElapsedMilliseconds >= 3000)
                        throw;
                    Thread.Sleep(25);
                }
            }

            var decoder = new PipeFrameDecoder();
            decoder.Data += (buf, count) =>
            {
                long now = Stopwatch.GetTimestamp();
                lock (_sync)
                    for (int i = 0; i < count; i++)
                    {
                        _data.Add(buf[i]);
                        _dataTicks.Add(now);
                    }
            };
            decoder.Lines += b => { lock (_sync) _lines.Add(b); };

            _reader = new Thread(() =>
            {
                var buffer = new byte[512];
                while (true)
                {
                    int n;
                    try { n = _pipe.Read(buffer, 0, buffer.Length); }
                    catch (Exception ex) when (ex is IOException or ObjectDisposedException) { break; }
                    if (n == 0 || !decoder.Feed(buffer, n))
                        break;
                }
                _closed = true;
            })
            { IsBackground = true };
            _reader.Start();
        }

        public bool Closed => _closed;
        public byte[] Data { get { lock (_sync) return _data.ToArray(); } }
        public long[] DataTicks { get { lock (_sync) return _dataTicks.ToArray(); } }
        public byte[] Lines { get { lock (_sync) return _lines.ToArray(); } }

        public void SendRaw(params byte[] bytes)
        {
            _pipe.Write(bytes, 0, bytes.Length);
            _pipe.Flush();
        }

        public void SendData(byte[] payload)
        {
            var frame = new byte[2 + payload.Length];
            frame[0] = PipeFraming.DataType;
            frame[1] = (byte)payload.Length;
            payload.CopyTo(frame, 2);
            SendRaw(frame);
        }

        public void Dispose()
        {
            _pipe.Dispose();
            _reader.Join(1000);
        }
    }

    private static string UniqueName() => $"vrio-test-{Guid.NewGuid():N}";

    private static void WaitFor(Func<bool> condition, int timeoutMs = 2000)
    {
        var clock = Stopwatch.StartNew();
        while (!condition())
        {
            Assert.True(clock.ElapsedMilliseconds < timeoutMs, "timed out waiting for a condition");
            Thread.Sleep(10);
        }
    }

    [Fact]
    public void Client_gets_board_present_lines_on_connect()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        service.Start();

        using var client = new TestClient(name);
        WaitFor(() => client.Lines.Length > 0);

        Assert.Equal(PipeFraming.LinesPresent, client.Lines[0]); // DTR+RTS: board present
    }

    [Fact]
    public void Peer_dtr_edges_surface_as_host_handshake()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        var edges = new List<bool>();
        service.HostHandshake += high => { lock (edges) edges.Add(high); };
        service.Start();

        using var client = new TestClient(name);
        client.SendRaw(PipeFraming.LinesType, PipeFraming.LineDtr); // DTR up
        client.SendRaw(PipeFraming.LinesType, 0x00);                // DTR down (the reset pulse)

        WaitFor(() => { lock (edges) return edges.Count == 2; });
        lock (edges)
            Assert.Equal(new[] { true, false }, edges);
    }

    [Fact]
    public void Version_request_round_trips_ack_then_reply()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        service.Start();

        using var client = new TestClient(name);
        WaitFor(() => client.Lines.Length > 0); // fully connected
        client.SendData(PacketBuilder.Build(RioCommand.VersionRequest));

        byte[] expected = new[] { (byte)RioControl.Ack }
            .Concat(PacketBuilder.VersionReply(4, 2)).ToArray();
        WaitFor(() => client.Data.Length >= expected.Length);

        Assert.Equal(expected, client.Data);
    }

    [Fact]
    public void Reply_bytes_are_paced_at_the_wire_rate()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        service.Start();

        using var client = new TestClient(name);
        WaitFor(() => client.Lines.Length > 0);
        client.SendData(PacketBuilder.Build(RioCommand.VersionRequest));

        // ACK + 4-byte VersionReply = 5 bytes = 4 inter-byte gaps ≈ 4.2 ms at
        // 9600 baud. An unpaced writer would land them in well under 1 ms.
        WaitFor(() => client.Data.Length >= 5);
        long[] ticks = client.DataTicks;
        double spanMs = (ticks[4] - ticks[0]) * 1000.0 / Stopwatch.Frequency;

        Assert.True(spanMs >= 2.5, $"5 reply bytes arrived in {spanMs:F2} ms — pipe TX is not paced");
    }

    [Fact]
    public void Unknown_frame_type_drops_the_client_and_server_recovers()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        service.Start();

        using (var bad = new TestClient(name))
        {
            WaitFor(() => bad.Lines.Length > 0);
            bad.SendRaw(0x7F); // not a frame type
            WaitFor(() => bad.Closed); // contract: log + drop, no resync
        }

        // The listener must come back for the next client.
        using var good = new TestClient(name);
        WaitFor(() => good.Lines.Length > 0);
        Assert.Equal(PipeFraming.LinesPresent, good.Lines[0]);
    }

    [Fact]
    public void Client_disconnect_drops_dtr_low()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        var edges = new List<bool>();
        service.HostHandshake += high => { lock (edges) edges.Add(high); };
        service.Start();

        using (var client = new TestClient(name))
        {
            client.SendRaw(PipeFraming.LinesType, PipeFraming.LineDtr);
            WaitFor(() => { lock (edges) return edges.Count == 1; });
        } // client leaves with DTR still high

        WaitFor(() => { lock (edges) return edges.Count == 2; });
        lock (edges)
            Assert.Equal(new[] { true, false }, edges);
    }

    [Fact]
    public void Data_split_mid_frame_still_reaches_the_device()
    {
        string name = UniqueName();
        var device = new VRioDevice();
        using var service = new VRioPipeService(device, name);
        var lamps = new List<(int Address, byte State)>();
        device.LampChanged += (address, state) => { lock (lamps) lamps.Add((address, state)); };
        service.Start();

        using var client = new TestClient(name);
        WaitFor(() => client.Lines.Length > 0);

        // LampRequest 0x00 ← 0x01, framed, then torn in half mid-payload.
        byte[] packet = PacketBuilder.Build(RioCommand.LampRequest, new byte[] { 0x00, 0x01 });
        var frame = new byte[] { PipeFraming.DataType, (byte)packet.Length }.Concat(packet).ToArray();
        client.SendRaw(frame.Take(3).ToArray());
        Thread.Sleep(20);
        client.SendRaw(frame.Skip(3).ToArray());

        WaitFor(() => { lock (lamps) return lamps.Count == 1; });
        lock (lamps)
            Assert.Equal((0x00, (byte)0x01), lamps[0]);
    }
}
