using DodoSSH.Client.Ssh;

namespace DodoSSH.Client.Terminal.Tests;

/// <summary>
/// The wire format between the host and the renderer page.
/// </summary>
/// <remarks>
/// Byte order is asserted explicitly rather than only round-tripped. Both ends of this protocol are
/// ours today, but the JavaScript side reads the header with a <c>DataView</c>, whose default is
/// big-endian — a round-trip-only test would pass just as happily with a little-endian header that the
/// page then misreads.
/// </remarks>
public sealed class TerminalFrameTests
{
    [Fact]
    public void AFrame_RoundTrips()
    {
        var frame = TerminalFrame.Create((byte)TerminalServerOpcode.Output, 42, "hello"u8);

        TerminalFrame.TryRead(frame, out var opcode, out var sessionId, out var payload).ShouldBeTrue();

        opcode.ShouldBe((byte)TerminalServerOpcode.Output);
        sessionId.ShouldBe(42u);
        payload.ToArray().ShouldBe("hello"u8.ToArray());
    }

    [Fact]
    public void TheSessionId_IsBigEndian()
    {
        var frame = TerminalFrame.Create((byte)TerminalServerOpcode.Output, 0x01020304, []);

        frame[1].ShouldBe((byte)0x01);
        frame[2].ShouldBe((byte)0x02);
        frame[3].ShouldBe((byte)0x03);
        frame[4].ShouldBe((byte)0x04);
    }

    [Fact]
    public void AnEmptyPayload_IsLegal()
    {
        // SessionOpened carries nothing.
        var frame = TerminalFrame.Create((byte)TerminalServerOpcode.SessionOpened, 1, []);

        frame.Length.ShouldBe(TerminalFrame.HeaderLength);
        TerminalFrame.TryRead(frame, out _, out _, out var payload).ShouldBeTrue();
        payload.Length.ShouldBe(0);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(4)]
    public void AFrameShorterThanTheHeader_IsRejected(int length)
    {
        // These arrive from a WebView page, so a malformed frame is untrusted input to drop rather
        // than an exceptional condition.
        TerminalFrame.TryRead(new byte[length], out _, out _, out _).ShouldBeFalse();
    }

    [Fact]
    public void Write_RejectsATooSmallDestination()
    {
        Should.Throw<ArgumentException>(() =>
            TerminalFrame.Write(new byte[4], 1, 1, "payload"u8));
    }

    [Fact]
    public void AnAcknowledgement_RoundTrips()
    {
        var payload = TerminalFrame.CreateAcknowledgementPayload(123_456);

        TerminalFrame.TryReadAcknowledgement(payload, out var rendered).ShouldBeTrue();
        rendered.ShouldBe(123_456u);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(3)]
    [InlineData(5)]
    public void AnAcknowledgementOfTheWrongLength_IsRejected(int length)
    {
        TerminalFrame.TryReadAcknowledgement(new byte[length], out _).ShouldBeFalse();
    }

    [Fact]
    public void AResize_RoundTrips()
    {
        var size = new TerminalSize(132, 43, 1320, 1075);
        var payload = TerminalFrame.CreateResizePayload(size);

        TerminalFrame.TryReadResize(payload, out var decoded).ShouldBeTrue();
        decoded.ShouldBe(size);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(7)]
    [InlineData(9)]
    public void AResizeOfTheWrongLength_IsRejected(int length)
    {
        TerminalFrame.TryReadResize(new byte[length], out _).ShouldBeFalse();
    }

    [Fact]
    public void AResizePayload_OrdersColumnsBeforeRows()
    {
        // The page builds this by hand, and columns-before-rows is the SSH convention. Swapping them
        // produces a terminal that is 24 columns by 80 rows, which looks like a rendering bug.
        var payload = TerminalFrame.CreateResizePayload(new TerminalSize(80, 24));

        payload[0].ShouldBe((byte)0);
        payload[1].ShouldBe((byte)80);
        payload[2].ShouldBe((byte)0);
        payload[3].ShouldBe((byte)24);
    }

    /// <remarks>
    /// The step byte is signed, and this is what says so. Read as unsigned, a step down arrives as 255 —
    /// which the shell clamps to the largest font it will set, so getting this wrong makes "smaller" do
    /// the most dramatic available version of "larger".
    /// </remarks>
    [Theory]
    [InlineData(1)]
    [InlineData(-1)]
    [InlineData(0)]
    [InlineData(-8)]
    public void AFontSizeStep_RoundTripsWithItsSign(int step)
    {
        var payload = TerminalFrame.CreateFontSizeStepPayload(step);

        TerminalFrame.TryReadFontSizeStep(payload, out var decoded).ShouldBeTrue();
        decoded.ShouldBe(step);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(2)]
    public void AFontSizeStepOfTheWrongLength_IsRejected(int length)
    {
        TerminalFrame.TryReadFontSizeStep(new byte[length], out _).ShouldBeFalse();
    }

    [Fact]
    public void AFontSize_IsOneUnsignedByte()
    {
        // Unsigned, unlike the step: a size is a size, and the byte is what the page reads straight into
        // xterm's fontSize option.
        TerminalFrame.CreateFontSizePayload(20).ShouldBe([(byte)20]);
    }
}
