using VPlasma.Core.Device;
using Xunit;

namespace VPlasma.Core.Tests;

public class VPlasmaDeviceTests
{
    private const byte Esc = 0x1B;

    private static void Feed(VPlasmaDevice device, params byte[] bytes)
        => device.OnReceived(bytes, bytes.Length);

    private static void Feed(VPlasmaDevice device, IEnumerable<byte> bytes)
    {
        byte[] arr = bytes.ToArray();
        device.OnReceived(arr, arr.Length);
    }

    private static byte Pixel(VPlasmaDevice device, int x, int y)
    {
        var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
        device.CopyFrame(frame);
        return frame[y * VPlasmaDevice.Width + x];
    }

    /// <summary>A full-width ESC P row the way L4PLASMA.CPP sends one.</summary>
    private static byte[] GraphicsRow(int y, params byte[] data)
    {
        var row = new List<byte> { Esc, (byte)'P', 0, (byte)y, 0, (byte)data.Length, 1 };
        row.AddRange(data);
        return row.ToArray();
    }

    // ---- graphics writes -------------------------------------------------

    [Fact]
    public void GraphicsRow_SetsPixelsMsbFirst()
    {
        var device = new VPlasmaDevice();
        Feed(device, GraphicsRow(5, 0x80, 0x01)); // xbyte 0..1

        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 5));   // MSB of byte 0
        Assert.Equal(0, Pixel(device, 1, 5));
        Assert.Equal(0, Pixel(device, 14, 5));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 15, 5));  // LSB of byte 1
        Assert.Equal(0, Pixel(device, 0, 4));
        Assert.Equal(0, Pixel(device, 0, 6));
        Assert.Equal(1, device.GraphicsRows);
    }

    [Fact]
    public void GraphicsRow_SurvivesAnyChunkBoundary()
    {
        byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray());

        // Split the same command at every possible boundary.
        for (int split = 1; split < wire.Length; ++split)
        {
            var device = new VPlasmaDevice();
            device.OnReceived(wire, split);
            byte[] rest = wire.Skip(split).ToArray();
            device.OnReceived(rest, rest.Length);

            Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3));
            Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 3));
            Assert.Equal(1, device.GraphicsRows);
        }
    }

    [Fact]
    public void GraphicsBlock_MultipleRowsAdvanceY()
    {
        var device = new VPlasmaDevice();
        // screen 0, y=10, xbyte=0, 1 byte/row, 3 rows.
        Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80);

        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 11));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 12));
        Assert.Equal(0, Pixel(device, 0, 13));
        Assert.Equal(3, device.GraphicsRows);
    }

    [Fact]
    public void GraphicsWrite_HonorsByteColumnOffset()
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'P', 0, 0, 2, 1, 1, 0xFF); // xbyte=2 → x 16..23

        Assert.Equal(0, Pixel(device, 15, 0));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 16, 0));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 23, 0));
        Assert.Equal(0, Pixel(device, 24, 0));
    }

    [Fact]
    public void GraphicsWrite_OutOfRangeRowIsConsumedNotDrawn()
    {
        var device = new VPlasmaDevice();
        Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray()));
        Feed(device, (byte)'!'); // parser must be back in text mode

        var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height];
        device.CopyFrame(frame);
        Assert.Equal(1, device.TextCharsDrawn);
        Assert.Equal(0, device.GraphicsRows);
    }

    [Fact]
    public void GraphicsWrite_OverwritesTextAttributes()
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'H', 4); // flashing text
        Feed(device, (byte)'H');         // glyph col 0 is a full bar at x=0
        Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelFlash, Pixel(device, 0, 0));

        Feed(device, GraphicsRow(0, 0x80)); // repaint that row from the wire
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
    }

    // ---- text mode ---------------------------------------------------------

    [Fact]
    public void Text_DrawsGlyphAndAdvancesCursor()
    {
        var device = new VPlasmaDevice();
        Feed(device, (byte)'H'); // 5×7 'H': column 0 = 0x7F (all seven rows)

        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 6));
        Assert.Equal(0, Pixel(device, 0, 7)); // gap row
        Assert.Equal(0, Pixel(device, 5, 0)); // gap column
        Assert.Equal(new VPlasmaDevice.Point(1, 0), device.CursorCell);
        Assert.Equal(1, device.TextCharsDrawn);
    }

    [Fact]
    public void ControlChars_MoveTheCursor()
    {
        var device = new VPlasmaDevice();

        Feed(device, 0x09, 0x09, 0x09); // HT ×3
        Assert.Equal(new VPlasmaDevice.Point(3, 0), device.CursorCell);

        Feed(device, 0x08); // BS
        Assert.Equal(new VPlasmaDevice.Point(2, 0), device.CursorCell);

        Feed(device, 0x0A); // LF
        Assert.Equal(new VPlasmaDevice.Point(2, 1), device.CursorCell);

        Feed(device, 0x0D); // CR
        Assert.Equal(new VPlasmaDevice.Point(0, 1), device.CursorCell);

        Feed(device, 0x0B); // VT
        Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);

        Feed(device, 0x0B); // VT off the top wraps to the bottom row
        Assert.Equal(new VPlasmaDevice.Point(0, 3), device.CursorCell);
    }

    [Fact]
    public void Text_WrapsAtRowAndScreenEnd()
    {
        var device = new VPlasmaDevice();
        Feed(device, Enumerable.Repeat((byte)'X', 22)); // one full 21-cell row + 1

        Assert.Equal(new VPlasmaDevice.Point(1, 1), device.CursorCell);

        Feed(device, Enumerable.Repeat((byte)'X', 21 * 3 - 1)); // exactly to the end
        Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell); // wrapped to top
    }

    [Fact]
    public void EscAt_ClearsScreenAndResetsTextState()
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'K', 4, Esc, (byte)'H', 1, (byte)'H');
        Feed(device, Esc, (byte)'@');

        Assert.Equal(0, Pixel(device, 0, 0));
        Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
        Assert.Equal(0, device.Font);
        Assert.Equal(PlasmaAttributes.None, device.Attributes);
    }

    [Fact]
    public void EscL_HomesCursorWithoutClearing()
    {
        var device = new VPlasmaDevice();
        Feed(device, (byte)'H', Esc, (byte)'L');

        Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives
    }

    [Theory]
    [InlineData(0x00, PlasmaCursorMode.Hidden)]  // the game's cursor-off
    [InlineData(0xFF, PlasmaCursorMode.Hidden)]  // the test tool's hide
    [InlineData(0x01, PlasmaCursorMode.Steady)]
    [InlineData(0x03, PlasmaCursorMode.Flashing)]
    public void EscG_SetsCursorMode(byte operand, PlasmaCursorMode expected)
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'G', operand);
        Assert.Equal(expected, device.CursorMode);
    }

    [Fact]
    public void EscK_SelectsFontGrids()
    {
        var device = new VPlasmaDevice();

        Feed(device, Esc, (byte)'K', 4); // large: 12×16 cells
        Assert.Equal(4, device.Font);
        Assert.Equal(12, device.CellWidth);
        Assert.Equal(16, device.CellHeight);

        Feed(device, (byte)'A'); // large glyph: 'A' col 1 (0x11) row 0 → 2×2 dots at (2..3, 0..1)
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 2, 0));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 3, 1));

        Feed(device, Esc, (byte)'K', 0xFF); // FF → default font 0
        Assert.Equal(0, device.Font);
        Assert.Equal(6, device.CellWidth);
    }

    [Fact]
    public void EscH_AppliesAttributes()
    {
        var device = new VPlasmaDevice();

        Feed(device, Esc, (byte)'H', 1, (byte)'H'); // half intensity
        Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelHalf, Pixel(device, 0, 0));

        Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: gap row lit
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 6, 7));

        Feed(device, Esc, (byte)'H', 3, (byte)' '); // reverse: a space renders solid
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 12, 0));
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 17, 7));

        Feed(device, Esc, (byte)'H', 0xFF, (byte)'H'); // defaults restored
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 18, 0));
        Assert.Equal(0, Pixel(device, 18, 7));
        Assert.Equal(PlasmaAttributes.None, device.Attributes);
    }

    // ---- robustness ----------------------------------------------------------

    [Fact]
    public void UnknownEscape_IsConsumedAndTextResumes()
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'Z', (byte)'H');

        Assert.Equal(1, device.TextCharsDrawn);
        Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0));
    }

    [Fact]
    public void GameStartupSequence_HidesCursor()
    {
        // L4PLASMA.CPP's constructor sends exactly ESC 'G' 0x00.
        var device = new VPlasmaDevice();
        Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default

        Feed(device, 27, (byte)'G', 0x00);
        Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode);
    }

    [Fact]
    public void SelfTestPages_ParseWithoutUnknownCommands()
    {
        for (int page = 0; page < PlasmaSelfTest.PageCount; ++page)
        {
            var device = new VPlasmaDevice();
            var complaints = new List<string>();
            device.Logged += line =>
            {
                if (line.StartsWith("Unknown", StringComparison.Ordinal)
                    || line.StartsWith("Unhandled", StringComparison.Ordinal))
                    complaints.Add(line);
            };

            byte[] bytes = PlasmaSelfTest.BuildPage(page);
            device.OnReceived(bytes, bytes.Length);

            Assert.Empty(complaints);
            Assert.True(device.BytesReceived > 0);
        }
    }

    [Fact]
    public void Reset_RestoresPowerOnState()
    {
        var device = new VPlasmaDevice();
        Feed(device, Esc, (byte)'K', 5, Esc, (byte)'H', 4, Esc, (byte)'G', 0, (byte)'H');

        device.Reset();

        Assert.Equal(0, Pixel(device, 0, 0));
        Assert.Equal(0, device.Font);
        Assert.Equal(PlasmaAttributes.None, device.Attributes);
        Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode);
        Assert.Equal(new VPlasmaDevice.Point(0, 0), device.CursorCell);
    }
}
