//! Kitty's desktop notification protocol (OSC 99)
//! Specification: https://sw.kovidgoyal.net/kitty/desktop-notifications/

const std = @import("std");

const assert = @import("../../../quirks.zig").inlineAssert;

const Parser = @import("../../osc.zig").Parser;
const Command = @import("../../osc.zig").Command;
const Terminator = @import("../../osc.zig").Terminator;
const kitty_metadata = @import("../kitty_metadata.zig");
const encoding = @import("../encoding.zig");

const log = std.log.scoped(.kitty_desktop_notification);

pub const MAX_PLAIN_PAYLOAD_BYTES = 2048;
pub const MAX_ENCODED_PAYLOAD_BYTES = 4096;

pub const OSC = struct {
    /// The raw metadata that was received. It can be parsed by using the `readOption` method.
    metadata: []const u8,
    /// The raw payload. It may be Base64 encoded, check the `e` option.
    payload: []const u8,
    /// The terminator that was used in case we need to send a response.
    terminator: Terminator,

    /// Decode an option from the metadata.
    pub fn readOption(self: OSC, comptime key: Option) key.Type() {
        return key.read(self.metadata);
    }
};

pub const Action = packed struct {
    focus: bool,
    report: bool,

    pub const default: Action = .{
        .focus = true,
        .report = false,
    };

    pub fn init(str: []const u8) Action {
        return parsePackedStruct(Action, str);
    }
};

pub const Occasion = enum {
    always,
    invisible,
    unfocused,

    pub const default: Occasion = .always;

    pub fn init(str: []const u8) Occasion {
        return std.meta.stringToEnum(Occasion, str) orelse .default;
    }
};

pub const Payload = enum {
    alive,
    body,
    buttons,
    close,
    icon,
    query,
    title,
    /// This is a special value to indicate that an unknown payload value was
    /// specified and it should be ignored.
    unknown,

    pub const default: Payload = .title;

    pub fn init(str: []const u8) Payload {
        if (str.len == 1 and str[0] == '?') return .query;
        // The string `query` is not allowed, it should be a single question
        // mark if you want a query.
        if (std.mem.eql(u8, "query", str)) return .unknown;
        return std.meta.stringToEnum(Payload, str) orelse .unknown;
    }
};

pub const Urgency = enum {
    low,
    normal,
    high,

    pub const default: Urgency = .normal;

    pub fn init(str: []const u8) Urgency {
        if (str.len != 1) return .default;
        return switch (str[0]) {
            '0' => .low,
            '1' => .normal,
            '2' => .high,
            else => .default,
        };
    }
};

pub const Option = enum {
    /// What action(s) should be taken when a notification is clicked.
    a,
    /// Should a notification be sent to the application when the notification
    /// is closed?
    c,
    /// Are we done with the notification, and it is ready to be sent?
    d,
    /// Is the payload encoded with Base64?
    e,
    /// The name of the application that is sending the notification.
    f,
    /// Identifier for icon data. Only used when the payload is icon data.
    g,
    /// Identifier for the notification.
    i,
    /// Icon name.
    n,
    /// When to honor the notification request.
    o,
    /// Type of the payload.
    p,
    /// The sound name to play with the notification.
    s,
    /// The type of the notification.
    t,
    /// The urgency of the notification.
    u,
    /// When to auto-close the notification.
    w,

    pub fn Type(comptime key: Option) type {
        return switch (key) {
            .a => Action,
            .c => bool,
            .d => bool,
            .e => bool,
            .f => ?[]const u8,
            .g => ?[]const u8,
            .i => ?[]const u8,
            .n => kitty_metadata.ValueIterator(
                @tagName(key),
                valid_metadata_value_characters,
            ),
            .o => Occasion,
            .p => Payload,
            .s => []const u8,
            .t => kitty_metadata.ValueIterator(
                @tagName(key),
                valid_metadata_value_characters,
            ),
            .u => Urgency,
            .w => i32,
        };
    }

    pub fn default(comptime key: Option) key.Type() {
        return switch (key) {
            .a => .default,
            .c => false,
            .d => true,
            .e => false,
            .f => null,
            .g => null,
            .i => null,
            .n => unreachable,
            .o => .default,
            .p => .default,
            .s => "system",
            .t => unreachable,
            .u => .default,
            .w => -1,
        };
    }

    /// Read the option value from the raw metadata string.
    ///
    /// Unknown and malformed values are ignored. Optional values return null;
    /// all other values return the protocol default.
    pub fn read(
        comptime key: Option,
        metadata: []const u8,
    ) key.Type() {
        var it: kitty_metadata.ValueIterator(
            @tagName(key),
            valid_metadata_value_characters,
        ) = switch (key) {
            .t, .n => return .init(metadata),
            else => .init(metadata),
        };

        const value = it.next() orelse return key.default();

        // return the parsed value, the iterator guarantees that it's a valid
        // metadata value
        return switch (key) {
            .a => .init(value),
            .c => parseBool(value) orelse key.default(),
            .d => parseBool(value) orelse key.default(),
            .e => parseBool(value) orelse key.default(),
            .f => value,
            .g => parseIdentifier(value),
            .i => parseIdentifier(value),
            .n => unreachable,
            .o => .init(value),
            .p => .init(value),
            .s => value,
            .t => unreachable,
            .u => .init(value),
            .w => value: {
                // Zig's integer parser allows '_', we don't
                if (std.mem.indexOfScalar(u8, value, '_')) |_| break :value key.default();
                const tmp = std.fmt.parseInt(i32, value, 10) catch break :value key.default();
                // negative values less than -1 are not allowed
                if (tmp < -1) break :value key.default();
                break :value tmp;
            },
        };
    }
};

/// Parse the protocol's booleans
fn parseBool(str: []const u8) ?bool {
    if (str.len != 1) return null;
    return switch (str[0]) {
        '0' => false,
        '1' => true,
        else => null,
    };
}

/// This is similar to the packed struct parser used in the configs. The
/// differences are that a literal `true` or `false` value does not turn on/off
/// all the values, and the negation prefix is `-` not `no-`.
fn parsePackedStruct(comptime T: type, str: []const u8) T {
    const info = @typeInfo(T).@"struct";
    comptime assert(info.layout == .@"packed");

    var result: T = .default;

    // We split each value by ","
    var iter = std.mem.splitSequence(u8, str, ",");
    loop: while (iter.next()) |raw| {
        // Determine the field we're looking for and the value. If the
        // field is prefixed with "-" then we set the value to false.
        const part, const value = part: {
            const negation_prefix = "-";
            const trimmed = std.mem.trim(u8, raw, &std.ascii.whitespace);
            if (std.mem.startsWith(u8, trimmed, negation_prefix)) {
                break :part .{ trimmed[negation_prefix.len..], false };
            } else {
                break :part .{ trimmed, true };
            }
        };

        inline for (info.fields) |field| {
            comptime assert(field.type == bool);
            if (std.mem.eql(u8, field.name, part)) {
                @field(result, field.name) = value;
                continue :loop;
            }
        }

        // No field matched
        return .default;
    }

    return result;
}

/// Characters that are valid in a metadata value. Including `=` is technically
/// against the spec but is needed since Base64 encoded values (with padding)
/// are valid for some options. Including `?` is technically against the spec
/// but is needed since it is a valid value for the `p` option.
const valid_metadata_value_characters: []const u8 = valid_identifier_characters ++ "/,(){}[]*&^%$#@!`~=?";

/// Characters that are valid in identifiers.
const valid_identifier_characters: []const u8 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+.";

fn isValidIdentifier(str: []const u8) bool {
    return std.mem.indexOfNone(u8, str, valid_identifier_characters) == null;
}

fn parseIdentifier(str: []const u8) ?[]const u8 {
    if (isValidIdentifier(str)) return str;
    return null;
}

pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command {
    assert(parser.state == .@"99");

    const cap = if (parser.capture) |*c| c else {
        parser.state = .invalid;
        return null;
    };

    const data = cap.trailing();

    const payload_start = std.mem.indexOfScalar(u8, data, ';') orelse {
        log.warn("missing semicolon before payload", .{});
        parser.state = .invalid;
        return null;
    };

    const metadata = data[0..payload_start];
    const payload = data[payload_start + 1 .. data.len];

    const max_payload_bytes: usize = if (Option.e.read(metadata))
        MAX_ENCODED_PAYLOAD_BYTES
    else
        MAX_PLAIN_PAYLOAD_BYTES;
    if (payload.len > max_payload_bytes) {
        log.warn(
            "payload is too large: size={d} max={d}",
            .{ payload.len, max_payload_bytes },
        );
        parser.state = .invalid;
        return null;
    }

    // Payload has to be an escape-code-safe UTF-8 string.
    if (!encoding.isSafeUtf8(payload)) {
        log.warn("payload is not escape code safe UTF-8", .{});
        parser.state = .invalid;
        return null;
    }

    parser.command = .{
        .kitty_desktop_notification = .{
            .metadata = metadata,
            .payload = payload,
            .terminator = .init(terminator_ch),
        },
    };

    return &parser.command;
}

test "OSC 99: empty metadata and payload" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;;";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("", cmd.kitty_desktop_notification.metadata);
    try testing.expectEqualStrings("", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Option.a.default(), cmd.kitty_desktop_notification.readOption(.a));
    try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.f) == null);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null);
    {
        var it = cmd.kitty_desktop_notification.readOption(.n);
        try testing.expect(it.next() == null);
    }
    try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o));
    try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p));
    try testing.expectEqualStrings("system", cmd.kitty_desktop_notification.readOption(.s));
    {
        var it = cmd.kitty_desktop_notification.readOption(.t);
        try testing.expect(it.next() == null);
    }
    try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u));
    try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w));
    try testing.expectEqual(.st, cmd.kitty_desktop_notification.terminator);
}

test "OSC 99: empty metadata with payload" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;;bobr";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("", cmd.kitty_desktop_notification.metadata);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Option.a.default(), cmd.kitty_desktop_notification.readOption(.a));
    try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.f) == null);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null);
    {
        var it = cmd.kitty_desktop_notification.readOption(.n);
        try testing.expect(it.next() == null);
    }
    try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o));
    try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p));
    try testing.expectEqualStrings("system", cmd.kitty_desktop_notification.readOption(.s));
    {
        var it = cmd.kitty_desktop_notification.readOption(.t);
        try testing.expect(it.next() == null);
    }
    try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u));
    try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w));
}

test "OSC 99: payload size limits" {
    const testing = std.testing;
    const cases = [_]struct {
        metadata: []const u8,
        payload_size: usize,
        valid: bool,
    }{
        .{ .metadata = "", .payload_size = MAX_PLAIN_PAYLOAD_BYTES, .valid = true },
        .{ .metadata = "", .payload_size = MAX_PLAIN_PAYLOAD_BYTES + 1, .valid = false },
        .{ .metadata = "e=1", .payload_size = MAX_ENCODED_PAYLOAD_BYTES, .valid = true },
        .{ .metadata = "e=1", .payload_size = MAX_ENCODED_PAYLOAD_BYTES + 1, .valid = false },
    };

    for (cases) |case| {
        var p: Parser = .init(testing.allocator);
        defer p.deinit();

        for ("99;") |ch| p.next(ch);
        for (case.metadata) |ch| p.next(ch);
        p.next(';');
        for (0..case.payload_size) |_| p.next('a');

        try testing.expectEqual(case.valid, p.end('\x1b') != null);
    }
}

test "OSC 99: unknown prefix does not hide dotted identifier" {
    const testing = std.testing;

    var p: Parser = .init(null);
    const input = "99;invalid=wrong:i=org.ghostty;payload";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expectEqualStrings(
        "org.ghostty",
        cmd.kitty_desktop_notification.readOption(.i).?,
    );
}

test "OSC 99: single parameter i" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i=bobr;payload";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?);
    try testing.expectEqualStrings("payload", cmd.kitty_desktop_notification.payload);
}

test "OSC 99: repeated parameter i" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i=bobr:i=foobar;payload";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?);
    try testing.expectEqualStrings("payload", cmd.kitty_desktop_notification.payload);
}

test "OSC 99: multiple types" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;t=mail: t = chat : t = alert ;notification";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("notification", cmd.kitty_desktop_notification.payload);
    var it = cmd.kitty_desktop_notification.readOption(.t);
    try testing.expectEqualStrings("mail", it.next().?);
    try testing.expectEqualStrings("chat", it.next().?);
    try testing.expectEqualStrings("alert", it.next().?);
    try testing.expect(it.next() == null);
}

test "OSC 99: a 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;a=report,focus;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Action{ .report = true, .focus = true }, cmd.kitty_desktop_notification.readOption(.a));
}

test "OSC 99: a 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;a=report,-focus;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Action{ .report = true, .focus = false }, cmd.kitty_desktop_notification.readOption(.a));
}

test "OSC 99: a 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;a=-report,focus;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Action{ .report = false, .focus = true }, cmd.kitty_desktop_notification.readOption(.a));
}

test "OSC 99: a 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;a=-report,-focus;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualDeep(Action{ .report = false, .focus = false }, cmd.kitty_desktop_notification.readOption(.a));
}

test "OSC 99: c 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=0;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false);
}

test "OSC 99: c 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=1;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == true);
}

test "OSC 99: c 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.c) == false);
}

test "OSC 99: d 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;d=0;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == false);
}

test "OSC 99: d 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;d=1;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true);
}

test "OSC 99: d 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;d=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.d) == true);
}

test "OSC 99: e 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;e=0;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false);
}

test "OSC 99: e 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;e=1;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == true);
}

test "OSC 99: e 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;e=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.e) == false);
}

test "OSC 99: f 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;f=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.f).?);
}

test "OSC 99: f 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=0:f= R2hvc3R0eQ== ;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.f).?);
}

test "OSC 99: g 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=0:g=7f8a9129-a35d-4e9f-8043-ce2700e15e2c;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("7f8a9129-a35d-4e9f-8043-ce2700e15e2c", cmd.kitty_desktop_notification.readOption(.g).?);
}

test "OSC 99: g 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;c=0:g=aaa*bbb;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.g) == null);
}

test "OSC 99: i 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expect(cmd.kitty_desktop_notification.readOption(.i) == null);
}

test "OSC 99: i 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?);
}

test "OSC 99: i 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i=;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("", cmd.kitty_desktop_notification.readOption(.i).?);
}

test "OSC 99: i 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i= :;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("", cmd.kitty_desktop_notification.readOption(.i).?);
}

test "OSC 99: i 5" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i= bobr ;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?);
}

test "OSC 99: i 6" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;i= bobr : i=kurwa ;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("bobr", cmd.kitty_desktop_notification.readOption(.i).?);
}

test "OSC 99: n 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;n=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    var it = cmd.kitty_desktop_notification.readOption(.n);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expect(it.next() == null);
}

test "OSC 99: n 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;n=R2hvc3R0eQ==:n=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    var it = cmd.kitty_desktop_notification.readOption(.n);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expect(it.next() == null);
}

test "OSC 99: o 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;o= ;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o));
}

test "OSC 99: o 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;o=always;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o));
}

test "OSC 99: o 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;o=unfocused;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.unfocused, cmd.kitty_desktop_notification.readOption(.o));
}

test "OSC 99: o 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;o=invisible;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.invisible, cmd.kitty_desktop_notification.readOption(.o));
}

test "OSC 99: o 5" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;o=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.always, cmd.kitty_desktop_notification.readOption(.o));
}

test "OSC 99: p 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=alive;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.alive, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=body;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.body, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=buttons;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.buttons, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=close;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.close, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 5" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=icon;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.icon, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 6" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=?;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.query, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 7" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=title;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.title, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 8" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=query;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.unknown, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: p 9" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;p=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.unknown, cmd.kitty_desktop_notification.readOption(.p));
}

test "OSC 99: s 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;s=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqualStrings("R2hvc3R0eQ==", cmd.kitty_desktop_notification.readOption(.s));
}

test "OSC 99: t 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;t=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    var it = cmd.kitty_desktop_notification.readOption(.t);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expect(it.next() == null);
}

test "OSC 99: t 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;t=R2hvc3R0eQ==:t=R2hvc3R0eQ==;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    var it = cmd.kitty_desktop_notification.readOption(.t);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expectEqualStrings("R2hvc3R0eQ==", it.next().?);
    try testing.expect(it.next() == null);
}

test "OSC 99: u 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;u=0;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.low, cmd.kitty_desktop_notification.readOption(.u));
}

test "OSC 99: u 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;u=1;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u));
}

test "OSC 99: u 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;u=2;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.high, cmd.kitty_desktop_notification.readOption(.u));
}

test "OSC 99: u 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;u=bobr;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(.normal, cmd.kitty_desktop_notification.readOption(.u));
}

test "OSC 99: w 1" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;w=0;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(0, cmd.kitty_desktop_notification.readOption(.w));
}

test "OSC 99: w 2" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;w=-1;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w));
}

test "OSC 99: w 3" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;w=-42;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w));
}

test "OSC 99: w 4" {
    const testing = std.testing;

    var p: Parser = .init(null);

    const input = "99;w=4294967296;foobar";
    for (input) |ch| p.next(ch);

    const cmd = p.end('\x1b').?.*;
    try testing.expect(cmd == .kitty_desktop_notification);
    try testing.expectEqualStrings("foobar", cmd.kitty_desktop_notification.payload);
    try testing.expectEqual(-1, cmd.kitty_desktop_notification.readOption(.w));
}
