//! Represents a single selection within the terminal (i.e. a highlight region).
const Selection = @This();

const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const lib = @import("lib.zig");
const page = @import("page.zig");
const point = @import("point.zig");
const PageList = @import("PageList.zig");
const Screen = @import("Screen.zig");
const Pin = PageList.Pin;

// NOTE(mitchellh): I'm not very happy with how this is implemented, because
// the ordering operations which are used frequently require using
// pointFromPin which -- at the time of writing this -- is slow. The overall
// style of this struct is due to porting it from the previous implementation
// which had an efficient ordering operation.
//
// While reimplementing this, there were too many callers that already
// depended on this behavior so I kept it despite the inefficiency. In the
// future, we should take a look at this again!

/// The bounds of the selection.
bounds: Bounds,

/// Whether or not this selection refers to a rectangle, rather than whole
/// lines of a buffer. In this mode, start and end refer to the top left and
/// bottom right of the rectangle, or vice versa if the selection is backwards.
rectangle: bool = false,

/// The bounds of the selection. A selection bounds can be either tracked
/// or untracked. Untracked bounds are unsafe beyond the point the terminal
/// screen may be modified, since they may point to invalid memory. Tracked
/// bounds are always valid and will be updated as the screen changes, but
/// are more expensive to exist.
///
/// In all cases, start and end can be in any order. There is no guarantee that
/// start is before end or vice versa. If a user selects backwards,
/// start will be after end, and vice versa. Use the struct functions
/// to not have to worry about this.
pub const Bounds = union(enum) {
    untracked: struct {
        start: Pin,
        end: Pin,
    },

    tracked: struct {
        start: *Pin,
        end: *Pin,
    },
};

/// Initialize a new selection with the given start and end pins on
/// the screen. The screen will be used for pin tracking.
pub fn init(
    start_pin: Pin,
    end_pin: Pin,
    rect: bool,
) Selection {
    return .{
        .bounds = .{ .untracked = .{
            .start = start_pin,
            .end = end_pin,
        } },
        .rectangle = rect,
    };
}

pub fn deinit(
    self: Selection,
    s: *Screen,
) void {
    switch (self.bounds) {
        .tracked => |v| {
            s.pages.untrackPin(v.start);
            s.pages.untrackPin(v.end);
        },

        .untracked => {},
    }
}

/// Returns true if this selection is equal to another selection.
pub fn eql(self: Selection, other: Selection) bool {
    return self.start().eql(other.start()) and
        self.end().eql(other.end()) and
        self.rectangle == other.rectangle;
}

/// The starting pin of the selection. This is NOT ordered.
pub fn startPtr(self: *Selection) *Pin {
    return switch (self.bounds) {
        .untracked => |*v| &v.start,
        .tracked => |v| v.start,
    };
}

/// The ending pin of the selection. This is NOT ordered.
pub fn endPtr(self: *Selection) *Pin {
    return switch (self.bounds) {
        .untracked => |*v| &v.end,
        .tracked => |v| v.end,
    };
}

pub fn start(self: Selection) Pin {
    return switch (self.bounds) {
        .untracked => |v| v.start,
        .tracked => |v| v.start.*,
    };
}

pub fn end(self: Selection) Pin {
    return switch (self.bounds) {
        .untracked => |v| v.end,
        .tracked => |v| v.end.*,
    };
}

/// Returns true if this is a tracked selection.
pub fn tracked(self: *const Selection) bool {
    return switch (self.bounds) {
        .untracked => false,
        .tracked => true,
    };
}

/// Convert this selection a tracked selection. It is asserted this is
/// an untracked selection. The tracked selection is returned.
pub fn track(self: *const Selection, s: *Screen) Allocator.Error!Selection {
    assert(!self.tracked());

    // Track our pins
    const start_pin = self.bounds.untracked.start;
    const end_pin = self.bounds.untracked.end;
    const tracked_start = try s.pages.trackPin(start_pin);
    errdefer s.pages.untrackPin(tracked_start);
    const tracked_end = try s.pages.trackPin(end_pin);
    errdefer s.pages.untrackPin(tracked_end);

    return .{
        .bounds = .{ .tracked = .{
            .start = tracked_start,
            .end = tracked_end,
        } },
        .rectangle = self.rectangle,
    };
}

/// Returns the top left point of the selection.
pub fn topLeft(self: Selection, s: *const Screen) Pin {
    return switch (self.order(s)) {
        .forward => self.start(),
        .reverse => self.end(),
        .mirrored_forward => pin: {
            var p = self.start();
            p.x = self.end().x;
            break :pin p;
        },
        .mirrored_reverse => pin: {
            var p = self.end();
            p.x = self.start().x;
            break :pin p;
        },
    };
}

/// Returns the bottom right point of the selection.
pub fn bottomRight(self: Selection, s: *const Screen) Pin {
    return switch (self.order(s)) {
        .forward => self.end(),
        .reverse => self.start(),
        .mirrored_forward => pin: {
            var p = self.end();
            p.x = self.start().x;
            break :pin p;
        },
        .mirrored_reverse => pin: {
            var p = self.start();
            p.x = self.end().x;
            break :pin p;
        },
    };
}

/// The order of the selection:
///
///  * forward: start(x, y) is before end(x, y) (top-left to bottom-right).
///  * reverse: end(x, y) is before start(x, y) (bottom-right to top-left).
///  * mirrored_[forward|reverse]: special, rectangle selections only (see below).
///
///  For regular selections, the above also holds for top-right to bottom-left
///  (forward) and bottom-left to top-right (reverse). However, for rectangle
///  selections, both of these selections are *mirrored* as orientation
///  operations only flip the x or y axis, not both. Depending on the y axis
///  direction, this is either mirrored_forward or mirrored_reverse.
///
pub const Order = lib.Enum(lib.target, &.{
    "forward",
    "reverse",
    "mirrored_forward",
    "mirrored_reverse",
});

pub fn order(self: Selection, s: *const Screen) Order {
    const start_pt = s.pages.pointFromPin(.screen, self.start()).?.screen;
    const end_pt = s.pages.pointFromPin(.screen, self.end()).?.screen;

    if (self.rectangle) {
        // Reverse (also handles single-column)
        if (start_pt.y > end_pt.y and start_pt.x >= end_pt.x) return .reverse;
        if (start_pt.y >= end_pt.y and start_pt.x > end_pt.x) return .reverse;

        // Mirror, bottom-left to top-right
        if (start_pt.y > end_pt.y and start_pt.x < end_pt.x) return .mirrored_reverse;

        // Mirror, top-right to bottom-left
        if (start_pt.y < end_pt.y and start_pt.x > end_pt.x) return .mirrored_forward;

        // Forward
        return .forward;
    }

    if (start_pt.y < end_pt.y) return .forward;
    if (start_pt.y > end_pt.y) return .reverse;
    if (start_pt.x <= end_pt.x) return .forward;
    return .reverse;
}

/// Returns the selection in the given order.
///
/// The returned selection is always a new untracked selection.
///
/// Note that only forward and reverse are useful desired orders for this
/// function. All other orders act as if forward order was desired.
pub fn ordered(self: Selection, s: *const Screen, desired: Order) Selection {
    if (self.order(s) == desired) return .init(
        self.start(),
        self.end(),
        self.rectangle,
    );

    const tl = self.topLeft(s);
    const br = self.bottomRight(s);
    return switch (desired) {
        .forward => .init(tl, br, self.rectangle),
        .reverse => .init(br, tl, self.rectangle),
        else => .init(tl, br, self.rectangle),
    };
}

/// Returns true if the selection contains the given point.
///
/// This recalculates top left and bottom right each call. If you have
/// many points to check, it is cheaper to do the containment logic
/// yourself and cache the topleft/bottomright.
pub fn contains(self: Selection, s: *const Screen, pin: Pin) bool {
    const tl_pin = self.topLeft(s);
    const br_pin = self.bottomRight(s);

    // This is definitely not very efficient. Low-hanging fruit to
    // improve this.
    const tl = s.pages.pointFromPin(.screen, tl_pin).?.screen;
    const br = s.pages.pointFromPin(.screen, br_pin).?.screen;
    const p = s.pages.pointFromPin(.screen, pin).?.screen;

    // If we're in rectangle select, we can short-circuit with an easy check
    // here
    if (self.rectangle)
        return p.y >= tl.y and p.y <= br.y and p.x >= tl.x and p.x <= br.x;

    // If tl/br are same line
    if (tl.y == br.y) return p.y == tl.y and
        p.x >= tl.x and
        p.x <= br.x;

    // If on top line, just has to be left of X
    if (p.y == tl.y) return p.x >= tl.x;

    // If on bottom line, just has to be right of X
    if (p.y == br.y) return p.x <= br.x;

    // If between the top/bottom, always good.
    return p.y > tl.y and p.y < br.y;
}

/// Get a selection for a single row in the screen. This will return null
/// if the row is not included in the selection.
///
/// This is a very expensive operation. It has to traverse the linked list
/// of pages for the top-left, bottom-right, and the given pin to find
/// the coordinates. If you are calling this repeatedly, prefer
/// `containedRowCached`.
pub fn containedRow(self: Selection, s: *const Screen, pin: Pin) ?Selection {
    const tl_pin = self.topLeft(s);
    const br_pin = self.bottomRight(s);

    // This is definitely not very efficient. Low-hanging fruit to
    // improve this. Callers should prefer containedRowCached if they
    // can swing it.
    const tl = s.pages.pointFromPin(.screen, tl_pin).?.screen;
    const br = s.pages.pointFromPin(.screen, br_pin).?.screen;
    const p = s.pages.pointFromPin(.screen, pin).?.screen;

    return self.containedRowCached(
        s,
        tl_pin,
        br_pin,
        pin,
        tl,
        br,
        p,
    );
}

/// Same as containedRow but useful if you're calling it repeatedly
/// so that the pins can be cached across calls. Advanced.
pub fn containedRowCached(
    self: Selection,
    s: *const Screen,
    tl_pin: Pin,
    br_pin: Pin,
    pin: Pin,
    tl: point.Coordinate,
    br: point.Coordinate,
    p: point.Coordinate,
) ?Selection {
    if (p.y < tl.y or p.y > br.y) return null;

    // Rectangle case: we can return early as the x range will always be the
    // same. We've already validated that the row is in the selection.
    if (self.rectangle) return init(
        start: {
            var copy: Pin = pin;
            copy.x = tl.x;
            break :start copy;
        },
        end: {
            var copy: Pin = pin;
            copy.x = br.x;
            break :end copy;
        },
        true,
    );

    if (p.y == tl.y) {
        // If the selection is JUST this line, return it as-is.
        if (p.y == br.y) {
            return init(tl_pin, br_pin, false);
        }

        // Selection top-left line matches only.
        return init(
            tl_pin,
            end: {
                var copy: Pin = pin;
                copy.x = s.pages.cols - 1;
                break :end copy;
            },
            false,
        );
    }

    // Row is our bottom selection, so we return the selection from the
    // beginning of the line to the br. We know our selection is more than
    // one line (due to conditionals above)
    if (p.y == br.y) {
        assert(p.y != tl.y);
        return init(
            start: {
                var copy: Pin = pin;
                copy.x = 0;
                break :start copy;
            },
            br_pin,
            false,
        );
    }

    // Row is somewhere between our selection lines so we return the full line.
    return init(
        start: {
            var copy: Pin = pin;
            copy.x = 0;
            break :start copy;
        },
        end: {
            var copy: Pin = pin;
            copy.x = s.pages.cols - 1;
            break :end copy;
        },
        false,
    );
}

/// Possible adjustments to the selection.
pub const Adjustment = lib.Enum(lib.target, &.{
    "left",
    "right",
    "up",
    "down",
    "home",
    "end",
    "page_up",
    "page_down",
    "beginning_of_line",
    "end_of_line",
});

/// Adjust the selection by some given adjustment. An adjustment allows
/// a selection to be expanded slightly left, right, up, down, etc.
pub fn adjust(
    self: *Selection,
    s: *const Screen,
    adjustment: Adjustment,
) void {
    // Note that we always adjust "end" because end always represents
    // the last point of the selection by mouse, not necessarily the
    // top/bottom visually. So this results in the correct behavior
    // whether the user drags up or down.
    const end_pin = self.endPtr();
    switch (adjustment) {
        .up => if (end_pin.up(1)) |new_end| {
            end_pin.* = new_end;
        } else {
            self.adjust(s, .beginning_of_line);
        },

        .down => {
            // Find the next non-blank row
            var current = end_pin.*;
            while (current.down(1)) |next| : (current = next) {
                const rac = next.rowAndCell();
                const cells = next.node.data.getCells(rac.row);
                if (page.Cell.hasTextAny(cells)) {
                    end_pin.* = next;
                    break;
                }
            } else {
                // If we're at the bottom, just go to the end of the line
                self.adjust(s, .end_of_line);
            }
        },

        .left => {
            var it = end_pin.cellIterator(.left_up, null);
            _ = it.next();
            while (it.next()) |next| {
                const rac = next.rowAndCell();
                if (rac.cell.hasText()) {
                    end_pin.* = next;
                    break;
                }
            }
        },

        .right => {
            // Step right, wrapping to the next row down at the start of each new line,
            // until we find a non-empty cell.
            var it = end_pin.cellIterator(.right_down, null);
            _ = it.next();
            while (it.next()) |next| {
                const rac = next.rowAndCell();
                if (rac.cell.hasText()) {
                    end_pin.* = next;
                    break;
                }
            }
        },

        .page_up => if (end_pin.up(s.pages.rows)) |new_end| {
            end_pin.* = new_end;
        } else {
            self.adjust(s, .home);
        },

        .page_down => if (end_pin.down(s.pages.rows)) |new_end| {
            end_pin.* = new_end;
        } else {
            self.adjust(s, .end);
        },

        .home => end_pin.* = s.pages.pin(.{ .screen = .{
            .x = 0,
            .y = 0,
        } }).?,

        .end => {
            var it = s.pages.rowIterator(
                .left_up,
                .{ .screen = .{} },
                null,
            );
            while (it.next()) |next| {
                const rac = next.rowAndCell();
                const cells = next.node.data.getCells(rac.row);
                if (page.Cell.hasTextAny(cells)) {
                    end_pin.* = next;
                    end_pin.x = @intCast(cells.len - 1);
                    break;
                }
            }
        },

        .beginning_of_line => end_pin.x = 0,

        .end_of_line => end_pin.x = end_pin.node.data.size.cols - 1,
    }
}

test "Selection: adjust right" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A1234\nB5678\nC1234\nD5678");

    // Simple movement right
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .right);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Already at end of the line.
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 2 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .right);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Already at end of the screen
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .right);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust left" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A1234\nB5678\nC1234\nD5678");

    // Simple movement left
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .left);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 2,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Already at beginning of the line.
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .left);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 2,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust left skips blanks" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A1234\nB5678\nC12\nD56");

    // Same line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .left);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 2,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Edge
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .left);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 2,
            .y = 2,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust up" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A\nB\nC\nD\nE");

    // Not on the first line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .up);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 2,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // On the first line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .up);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust down" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A\nB\nC\nD\nE");

    // Not on the first line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .down);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 4,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // On the last line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 4 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .down);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 9,
            .y = 4,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust down with not full screen" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A\nB\nC");

    // On the last line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .down);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 9,
            .y = 2,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust home" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A\nB\nC");

    // On the last line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .home);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust end with not full screen" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A\nB\nC");

    // On the last line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 4, .y = 0 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .end);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 4,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 9,
            .y = 2,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust beginning of line" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 8, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A12 B34\nC12 D34");

    // Not at beginning of the line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .beginning_of_line);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Already at beginning of the line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .beginning_of_line);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 5,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // End pin moves to start pin
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .beginning_of_line);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 0,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: adjust end of line" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 8, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    try s.testWriteString("A12 B34\nC12 D34");

    // Not at end of the line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .end_of_line);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 7,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // Already at end of the line
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 7, .y = 0 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .end_of_line);

        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 7,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }

    // End pin moves to start pin
    {
        var sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 7, .y = 0 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
            false,
        );
        defer sel.deinit(&s);
        sel.adjust(&s, .end_of_line);

        // Start line
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 7,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.start()).?);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 7,
            .y = 0,
        } }, s.pages.pointFromPin(.screen, sel.end()).?);
    }
}

test "Selection: order, standard" {
    const testing = std.testing;
    const alloc = testing.allocator;

    var s = try Screen.init(alloc, .{ .cols = 100, .rows = 100, .max_scrollback = 1 });
    defer s.deinit();

    {
        // forward, multi-line
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
            false,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // reverse, multi-line
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .reverse);
    }
    {
        // forward, same-line
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // forward, single char
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // reverse, single line
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .reverse);
    }
}

test "Selection: order, rectangle" {
    const testing = std.testing;
    const alloc = testing.allocator;

    var s = try Screen.init(alloc, .{ .cols = 100, .rows = 100, .max_scrollback = 1 });
    defer s.deinit();

    // Conventions:
    // TL - top left
    // BL - bottom left
    // TR - top right
    // BR - bottom right
    {
        // forward (TL -> BR)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // reverse (BR -> TL)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .reverse);
    }
    {
        // mirrored_forward (TR -> BL)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .mirrored_forward);
    }
    {
        // mirrored_reverse (BL -> TR)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .mirrored_reverse);
    }
    {
        // forward, single line (left -> right )
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // reverse, single line (right -> left)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .reverse);
    }
    {
        // forward, single column (top -> bottom)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
    {
        // reverse, single column (bottom -> top)
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .reverse);
    }
    {
        // forward, single cell
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);

        try testing.expect(sel.order(&s) == .forward);
    }
}

test "topLeft" {
    const testing = std.testing;

    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    {
        // forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const tl = sel.topLeft(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, tl));
    }
    {
        // reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const tl = sel.topLeft(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, tl));
    }
    {
        // mirrored_forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const tl = sel.topLeft(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, tl));
    }
    {
        // mirrored_reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const tl = sel.topLeft(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 1,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, tl));
    }
}

test "bottomRight" {
    const testing = std.testing;

    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    {
        // forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        const br = sel.bottomRight(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, br));
    }
    {
        // reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            false,
        );
        defer sel.deinit(&s);
        const br = sel.bottomRight(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 1,
        } }, s.pages.pointFromPin(.screen, br));
    }
    {
        // mirrored_forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const br = sel.bottomRight(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, br));
    }
    {
        // mirrored_reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        defer sel.deinit(&s);
        const br = sel.bottomRight(&s);
        try testing.expectEqual(point.Point{ .screen = .{
            .x = 3,
            .y = 3,
        } }, s.pages.pointFromPin(.screen, br));
    }
}

test "ordered" {
    const testing = std.testing;

    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    {
        // forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            false,
        );
        const sel_reverse = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            false,
        );
        try testing.expect(sel.ordered(&s, .forward).eql(sel));
        try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
        try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel));
    }
    {
        // reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            false,
        );
        const sel_forward = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            false,
        );
        try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
        try testing.expect(sel.ordered(&s, .reverse).eql(sel));
        try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel_forward));
    }
    {
        // mirrored_forward
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            true,
        );
        const sel_forward = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            true,
        );
        const sel_reverse = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
        try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
        try testing.expect(sel.ordered(&s, .mirrored_reverse).eql(sel_forward));
    }
    {
        // mirrored_reverse
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            true,
        );
        const sel_forward = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            true,
        );
        const sel_reverse = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
            true,
        );
        try testing.expect(sel.ordered(&s, .forward).eql(sel_forward));
        try testing.expect(sel.ordered(&s, .reverse).eql(sel_reverse));
        try testing.expect(sel.ordered(&s, .mirrored_forward).eql(sel_forward));
    }
}

test "Selection: contains" {
    const testing = std.testing;

    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 10, .max_scrollback = 0 });
    defer s.deinit();
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
            false,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?));
    }

    // Reverse
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            false,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?));
    }

    // Single line
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 8, .y = 1 } }).?,
            false,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 9, .y = 1 } }).?));
    }
}

test "Selection: contains, rectangle" {
    const testing = std.testing;

    var s = try Screen.init(testing.allocator, .{ .cols = 15, .rows = 15, .max_scrollback = 0 });
    defer s.deinit();
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 7, .y = 9 } }).?,
            true,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 6 } }).?)); // Center
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 3, .y = 6 } }).?)); // Left border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 7, .y = 6 } }).?)); // Right border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 3 } }).?)); // Top border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 9 } }).?)); // Bottom border

        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?)); // Above center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 10 } }).?)); // Below center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?)); // Left center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 6 } }).?)); // Right center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 3 } }).?)); // Just right of top right
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 9 } }).?)); // Just left of bottom left
    }

    // Reverse
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 7, .y = 9 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            true,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 6 } }).?)); // Center
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 3, .y = 6 } }).?)); // Left border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 7, .y = 6 } }).?)); // Right border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 3 } }).?)); // Top border
        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 9 } }).?)); // Bottom border

        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 2 } }).?)); // Above center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 5, .y = 10 } }).?)); // Below center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 6 } }).?)); // Left center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 6 } }).?)); // Right center
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 8, .y = 3 } }).?)); // Just right of top right
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 9 } }).?)); // Just left of bottom left
    }

    // Single line
    // NOTE: This is the same as normal selection but we just do it for brevity
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 10, .y = 1 } }).?,
            true,
        );

        try testing.expect(sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?));
        try testing.expect(!sel.contains(&s, s.pages.pin(.{ .screen = .{ .x = 12, .y = 1 } }).?));
    }
}

test "Selection: containedRow" {
    const testing = std.testing;
    var s = try Screen.init(testing.allocator, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
    defer s.deinit();

    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 5, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            false,
        );

        // Not contained
        try testing.expect(sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 4 } }).?,
        ) == null);

        // Start line
        try testing.expectEqual(Selection.init(
            sel.start(),
            s.pages.pin(.{ .screen = .{ .x = s.pages.cols - 1, .y = 1 } }).?,
            false,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
        ).?);

        // End line
        try testing.expectEqual(Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 3 } }).?,
            sel.end(),
            false,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
        ).?);

        // Middle line
        try testing.expectEqual(Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 0, .y = 2 } }).?,
            s.pages.pin(.{ .screen = .{ .x = s.pages.cols - 1, .y = 2 } }).?,
            false,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
        ).?);
    }

    // Rectangle
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 6, .y = 3 } }).?,
            true,
        );

        // Not contained
        try testing.expect(sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 4 } }).?,
        ) == null);

        // Start line
        try testing.expectEqual(Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?,
            true,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
        ).?);

        // End line
        try testing.expectEqual(Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 3 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 6, .y = 3 } }).?,
            true,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 3 } }).?,
        ).?);

        // Middle line
        try testing.expectEqual(Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 3, .y = 2 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 6, .y = 2 } }).?,
            true,
        ), sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 2 } }).?,
        ).?);
    }

    // Single-line selection
    {
        const sel = Selection.init(
            s.pages.pin(.{ .screen = .{ .x = 2, .y = 1 } }).?,
            s.pages.pin(.{ .screen = .{ .x = 6, .y = 1 } }).?,
            false,
        );

        // Not contained
        try testing.expect(sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?,
        ) == null);
        try testing.expect(sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 2 } }).?,
        ) == null);

        // Contained
        try testing.expectEqual(sel, sel.containedRow(
            &s,
            s.pages.pin(.{ .screen = .{ .x = 1, .y = 1 } }).?,
        ).?);
    }
}
