//! SGR (Select Graphic Rendition) attrinvbute parsing and types.

const std = @import("std");
const build_options = @import("terminal_options");
const assert = @import("../quirks.zig").inlineAssert;
const testing = std.testing;
const lib = @import("../lib/main.zig");
const color = @import("color.zig");
const SepList = @import("Parser.zig").Action.CSI.SepList;

const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;

/// Attribute type for SGR
pub const Attribute = union(Tag) {
    /// Unset all attributes
    unset,

    /// Unknown attribute, the raw CSI command parameters are here.
    unknown: Unknown,

    /// Bold the text.
    bold,
    reset_bold,

    /// Italic text.
    italic,
    reset_italic,

    /// Faint/dim text.
    /// Note: reset faint is the same SGR code as reset bold
    faint,

    /// Underline the text
    underline: Underline,
    underline_color: color.RGB,
    @"256_underline_color": u8,
    reset_underline_color,

    // Overline the text
    overline,
    reset_overline,

    /// Blink the text
    blink,
    reset_blink,

    /// Invert fg/bg colors.
    inverse,
    reset_inverse,

    /// Invisible
    invisible,
    reset_invisible,

    /// Strikethrough the text.
    strikethrough,
    reset_strikethrough,

    /// Set foreground color as RGB values.
    direct_color_fg: color.RGB,

    /// Set background color as RGB values.
    direct_color_bg: color.RGB,

    /// Set the background/foreground as a named color attribute.
    @"8_bg": color.Name,
    @"8_fg": color.Name,

    /// Reset the fg/bg to their default values.
    reset_fg,
    reset_bg,

    /// Set the background/foreground as a named bright color attribute.
    @"8_bright_bg": color.Name,
    @"8_bright_fg": color.Name,

    /// Set background color as 256-color palette.
    @"256_bg": u8,

    /// Set foreground color as 256-color palette.
    @"256_fg": u8,

    pub const Tag = lib.Enum(
        lib_target,
        &.{
            "unset",
            "unknown",
            "bold",
            "reset_bold",
            "italic",
            "reset_italic",
            "faint",
            "underline",
            "underline_color",
            "256_underline_color",
            "reset_underline_color",
            "overline",
            "reset_overline",
            "blink",
            "reset_blink",
            "inverse",
            "reset_inverse",
            "invisible",
            "reset_invisible",
            "strikethrough",
            "reset_strikethrough",
            "direct_color_fg",
            "direct_color_bg",
            "8_bg",
            "8_fg",
            "reset_fg",
            "reset_bg",
            "8_bright_bg",
            "8_bright_fg",
            "256_bg",
            "256_fg",
        },
    );

    pub const Unknown = struct {
        /// Full is the full SGR input.
        full: []const u16,

        /// Partial is the remaining, where we got hung up.
        partial: []const u16,

        pub const C = extern struct {
            full_ptr: [*]const u16,
            full_len: usize,
            partial_ptr: [*]const u16,
            partial_len: usize,
        };

        pub fn cval(self: Unknown) Unknown.C {
            return .{
                .full_ptr = self.full.ptr,
                .full_len = self.full.len,
                .partial_ptr = self.partial.ptr,
                .partial_len = self.partial.len,
            };
        }
    };

    pub const Underline = enum(u3) {
        none = 0,
        single = 1,
        double = 2,
        curly = 3,
        dotted = 4,
        dashed = 5,

        pub const C = c_int;

        pub fn cval(self: Underline) Underline.C {
            return @intFromEnum(self);
        }
    };

    /// C ABI functions.
    const c_union = lib.TaggedUnion(
        lib_target,
        @This(),
        // Padding size for C ABI compatibility.
        // Largest variant is Unknown.C: 2 pointers + 2 usize = 32 bytes on 64-bit.
        // We use [8]u64 (64 bytes) to allow room for future expansion while
        // maintaining ABI compatibility.
        [8]u64,
    );
    pub const Value = c_union.Value;
    pub const C = c_union.C;
    pub const CValue = c_union.CValue;
    pub const cval = c_union.cval;
};

/// Parser parses the attributes from a list of SGR parameters.
pub const Parser = struct {
    params: []const u16 = &.{},
    params_sep: SepList = .initEmpty(),
    idx: usize = 0,

    /// Empty state parser.
    pub const empty: Parser = .{};

    /// Next returns the next attribute or null if there are no more attributes.
    pub fn next(self: *Parser) ?Attribute {
        if (self.idx >= self.params.len) {
            // We're more likely to not be done than to be done.
            @branchHint(.unlikely);

            // Add one to ensure we don't loop on unset
            defer self.idx += 1;

            // If we're at index zero it means we must have an empty list
            // and an empty list implicitly means unset, otherwise we're
            // done and return null.
            return if (self.idx == 0) .unset else null;
        }

        const slice = self.params[self.idx..self.params.len];
        // Call inlined for performance reasons.
        const colon = @call(
            .always_inline,
            SepList.isSet,
            .{ self.params_sep, self.idx },
        );
        self.idx += 1;

        // Our last one will have an idx be the last value.
        if (slice.len == 0) return null;

        // If we have a colon separator then we need to ensure we're
        // parsing a value that allows it.
        if (colon) {
            // Colons are fairly rare in the wild.
            @branchHint(.unlikely);

            switch (slice[0]) {
                4, 38, 48, 58 => {},

                else => {
                    // In real world use it's very rare
                    // that we receive an invalid sequence.
                    @branchHint(.cold);

                    // Consume all the colon separated
                    // values and return them as unknown.
                    const start = self.idx;
                    while (self.params_sep.isSet(self.idx)) self.idx += 1;
                    self.idx += 1;
                    return .{ .unknown = .{
                        .full = self.params,
                        .partial = slice[0..@min(self.idx - start + 1, slice.len)],
                    } };
                },
            }
        }

        switch (slice[0]) {
            0 => return .unset,

            1 => return .bold,

            2 => return .faint,

            3 => return .italic,

            4 => underline: {
                if (colon) {
                    // Colons are fairly rare in the wild.
                    @branchHint(.unlikely);

                    // A trailing colon with no following sub-param
                    // (e.g. "ESC[58:4:m") leaves the colon separator
                    // bit set on the last param without adding another
                    // entry, so we can see param 4 with a colon but
                    // nothing after it.
                    if (slice.len < 2) {
                        @branchHint(.cold);
                        break :underline;
                    }

                    if (self.isColon()) {
                        // Invalid/unknown SGRs are just not very likely.
                        @branchHint(.cold);

                        self.consumeUnknownColon();
                        break :underline;
                    }

                    self.idx += 1;
                    return .{
                        .underline = switch (slice[1]) {
                            0 => .none,
                            1 => .single,
                            2 => .double,
                            3 => .curly,
                            4 => .dotted,
                            5 => .dashed,

                            // For unknown underline styles,
                            // just render a single underline.
                            else => single: {
                                // This is quite a rare condition.
                                @branchHint(.cold);
                                break :single .single;
                            },
                        },
                    };
                }

                return .{ .underline = .single };
            },

            5 => return .blink,

            6 => return .blink,

            7 => return .inverse,

            8 => return .invisible,

            9 => return .strikethrough,

            21 => return .{ .underline = .double },

            22 => return .reset_bold,

            23 => return .reset_italic,

            24 => return .{ .underline = .none },

            25 => return .reset_blink,

            27 => return .reset_inverse,

            28 => return .reset_invisible,

            29 => return .reset_strikethrough,

            30...37 => return .{
                .@"8_fg" = @enumFromInt(slice[0] - 30),
            },

            38 => if (slice.len >= 2) {
                // We are very likely to have enough parameters.
                @branchHint(.likely);

                switch (slice[1]) {
                    // `2` indicates direct-color (r, g, b).
                    // We need at least 3 more params for this to make sense.
                    2 => if (self.parseDirectColor(
                        .direct_color_fg,
                        slice,
                        colon,
                    )) |v| return v,

                    // `5` indicates indexed color.
                    5 => if (slice.len >= 3) {
                        // We are very likely to have enough parameters.
                        @branchHint(.likely);

                        self.idx += 2;
                        return .{
                            .@"256_fg" = @truncate(slice[2]),
                        };
                    },

                    else => {},
                }
            },

            39 => return .reset_fg,

            40...47 => return .{
                .@"8_bg" = @enumFromInt(slice[0] - 40),
            },

            48 => if (slice.len >= 2) {
                // We are very likely to have enough parameters.
                @branchHint(.likely);

                switch (slice[1]) {
                    // `2` indicates direct-color (r, g, b).
                    // We need at least 3 more params for this to make sense.
                    2 => if (self.parseDirectColor(
                        .direct_color_bg,
                        slice,
                        colon,
                    )) |v| return v,

                    // `5` indicates indexed color.
                    5 => if (slice.len >= 3) {
                        // We are very likely to have enough parameters.
                        @branchHint(.likely);

                        self.idx += 2;
                        return .{
                            .@"256_bg" = @truncate(slice[2]),
                        };
                    },

                    else => {},
                }
            },

            49 => return .reset_bg,

            53 => return .overline,
            55 => return .reset_overline,

            58 => if (slice.len >= 2) {
                // We are very likely to have enough parameters.
                @branchHint(.likely);

                switch (slice[1]) {
                    // `2` indicates direct-color (r, g, b).
                    // We need at least 3 more params for this to make sense.
                    2 => if (self.parseDirectColor(
                        .underline_color,
                        slice,
                        colon,
                    )) |v| return v,

                    // `5` indicates indexed color.
                    5 => if (slice.len >= 3) {
                        // We are very likely to have enough parameters.
                        @branchHint(.likely);

                        self.idx += 2;
                        return .{
                            .@"256_underline_color" = @truncate(slice[2]),
                        };
                    },
                    else => {},
                }
            },

            59 => return .reset_underline_color,

            90...97 => return .{
                // 82 instead of 90 to offset to "bright" colors
                .@"8_bright_fg" = @enumFromInt(slice[0] - 82),
            },

            100...107 => return .{
                .@"8_bright_bg" = @enumFromInt(slice[0] - 92),
            },

            else => {},
        }

        return .{ .unknown = .{ .full = self.params, .partial = slice } };
    }

    fn parseDirectColor(
        self: *Parser,
        comptime tag: Attribute.Tag,
        slice: []const u16,
        colon: bool,
    ) ?Attribute {
        // Any direct color style must have at least 5 values.
        if (slice.len < 5) return null;

        // Only used for direct color sets (38, 48, 58) and subparam 2.
        assert(slice[1] == 2);

        // Note: We use @truncate because the value should be 0 to 255. If
        // it isn't, the behavior is undefined so we just... truncate it.

        // If we don't have a colon, then we expect exactly 3 semicolon
        // separated values.
        if (!colon) {
            // Semicolons are much more common than colons.
            @branchHint(.likely);

            self.idx += 4;
            return @unionInit(Attribute, @tagName(tag), .{
                .r = @truncate(slice[2]),
                .g = @truncate(slice[3]),
                .b = @truncate(slice[4]),
            });
        }

        // We have a colon, we might have either 5 or 6 values depending
        // on if the colorspace is present.
        const count = self.countColon();
        switch (count) {
            3 => {
                // This is the much more common case in the wild.
                @branchHint(.likely);

                self.idx += 4;
                return @unionInit(Attribute, @tagName(tag), .{
                    .r = @truncate(slice[2]),
                    .g = @truncate(slice[3]),
                    .b = @truncate(slice[4]),
                });
            },

            4 => {
                self.idx += 5;
                return @unionInit(Attribute, @tagName(tag), .{
                    .r = @truncate(slice[3]),
                    .g = @truncate(slice[4]),
                    .b = @truncate(slice[5]),
                });
            },

            else => {
                // Invalid/unknown SGRs just don't happen very often at all.
                @branchHint(.cold);

                self.consumeUnknownColon();
                return null;
            },
        }
    }

    /// Returns true if the present position has a colon separator.
    /// This always returns false for the last value since it has no
    /// separator.
    inline fn isColon(self: *Parser) bool {
        // Call inlined for performance reasons.
        return @call(
            .always_inline,
            SepList.isSet,
            .{ self.params_sep, self.idx },
        );
    }

    fn countColon(self: *Parser) usize {
        var count: usize = 0;
        var idx = self.idx;
        while (idx < self.params.len - 1 and self.params_sep.isSet(idx)) : (idx += 1) {
            count += 1;
        }
        return count;
    }

    /// Consumes all the remaining parameters separated by a colon and
    /// returns an unknown attribute.
    fn consumeUnknownColon(self: *Parser) void {
        const count = self.countColon();
        self.idx += count + 1;
    }
};

fn testParse(params: []const u16) Attribute {
    var p: Parser = .{ .params = params };
    return p.next().?;
}

fn testParseColon(params: []const u16) Attribute {
    var p: Parser = .{ .params = params };
    // Mark all parameters except the last as having a colon after.
    for (0..params.len - 1) |i| p.params_sep.set(i);
    return p.next().?;
}

test "sgr: Attribute C compat" {
    _ = Attribute.C;
}

test "sgr: Parser" {
    try testing.expect(testParse(&[_]u16{}) == .unset);
    try testing.expect(testParse(&[_]u16{0}) == .unset);

    {
        const v = testParse(&[_]u16{ 38, 2, 40, 44, 52 });
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 40), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 44), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 52), v.direct_color_fg.b);
    }

    try testing.expect(testParse(&[_]u16{ 38, 2, 44, 52 }) == .unknown);

    {
        const v = testParse(&[_]u16{ 48, 2, 40, 44, 52 });
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 40), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 44), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 52), v.direct_color_bg.b);
    }

    try testing.expect(testParse(&[_]u16{ 48, 2, 44, 52 }) == .unknown);
}

test "sgr: Parser multiple" {
    var p: Parser = .{ .params = &[_]u16{ 0, 38, 2, 40, 44, 52 } };
    try testing.expect(p.next().? == .unset);
    try testing.expect(p.next().? == .direct_color_fg);
    try testing.expect(p.next() == null);
    try testing.expect(p.next() == null);
}

test "sgr: unsupported with colon" {
    var p: Parser = .{
        .params = &[_]u16{ 0, 4, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            break :sep list;
        },
    };
    try testing.expect(p.next().? == .unknown);
    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: unsupported with multiple colon" {
    var p: Parser = .{
        .params = &[_]u16{ 0, 4, 2, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            list.set(1);
            break :sep list;
        },
    };
    try testing.expect(p.next().? == .unknown);
    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: bold" {
    {
        const v = testParse(&[_]u16{1});
        try testing.expect(v == .bold);
    }

    {
        const v = testParse(&[_]u16{22});
        try testing.expect(v == .reset_bold);
    }
}

test "sgr: italic" {
    {
        const v = testParse(&[_]u16{3});
        try testing.expect(v == .italic);
    }

    {
        const v = testParse(&[_]u16{23});
        try testing.expect(v == .reset_italic);
    }
}

test "sgr: underline" {
    {
        const v = testParse(&[_]u16{4});
        try testing.expect(v == .underline);
    }

    {
        const v = testParse(&[_]u16{24});
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .none);
    }
}

test "sgr: underline styles" {
    {
        const v = testParseColon(&[_]u16{ 4, 2 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .double);
    }

    {
        const v = testParseColon(&[_]u16{ 4, 0 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .none);
    }

    {
        const v = testParseColon(&[_]u16{ 4, 1 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .single);
    }

    {
        const v = testParseColon(&[_]u16{ 4, 3 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .curly);
    }

    {
        const v = testParseColon(&[_]u16{ 4, 4 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .dotted);
    }

    {
        const v = testParseColon(&[_]u16{ 4, 5 });
        try testing.expect(v == .underline);
        try testing.expect(v.underline == .dashed);
    }
}

test "sgr: underline style with more" {
    var p: Parser = .{
        .params = &[_]u16{ 4, 2, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            break :sep list;
        },
    };

    try testing.expect(p.next().? == .underline);
    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: underline style with too many colons" {
    var p: Parser = .{
        .params = &[_]u16{ 4, 2, 3, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            list.set(1);
            break :sep list;
        },
    };

    try testing.expect(p.next().? == .unknown);
    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: blink" {
    {
        const v = testParse(&[_]u16{5});
        try testing.expect(v == .blink);
    }

    {
        const v = testParse(&[_]u16{6});
        try testing.expect(v == .blink);
    }

    {
        const v = testParse(&[_]u16{25});
        try testing.expect(v == .reset_blink);
    }
}

test "sgr: inverse" {
    {
        const v = testParse(&[_]u16{7});
        try testing.expect(v == .inverse);
    }

    {
        const v = testParse(&[_]u16{27});
        try testing.expect(v == .reset_inverse);
    }
}

test "sgr: strikethrough" {
    {
        const v = testParse(&[_]u16{9});
        try testing.expect(v == .strikethrough);
    }

    {
        const v = testParse(&[_]u16{29});
        try testing.expect(v == .reset_strikethrough);
    }
}

test "sgr: 8 color" {
    var p: Parser = .{ .params = &[_]u16{ 31, 43, 90, 103 } };

    {
        const v = p.next().?;
        try testing.expect(v == .@"8_fg");
        try testing.expect(v.@"8_fg" == .red);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .@"8_bg");
        try testing.expect(v.@"8_bg" == .yellow);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .@"8_bright_fg");
        try testing.expect(v.@"8_bright_fg" == .bright_black);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .@"8_bright_bg");
        try testing.expect(v.@"8_bright_bg" == .bright_yellow);
    }
}

test "sgr: 256 color" {
    var p: Parser = .{ .params = &[_]u16{ 38, 5, 161, 48, 5, 236 } };
    try testing.expect(p.next().? == .@"256_fg");
    try testing.expect(p.next().? == .@"256_bg");
    try testing.expect(p.next() == null);
}

test "sgr: 256 color underline" {
    var p: Parser = .{ .params = &[_]u16{ 58, 5, 9 } };
    try testing.expect(p.next().? == .@"256_underline_color");
    try testing.expect(p.next() == null);
}

test "sgr: 24-bit bg color" {
    {
        const v = testParseColon(&[_]u16{ 48, 2, 1, 2, 3 });
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b);
    }
}

test "sgr: underline color" {
    {
        const v = testParseColon(&[_]u16{ 58, 2, 1, 2, 3 });
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 1), v.underline_color.r);
        try testing.expectEqual(@as(u8, 2), v.underline_color.g);
        try testing.expectEqual(@as(u8, 3), v.underline_color.b);
    }

    {
        const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 1), v.underline_color.r);
        try testing.expectEqual(@as(u8, 2), v.underline_color.g);
        try testing.expectEqual(@as(u8, 3), v.underline_color.b);
    }
}

test "sgr: reset underline color" {
    var p: Parser = .{ .params = &[_]u16{59} };
    try testing.expect(p.next().? == .reset_underline_color);
}

test "sgr: invisible" {
    var p: Parser = .{ .params = &[_]u16{ 8, 28 } };
    try testing.expect(p.next().? == .invisible);
    try testing.expect(p.next().? == .reset_invisible);
}

test "sgr: underline, bg, and fg" {
    var p: Parser = .{
        .params = &[_]u16{ 4, 38, 2, 255, 247, 219, 48, 2, 242, 93, 147, 4 },
    };
    {
        const v = p.next().?;
        try testing.expect(v == .underline);
        try testing.expectEqual(Attribute.Underline.single, v.underline);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 255), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 247), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 219), v.direct_color_fg.b);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 242), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 93), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 147), v.direct_color_bg.b);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .underline);
        try testing.expectEqual(Attribute.Underline.single, v.underline);
    }
}

test "sgr: direct color fg missing color" {
    // This used to crash
    var p: Parser = .{ .params = &[_]u16{ 38, 5 } };
    while (p.next()) |_| {}
}

test "sgr: direct color bg missing color" {
    // This used to crash
    var p: Parser = .{ .params = &[_]u16{ 48, 5 } };
    while (p.next()) |_| {}
}

test "sgr: direct fg/bg/underline ignore optional color space" {
    // These behaviors have been verified against xterm.

    // Colon version should skip the optional color space identifier
    {
        // 3 8 : 2 : Pi : Pr : Pg : Pb
        const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3 });
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
    }
    {
        // 4 8 : 2 : Pi : Pr : Pg : Pb
        const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3 });
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b);
    }
    {
        // 5 8 : 2 : Pi : Pr : Pg : Pb
        const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3 });
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 1), v.underline_color.r);
        try testing.expectEqual(@as(u8, 2), v.underline_color.g);
        try testing.expectEqual(@as(u8, 3), v.underline_color.b);
    }

    // Semicolon version should not parse optional color space identifier
    {
        // 3 8 ; 2 ; Pr ; Pg ; Pb
        const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3 });
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 0), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 1), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 2), v.direct_color_fg.b);
    }
    {
        // 4 8 ; 2 ; Pr ; Pg ; Pb
        const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3 });
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 0), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 1), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 2), v.direct_color_bg.b);
    }
    {
        // 5 8 ; 2 ; Pr ; Pg ; Pb
        const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3 });
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 0), v.underline_color.r);
        try testing.expectEqual(@as(u8, 1), v.underline_color.g);
        try testing.expectEqual(@as(u8, 2), v.underline_color.b);
    }
}

test "sgr: direct fg colon with too many colons" {
    var p: Parser = .{
        .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 4, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            for (0..6) |idx| list.set(idx);
            break :sep list;
        },
    };

    try testing.expect(p.next().? == .unknown);
    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: direct fg colon with colorspace and extra param" {
    var p: Parser = .{
        .params = &[_]u16{ 38, 2, 0, 1, 2, 3, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            for (0..5) |idx| list.set(idx);
            break :sep list;
        },
    };

    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
    }

    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

test "sgr: direct fg colon no colorspace and extra param" {
    var p: Parser = .{
        .params = &[_]u16{ 38, 2, 1, 2, 3, 1 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            for (0..4) |idx| list.set(idx);
            break :sep list;
        },
    };

    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
    }

    try testing.expect(p.next().? == .bold);
    try testing.expect(p.next() == null);
}

// Kakoune sent this complex SGR sequence that caused invalid behavior.
test "sgr: kakoune input" {
    // This used to crash
    var p: Parser = .{
        .params = &[_]u16{ 0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(1);
            list.set(8);
            list.set(9);
            list.set(10);
            list.set(11);
            list.set(12);
            break :sep list;
        },
    };

    {
        const v = p.next().?;
        try testing.expect(v == .unset);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .underline);
        try testing.expectEqual(Attribute.Underline.curly, v.underline);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 175), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 175), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 215), v.direct_color_fg.b);
    }
    {
        const v = p.next().?;
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 190), v.underline_color.r);
        try testing.expectEqual(@as(u8, 80), v.underline_color.g);
        try testing.expectEqual(@as(u8, 70), v.underline_color.b);
    }

    //try testing.expect(p.next() == null);
}

// Discussion #5930, another input sent by kakoune
test "sgr: kakoune input issue underline, fg, and bg" {
    // echo -e "\033[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136mset everything in one sequence, broken\033[m"

    // This used to crash
    var p: Parser = .{
        .params = &[_]u16{ 4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            break :sep list;
        },
    };

    {
        const v = p.next().?;
        try testing.expect(v == .underline);
        try testing.expectEqual(Attribute.Underline.curly, v.underline);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_fg);
        try testing.expectEqual(@as(u8, 51), v.direct_color_fg.r);
        try testing.expectEqual(@as(u8, 51), v.direct_color_fg.g);
        try testing.expectEqual(@as(u8, 51), v.direct_color_fg.b);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .direct_color_bg);
        try testing.expectEqual(@as(u8, 170), v.direct_color_bg.r);
        try testing.expectEqual(@as(u8, 170), v.direct_color_bg.g);
        try testing.expectEqual(@as(u8, 170), v.direct_color_bg.b);
    }

    {
        const v = p.next().?;
        try testing.expect(v == .underline_color);
        try testing.expectEqual(@as(u8, 255), v.underline_color.r);
        try testing.expectEqual(@as(u8, 97), v.underline_color.g);
        try testing.expectEqual(@as(u8, 136), v.underline_color.b);
    }

    try testing.expect(p.next() == null);
}

// Fuzz crash: afl-out/stream/default/crashes/id:000021
// Input "ESC [ 5 8 : 4 : m" produces params [58, 4] with colon
// separator bits set at indices 0 and 1. The trailing colon causes
// the second iteration to see param 4 (underline) with a colon,
// triggering assert(slice.len >= 2) with slice.len == 1.
test "sgr: underline colon with trailing separator and short slice" {
    var p: Parser = .{
        .params = &[_]u16{ 58, 4 },
        .params_sep = sep: {
            var list = SepList.initEmpty();
            list.set(0);
            list.set(1);
            break :sep list;
        },
    };

    // 58:4 is not a valid underline color (sub-param 4 is not 2 or 5),
    // so it falls through as unknown.
    try testing.expect(p.next().? == .unknown);

    // Param 4 with a trailing colon but no sub-param is malformed,
    // so it also falls through as unknown rather than panicking.
    try testing.expect(p.next().? == .unknown);

    try testing.expect(p.next() == null);
}
