import CoreGraphics
import Foundation
import Observation
import Testing
@testable import Kanban

/// The keyboard grammar's pure halves — 04-interactions.md ▸ Grammar's spatial navigation
/// (`NavigationMath`), ▸ The map's within-lane sort (`SortMath`) and its successor-on-delete rule
/// (`SelectionGrammar.successor`), plus the navigation head the arrows step from
/// (`TransientBoardState.selectionHead`).
///
/// The arrow *handlers* are deliberately absent: they are dispatch over these functions and a
/// registry of drawn frames, so everything with a rule in it is here and the views hold nothing that
/// could be asserted without a window.
///
/// The board-level suites drive a **real `BoardStore` over a real temp tree** and read the result
/// back off disk, the write suites' rule — a sort is only correct if the bytes say so.
/// `WriterFixture`, `Ident` and `Item` live in `WriterTestSupport.swift`.

// MARK: - Identities

/// Two more card identities than `Ident` offers: the successor rule needs a *second* multi-card lane
/// to prove it reads the last selected member's lane rather than the first's.
private enum More {
    static let card5 = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
    static let card6 = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
    /// A card an agent files mid-navigation — a reload that changes the board without disturbing the
    /// cursor, which the sticky ordinal's survival rule needs.
    static let filed = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
}

private let lane1 = ItemID(rawValue: Ident.lane1)
private let lane2 = ItemID(rawValue: Ident.lane2)
private let lane3 = ItemID(rawValue: Ident.lane3)
private let card1 = ItemID(rawValue: Ident.card1)
private let card2 = ItemID(rawValue: Ident.card2)
private let card3 = ItemID(rawValue: Ident.card3)
private let card4 = ItemID(rawValue: Ident.card4)
private let card5 = ItemID(rawValue: More.card5)
private let card6 = ItemID(rawValue: More.card6)

/// Four cards in one lane, two in the next, and an empty third — the shapes every rule below needs:
/// a block with room on both sides, a second container to be redirected into, and a lane that
/// contributes nothing to card navigation.
@MainActor
private func makeBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.item("", Item.board)
    try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
    try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
    try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
    try fixture.item("\(Ident.lane1)/\(Ident.card3)", Item.rich(order: "3072", title: "Third"))
    try fixture.item("\(Ident.lane1)/\(Ident.card4)", Item.rich(order: "4096", title: "Fourth"))
    try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
    try fixture.item("\(Ident.lane2)/\(More.card5)", Item.rich(order: "1024", title: "Fifth"))
    try fixture.item("\(Ident.lane2)/\(More.card6)", Item.rich(order: "2048", title: "Sixth"))
    try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Done"))
    return fixture
}

private func load(_ fixture: WriterFixture) throws -> BoardModel {
    try BoardLoader.load(boardRoot: fixture.root).model
}

/// The ids a lane renders, top to bottom, as they are **on disk right now**.
private func cardOrder(_ laneID: String, in fixture: WriterFixture) throws -> [ItemID] {
    let model = try load(fixture)
    let lane = try #require(model.lanes.first { $0.id.rawValue == laneID })
    return lane.cards.filter { !$0.isDeleted }.map(\.id)
}

// MARK: - Frames

/// A drawn frame, with the two axes the score reads spelled out at the call site.
private func target(
    _ id: ItemID,
    x: CGFloat,
    y: CGFloat,
    width: CGFloat = 100,
    height: CGFloat = 100,
    kind: SelectionKind = .card,
    container: ItemContainer = .board
) -> MarqueeTarget {
    MarqueeTarget(id: id, kind: kind, container: container, frame: CGRect(x: x, y: y, width: width, height: height))
}

/// A two-by-two grid: `card1` `card3` on the top row, `card2` `card4` beneath them — the smallest
/// board shape with an interior column *and* a lane boundary to cross.
private let grid: [MarqueeTarget] = [
    target(card1, x: 0, y: 0),
    target(card2, x: 0, y: 120),
    target(card3, x: 120, y: 0),
    target(card4, x: 120, y: 120)
]

private let originFrame = CGRect(x: 0, y: 0, width: 100, height: 100)

// MARK: - NavigationMath

@Suite("NavigationMath ▸ nearest in the direction")
struct NavigationMathTests {

    @Test("Each direction picks its own neighbour")
    func fourDirections() {
        #expect(NavigationMath.nearest(from: grid[0].frame, direction: .down, among: grid) == card2)
        #expect(NavigationMath.nearest(from: grid[1].frame, direction: .up, among: grid) == card1)
        #expect(NavigationMath.nearest(from: grid[0].frame, direction: .right, among: grid) == card3)
        #expect(NavigationMath.nearest(from: grid[2].frame, direction: .left, among: grid) == card1)
    }

    @Test("A card straight ahead beats a nearer one off to the side — orthogonal drift costs double")
    func orthogonalDriftIsPenalised() {
        // Straight down at 100pt of primary distance (score 100) versus 40pt down but 200pt across
        // (score 40 + 400). Without the penalty the second would win and ↓ would wander out of the
        // column instead of walking it (04-interactions.md ▸ Grammar).
        let straight = target(card2, x: 0, y: 100)
        let sideways = target(card3, x: 400, y: 40)
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [straight, sideways]) == card2)
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [sideways, straight]) == card2)
    }

    @Test("A tie is broken by position, then identity — and the input order never decides")
    func tiesAreDeterministic() {
        // Both score 50 + 2 × 50: same primary distance, same drift, opposite sides.
        let right = target(card2, x: 50, y: 50)
        let left = target(card3, x: -50, y: 50)
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [right, left]) == card3)
        #expect(
            NavigationMath.nearest(from: originFrame, direction: .down, among: [left, right]) == card3,
            "reversing the candidate list must not change the answer"
        )

        // Same frame twice: position cannot separate them, so identity does.
        let low = target(ItemID(rawValue: "aaaa"), x: 0, y: 200)
        let high = target(ItemID(rawValue: "zzzz"), x: 0, y: 200)
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [high, low])?.rawValue == "aaaa")
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [low, high])?.rawValue == "aaaa")
    }

    @Test("Nothing beyond the origin in that direction is nil, and the origin never picks itself")
    func noCandidate() {
        #expect(NavigationMath.nearest(from: grid[0].frame, direction: .up, among: grid) == nil)
        #expect(NavigationMath.nearest(from: grid[0].frame, direction: .left, among: grid) == nil)
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: []) == nil)
        // A candidate level with the origin is not beyond it: the 1pt threshold excludes the origin
        // itself and its exact row-mates.
        #expect(NavigationMath.nearest(from: originFrame, direction: .down, among: [target(card2, x: 300, y: 0)]) == nil)
    }

    @Test("The predicate is the ⇧-arrow's restriction — the trash side is simply not a candidate")
    func predicateRestrictsCandidates() {
        let trashed = target(card2, x: 0, y: 100, container: .trash)
        let live = target(card3, x: 0, y: 400)
        let all = [trashed, live]

        #expect(
            NavigationMath.nearest(from: originFrame, direction: .down, among: all) == card2,
            "a plain arrow walks across the boundary"
        )
        #expect(
            NavigationMath.nearest(from: originFrame, direction: .down, among: all, where: { $0.container == .board }) == card3
        )
    }

    /// **Inside the trash, plain arrows walk every row and extension stops at the kind boundary**
    /// (04-interactions.md ▸ The trash: "plain arrows walk across, extension stops"), which is the two
    /// halves of the arrow handler expressed over one registry of drawn frames — a lane row registers
    /// like a card face precisely so ↓ can reach it.
    @Test("A trash lane row is a plain arrow's neighbour, and an extension's dead end")
    func trashLaneRowsAreNavigableButNotExtendable() {
        let origin = target(card1, x: 0, y: 0, container: .trash)
        let row = target(lane2, x: 0, y: 120, kind: .lane, container: .trash)
        let below = target(card2, x: 0, y: 240, container: .trash)
        let all = [origin, row, below]

        // Plain: the next row down, whatever its kind.
        #expect(NavigationMath.nearest(from: origin.frame, direction: .down, among: all) == lane2)

        // ⇧: the handler takes the *same* unrestricted neighbour and then tests it, so a crossing
        // row makes the press inert rather than being stepped over in search of a legal one — the
        // rule exists so a held range is never silently widened past what the user asked for.
        let next = NavigationMath.nearest(from: origin.frame, direction: .down, among: all)
        #expect(next == lane2)
        #expect(row.kind != origin.kind, "so the extension stops here")

        // From the row itself, ↓ reaches the card below it: navigation crosses back.
        #expect(NavigationMath.nearest(from: row.frame, direction: .down, among: all) == card2)
    }

    /// **A collapsed lane is scanned past by the absolute destinations** (03-board-ui.md § Lane ▸
    /// Collapsed lanes: "cards inside are not rendered — they are excluded from … keyboard spatial
    /// navigation"), which is what ⌥←/⌥→ and the empty-selection seed land through.
    ///
    /// The *relative* half — a plain or ⇧ arrow — needs nothing: a folded lane registers no card frame,
    /// so `nearest` has no candidate to reject, exactly as the hidden trash has none.
    /// `@MainActor` where its neighbours are not, and only because the fixture is: a board on disk is
    /// the honest way to ask what a *read-side* fold reads as, and `WriterFixture` is main-actor bound.
    @MainActor
    @Test("The absolute destinations scan past a folded lane, an empty one, and one the query emptied")
    func firstCardSkipsFoldedLanes() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        // Lane 1 folded, lane 2 open: the first card the *board* is showing is lane 2's.
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Todo\norder: 1024\ncollapsed: true\n---\n\n")
        let lanes = try load(fixture).lanes

        #expect(NavigationMath.firstCard(scanning: lanes) == card5)
        // Reversed — ⌥→'s scan — the last showing lane is lane 2 as well, since lane 3 is empty and
        // lane 1 is folded.
        #expect(NavigationMath.firstCard(scanning: lanes.reversed()) == card5)

        // The filter narrows the same scan: "Sixth" is in the open lane, "First" is in the folded one
        // and stays unreachable however well it matches.
        #expect(NavigationMath.firstCard(scanning: lanes, filter: SearchFilter(query: "Sixth")) == card6)
        #expect(NavigationMath.firstCard(scanning: lanes, filter: SearchFilter(query: "First")) == nil)

        // Every lane folded: there is nowhere to land at all, which is the same answer an empty board
        // gives and leaves the press inert rather than selecting something drawn nowhere.
        let allFolded = try WriterFixture()
        defer { allFolded.tearDown() }
        try allFolded.item("", Item.board)
        try allFolded.item(Ident.lane1, "---\nschema: 1\ntitle: Todo\norder: 1024\ncollapsed: true\n---\n\n")
        try allFolded.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
        #expect(NavigationMath.firstCard(scanning: try load(allFolded).lanes) == nil)

        // And unfolding restores it, key removal and all — nothing about the cards changed.
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Todo\norder: 1024\n---\n\n")
        #expect(NavigationMath.firstCard(scanning: try load(fixture).lanes) == card1)
    }
}

// MARK: - The sticky ordinal

/// A UUID-shaped folder name for the walk board — `(lane, position)`, with position `0` naming the
/// lane itself, so ten cards in a lane cost no ten literals.
private func name(_ lane: Int, _ position: Int) -> String {
    String(format: "%08x-0000-4000-8000-%012x", lane, position)
}

private func walkID(_ lane: Int, _ position: Int) -> ItemID {
    ItemID(rawValue: name(lane, position))
}

/// **The card's own board**: ten cards, three, ten. The middle lane is what a walk has to be clamped
/// by, and the third is what proves the clamp was not remembered.
///
/// The third lane is **two units wide**, so its cards lay out across interior masonry columns — the
/// case the ruling settles by saying interior position "does not participate". Nothing here can see
/// that: the hop takes no frames, which is the enforcement.
///
/// Every card's body carries two tokens the filter suites query for: `even`/`odd` by position, and
/// `keeplane` on the outer lanes only — so one query narrows all three lanes and another empties the
/// middle one entirely.
@MainActor
private func makeWalk() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.board()
    for (lane, count) in [(1, 10), (2, 3), (3, 10)] {
        try fixture.lane(
            name(lane, 0),
            order: "\(lane * 1024)",
            title: "Lane \(lane)",
            width: lane == 3 ? 2 : nil
        )
        for position in 1...count {
            try fixture.card(
                name(lane, position),
                in: name(lane, 0),
                order: "\(position * 1024)",
                title: "L\(lane)C\(position)",
                body: "\(position.isMultiple(of: 2) ? "even" : "odd") \(lane == 2 ? "" : "keeplane")"
            )
        }
    }
    return fixture
}

@Suite("NavigationMath ▸ the sticky ordinal")
@MainActor
struct LateralHopTests {

    @Test("10-3-10: the 8th card walks out through the 3rd and back to the 8th")
    func roundTrip() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes

        // Out: clamped to the short lane's last card, and still carrying the 8 it started with —
        // the clamp is a landing, never a new position to remember.
        let out = try #require(NavigationMath.lateralHop(from: walkID(1, 8), .right, scanning: lanes))
        #expect(out.target == walkID(2, 3))
        #expect(out.ordinal == 8)

        // Onward: the wide lane's 8th in *logical* order, whichever masonry column drew it.
        let onward = try #require(
            NavigationMath.lateralHop(from: out.target, .right, scanning: lanes, sticky: out.ordinal)
        )
        #expect(onward.target == walkID(3, 8))
        #expect(onward.ordinal == 8)

        // And back the way it came, which is the card's title in full.
        let returning = try #require(
            NavigationMath.lateralHop(from: onward.target, .left, scanning: lanes, sticky: onward.ordinal)
        )
        #expect(returning.target == walkID(2, 3))
        let home = try #require(
            NavigationMath.lateralHop(from: returning.target, .left, scanning: lanes, sticky: returning.ordinal)
        )
        #expect(home.target == walkID(1, 8), "the 8th again — the whole point")
    }

    @Test("Without a run in flight the hop starts one at the origin's own position")
    func noStickyStartsARun() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes

        // This is what a *reset* leaves behind, and it is also the failure the sticky exists to fix:
        // starting a fresh run from the clamped 3rd card lands on the third, not the eighth.
        let fresh = try #require(NavigationMath.lateralHop(from: walkID(2, 3), .right, scanning: lanes))
        #expect(fresh.target == walkID(3, 3))
        #expect(fresh.ordinal == 3)

        let first = try #require(NavigationMath.lateralHop(from: walkID(1, 4), .right, scanning: lanes))
        #expect(first.ordinal == 4, "the origin's 1-based position in its lane")
    }

    @Test("The clamp is the target lane's count, and a lane of one takes everything")
    func clampsToTheShortLane() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes

        for ordinal in 3...10 {
            let hop = try #require(
                NavigationMath.lateralHop(from: walkID(1, ordinal), .right, scanning: lanes, sticky: ordinal)
            )
            #expect(hop.target == walkID(2, 3), "everything past the 3rd lands on the 3rd")
            #expect(hop.ordinal == ordinal)
        }
        // Under the *filter* the middle lane shows one card, and the clamp follows what is showing
        // rather than what is on disk.
        let narrowed = try #require(
            NavigationMath.lateralHop(
                from: walkID(1, 8), .right, scanning: lanes, filter: SearchFilter(query: "even"), sticky: 4
            )
        )
        #expect(narrowed.target == walkID(2, 2))
    }

    @Test("The ordinal counts the cards the query left showing, not the cards on disk")
    func countsTheFilteredLane() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes
        let evens = SearchFilter(query: "even")

        // Lane 1 shows 2,4,6,8,10 — so card 8 is the *4th* thing the user can see, and starting a run
        // there carries 4 rather than 8.
        let out = try #require(
            NavigationMath.lateralHop(from: walkID(1, 8), .right, scanning: lanes, filter: evens)
        )
        #expect(out.ordinal == 4)
        #expect(out.target == walkID(2, 2), "the middle lane shows one card; 4 clamps onto it")

        let onward = try #require(
            NavigationMath.lateralHop(
                from: out.target, .right, scanning: lanes, filter: evens, sticky: out.ordinal
            )
        )
        #expect(onward.target == walkID(3, 8), "the 4th showing card of the far lane is card 8")
    }

    @Test("A lane the query emptied is hopped straight over, exactly as an absent one would be")
    func skipsAQueryEmptiedLane() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes
        // `keeplane` is on lanes 1 and 3 only: the middle lane shows nothing, so it is not a
        // destination and one press crosses it.
        let hop = try #require(
            NavigationMath.lateralHop(
                from: walkID(1, 8), .right, scanning: lanes, filter: SearchFilter(query: "keeplane")
            )
        )
        #expect(hop.target == walkID(3, 8))
        #expect(hop.ordinal == 8)
    }

    @Test("A collapsed lane is hopped over too, and its cards are never a landing")
    func skipsACollapsedLane() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        // 03-board-ui.md § Lane ▸ Collapsed lanes: the slim strip lays out no card faces, so a hop
        // that landed in there would select something drawn nowhere.
        try fixture.item(name(2, 0), "---\nschema: 1\ntitle: Lane 2\norder: 2048\ncollapsed: true\n---\n\n")
        let lanes = try fixture.snapshot().lanes

        let hop = try #require(NavigationMath.lateralHop(from: walkID(1, 8), .right, scanning: lanes))
        #expect(hop.target == walkID(3, 8), "one press, straight past the strip")
        #expect(hop.ordinal == 8)

        // And back: the strip is no more a destination from the right than from the left.
        let back = try #require(
            NavigationMath.lateralHop(from: hop.target, .left, scanning: lanes, sticky: hop.ordinal)
        )
        #expect(back.target == walkID(1, 8))

        // Folding the *last* lane leaves nothing to the right at all — the handler's cue to fall back
        // to geometry, which is how the shown trash stays reachable by →.
        try fixture.item(name(3, 0), "---\nschema: 1\ntitle: Lane 3\norder: 3072\ncollapsed: true\n---\n\n")
        #expect(
            NavigationMath.lateralHop(from: walkID(1, 8), .right, scanning: try fixture.snapshot().lanes) == nil
        )
    }

    @Test("The ends, the vertical directions and an origin the board is not showing are all nil")
    func refusals() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let lanes = try fixture.snapshot().lanes

        #expect(NavigationMath.lateralHop(from: walkID(1, 1), .left, scanning: lanes) == nil)
        #expect(NavigationMath.lateralHop(from: walkID(3, 1), .right, scanning: lanes) == nil)
        #expect(
            NavigationMath.lateralHop(from: walkID(1, 8), .down, scanning: lanes) == nil,
            "↑/↓ are not a lateral run and must never consume the ordinal"
        )
        #expect(NavigationMath.lateralHop(from: walkID(1, 8), .up, scanning: lanes) == nil)
        #expect(NavigationMath.lateralHop(from: card1, .right, scanning: lanes) == nil, "not on this board")
        #expect(
            NavigationMath.lateralHop(
                from: walkID(1, 1), .right, scanning: lanes, filter: SearchFilter(query: "even")
            ) == nil,
            "an origin the query hid is in no showing lane, so there is no position to carry"
        )
    }
}

// MARK: - The sticky ordinal's resets

@Suite("TransientBoardState ▸ the lateral run's resets")
@MainActor
struct LateralOrdinalResetTests {

    @Test("Only a lateral hop stores one — every other selection change is a reset")
    func selectResets() {
        let state = TransientBoardState()

        state.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        #expect(state.lateralOrdinal == 8)

        // Verbatim `BoardView.replaceSelection`, which is what ↑/↓, the ⌥-jumps and the lane domain
        // all land through — no ordinal passed, so the run ends.
        state.select([card2], in: .board, anchor: card2, head: card2)
        #expect(state.lateralOrdinal == nil, "a vertical step ends the run")

        state.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        state.select([card1, card2], in: .board, anchor: card1, head: card2)
        #expect(state.lateralOrdinal == nil, "so does a ⇧-arrow's extension")

        state.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        state.select([card1, card2], in: .board, anchor: nil, head: nil, defaultsSoleMember: false)
        #expect(state.lateralOrdinal == nil, "and so does a rubber band")

        state.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        state.clearSelection()
        #expect(state.lateralOrdinal == nil, "Escape leaves no run to be in the middle of")
    }

    @Test("A click ends the run, whatever it selects")
    func clickResets() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        store.click(SelectionTarget(id: card3, kind: .card, container: .board), modifier: .plain)
        #expect(store.selection.ids == [card3])
        #expect(store.transient.lateralOrdinal == nil)

        // A ⌘-click's toggle and a ⇧-click's range go through the same door, so they reset too.
        store.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        store.click(SelectionTarget(id: card3, kind: .card, container: .board), modifier: .shift)
        #expect(store.transient.lateralOrdinal == nil)
    }

    @Test("A ⌫ successor ends it, because a run's cursor was just deleted out from under it")
    func deleteResets() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card2], in: .board, anchor: card2, head: card2, lateralOrdinal: 8)
        store.deleteSelection()
        #expect(store.selection.ids == [card3])
        #expect(store.transient.lateralOrdinal == nil)
    }

    @Test("A reload keeps the run while the cursor stands, and drops it when the cursor goes")
    func reloadKeepsThenDrops() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)

        // An agent filing a card is not the user changing their mind: the head still names a card, so
        // the run survives untouched.
        try fixture.item("\(Ident.lane1)/\(More.filed)", Item.rich(order: "5120", title: "Filed"))
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()
        #expect(store.transient.selectionHead == card1)
        #expect(store.transient.lateralOrdinal == 8)

        // The cursor's own card vanishing is the other case, and the ordinal goes with it. What lands
        // afterwards is 10-accessibility.md's vanishing-focus recovery (`SelectionHeadTests` pins it):
        // the selection had emptied, so focus falls to the card's lane and takes the head with it —
        // through `select`, which is exactly the door that ends a run.
        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Ident.card1)"))
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()
        #expect(store.transient.selectionHead == lane1)
        #expect(store.transient.lateralOrdinal == nil)
    }

    @Test("A vanished cursor takes the ordinal with it, before any recovery gets a say")
    func resolveDropsTheOrdinalWithTheHead() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let state = TransientBoardState()

        state.select([card1, card2], in: .board, anchor: card1, head: card2, lateralOrdinal: 8)

        // The head's card goes and the selection does not: so it is the head rule doing this, not
        // `clearSelection` sweeping everything out on the way past.
        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Ident.card2)"))
        state.resolve(against: try load(fixture))

        #expect(state.selection.ids == [card1])
        #expect(state.selectionHead == nil)
        #expect(state.lateralOrdinal == nil)
    }

    @Test("A query that hides the cursor ends the run, because the ordinal counts what is showing")
    func hidingTheCursorResets() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1], in: .board, anchor: card1, head: card1, lateralOrdinal: 8)
        store.searchQuery = "Second"

        #expect(store.selection.isEmpty)
        #expect(store.transient.selectionHead == nil)
        #expect(store.transient.lateralOrdinal == nil)
    }

    @Test("The store's walk: hop, store, hop again — 10-3-10 through the selection itself")
    func theWalkThroughTheStore() throws {
        let fixture = try makeWalk()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Exactly what `BoardView.step` does on a lane hop, three presses running.
        func press(_ direction: NavigationMath.Direction) throws {
            let head = try #require(store.transient.selectionHead)
            let hop = try #require(
                NavigationMath.lateralHop(
                    from: head,
                    direction,
                    scanning: store.snapshot.lanes,
                    filter: store.searchFilter,
                    sticky: store.transient.lateralOrdinal
                )
            )
            store.select(
                [hop.target], in: .board, anchor: hop.target, head: hop.target, lateralOrdinal: hop.ordinal
            )
        }

        store.select([walkID(1, 8)], in: .board)
        #expect(store.transient.lateralOrdinal == nil, "a fresh selection is not a run")

        try press(.right)
        #expect(store.selection.ids == [walkID(2, 3)])
        try press(.right)
        #expect(store.selection.ids == [walkID(3, 8)])
        try press(.left)
        try press(.left)
        #expect(store.selection.ids == [walkID(1, 8)], "back where the walk began")
    }
}

// MARK: - The registry the arrows and the band read

/// The registry is the one piece of the marquee/arrow pair that is *written* from a view — every card
/// face keeps its drawn frame in it — and it is deliberately unobserved so those writes invalidate
/// nothing (`MarqueeTargetRegistry`, `LaneDropRegistry`'s rule). That is a property of the type rather
/// than of any call site, so it is pinned here: a drag's make-room reflow animates positions, and an
/// observed registry turns each sliding face into a stream of view invalidations at the display's
/// refresh rate for as long as the reflow runs.
@Suite("MarqueeTargetRegistry ▸ registering a frame invalidates nothing")
@MainActor
struct MarqueeTargetRegistryTests {

    /// A box the tracking callback can flip — `withObservationTracking`'s `onChange` is `@Sendable`,
    /// and Observation calls it from wherever the mutation happened.
    private final class Tripwire: @unchecked Sendable {
        var fired = false
    }

    @Test("Every reader the band and the arrows use registers no observation")
    func writesAreNotObserved() {
        let registry = MarqueeTargetRegistry()
        registry.update(target(card1, x: 0, y: 0))
        let tripwire = Tripwire()

        withObservationTracking {
            // Exactly the three reads the event-time callers make: the sample loop's `all`
            // (`MarqueeControl.gesture`), the arrows' keyed lookup (`BoardView.step`/`.extend`), and
            // the band's begin guard.
            _ = registry.all
            _ = registry.targets[card1]
            _ = registry.contains(.zero)
        } onChange: {
            tripwire.fired = true
        }

        // A reflow's worth of re-registration: the same card at a new frame, a new card, a removal.
        registry.update(target(card1, x: 0, y: 40))
        registry.update(target(card2, x: 0, y: 200))
        registry.remove(card2)

        #expect(tripwire.fired == false, "MarqueeTargetRegistry must not be @Observable")
    }

    @Test("The frames it hands back are still live after those writes")
    func readsStayCorrect() {
        let registry = MarqueeTargetRegistry()
        registry.update(target(card1, x: 0, y: 0))
        registry.update(target(card2, x: 0, y: 120))

        registry.update(target(card1, x: 0, y: 40))
        #expect(registry.targets[card1]?.frame.minY == 40)
        #expect(registry.all.count == 2)
        // Inside the moved card1 (y 40…140), and above where it now starts.
        #expect(registry.contains(CGPoint(x: 10, y: 50)))
        #expect(!registry.contains(CGPoint(x: 10, y: 20)))
        // Inside card2 (y 120…220) and nothing else.
        #expect(registry.contains(CGPoint(x: 10, y: 200)))

        registry.remove(card2)
        #expect(registry.targets[card2] == nil)
        #expect(!registry.contains(CGPoint(x: 10, y: 200)))
    }
}

// MARK: - SortMath

@Suite("SortMath ▸ within-lane sort")
struct SortMathTests {

    private let ordered = [card1, card2, card3, card4]

    @Test("A contiguous block steps one position, hopping its neighbour")
    func stepsOnePosition() {
        #expect(SortMath.reordered(ordered, moving: [card3], .up) == [card1, card3, card2, card4])
        #expect(SortMath.reordered(ordered, moving: [card2], .down) == [card1, card3, card2, card4])
        #expect(SortMath.reordered(ordered, moving: [card2, card3], .up) == [card2, card3, card1, card4])
        #expect(SortMath.reordered(ordered, moving: [card2, card3], .down) == [card1, card4, card2, card3])
    }

    @Test("A non-contiguous selection gathers behind its first card, relative order preserved")
    func gathersOnTheFirstPress() {
        // "Anchored at the first selected card (first = lowest logical order; the rest follow in
        // preserved relative order)" — and the press that gathers does not also step, which is why
        // both directions give the same answer.
        #expect(SortMath.reordered(ordered, moving: [card2, card4], .up) == [card1, card2, card4, card3])
        #expect(SortMath.reordered(ordered, moving: [card2, card4], .down) == [card1, card2, card4, card3])
        #expect(SortMath.reordered(ordered, moving: [card1, card3], .up) == [card1, card3, card2, card4])
        #expect(
            SortMath.reordered(ordered, moving: [card1, card4], .down) == [card1, card4, card2, card3],
            "the unselected cards keep their relative order around the block"
        )
    }

    @Test("At the ladder's end, and with nothing to move, the answer is nil rather than a no-op write")
    func edgesAndEmptyAreNil() {
        #expect(SortMath.reordered(ordered, moving: [card1], .up) == nil)
        #expect(SortMath.reordered(ordered, moving: [card4], .down) == nil)
        #expect(SortMath.reordered(ordered, moving: [card1, card2], .up) == nil)
        #expect(SortMath.reordered(ordered, moving: Set(ordered), .up) == nil)
        #expect(SortMath.reordered(ordered, moving: Set(ordered), .down) == nil)
        #expect(SortMath.reordered(ordered, moving: [], .up) == nil)
        #expect(SortMath.reordered([card1], moving: [card1], .down) == nil, "a lane of one has nowhere to go")
        #expect(
            SortMath.reordered(ordered, moving: [card5], .up) == nil,
            "ids the lane does not render are ignored, so a stale selection moves nothing"
        )
    }
}

// MARK: - The successor rule

@MainActor
@Suite("SelectionGrammar ▸ successor on delete")
struct SuccessorTests {

    @Test("The next card in the lane, so repeated ⌫ walks down it")
    func nextCardInTheLane() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        #expect(SelectionGrammar.successor(afterDeleting: [card2], snapshot: snapshot) == card3)
        #expect(SelectionGrammar.successor(afterDeleting: [card1], snapshot: snapshot) == card2)
        #expect(
            SelectionGrammar.successor(afterDeleting: [card1, card2], snapshot: snapshot) == card3,
            "a block's successor is the first survivor after its last member"
        )
    }

    @Test("The last sibling falls back to its predecessor")
    func predecessorFallback() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        #expect(SelectionGrammar.successor(afterDeleting: [card4], snapshot: snapshot) == card3)
        #expect(SelectionGrammar.successor(afterDeleting: [card3, card4], snapshot: snapshot) == card2)
    }

    @Test("A survivor between the members is found forwards first")
    func forwardSearchWinsOverBackward() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        // Doomed at positions 0 and 2: forward from the last one finds card4, which is what makes
        // repeated ⌫ keep moving down rather than bouncing back up the lane.
        #expect(SelectionGrammar.successor(afterDeleting: [card1, card3], snapshot: snapshot) == card4)
    }

    @Test("An emptied container selects nothing")
    func emptiedContainerIsNil() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        #expect(SelectionGrammar.successor(afterDeleting: [card1, card2, card3, card4], snapshot: snapshot) == nil)
        #expect(SelectionGrammar.successor(afterDeleting: [], snapshot: snapshot) == nil)
        #expect(
            SelectionGrammar.successor(afterDeleting: [ItemID(rawValue: "nobody")], snapshot: snapshot) == nil,
            "ids naming nothing name no container either"
        )
    }

    @Test("A cross-lane selection is answered in its last member's lane, in flatten order")
    func crossLaneUsesTheLastMembersLane() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        // card5 is later than card2 in flatten order (lane `order`, then card `order`), so the
        // container is lane2 — the same "last member" anchor ⌘N and paste already share.
        #expect(SelectionGrammar.successor(afterDeleting: [card2, card5], snapshot: snapshot) == card6)
    }

    @Test("Lanes follow the same rule in the live lane order")
    func laneSuccessors() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        #expect(SelectionGrammar.successor(afterDeleting: [lane1], snapshot: snapshot) == lane2)
        #expect(SelectionGrammar.successor(afterDeleting: [lane3], snapshot: snapshot) == lane2, "the last lane's predecessor")
        #expect(SelectionGrammar.successor(afterDeleting: [lane1, lane2, lane3], snapshot: snapshot) == nil)
    }

    @Test("⌫ selects the successor immediately, before the reload echoes the tombstone back")
    func deleteSelectsTheSuccessor() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card2], in: .board)
        store.deleteSelection()

        #expect(store.selection.ids == [card3])
        #expect(store.transient.selectionAnchor == card3, "the successor is a legitimate range origin")
        #expect(store.transient.selectionHead == card3, "and the place the next arrow steps from")

        // Repeated ⌫ walks down the lane — the whole point of the rule. The store's snapshot has not
        // reloaded, so card2 is still in it and card3's successor is card4.
        store.deleteSelection()
        #expect(store.selection.ids == [card4])
    }

    @Test("An emptied lane clears the selection instead of inventing one")
    func deleteClearsWhenNothingSurvives() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card5, card6], in: .board)
        store.deleteSelection()

        #expect(store.selection.isEmpty)
        #expect(store.transient.selectionHead == nil)
    }
}

// MARK: - The navigation head

@MainActor
@Suite("TransientBoardState ▸ the navigation head")
struct SelectionHeadTests {

    @Test("A sole member is its own head; any other count leaves none")
    func headDefaults() throws {
        let state = TransientBoardState()

        state.select([card1], in: .board)
        #expect(state.selectionHead == card1)

        state.select([card1, card2], in: .board)
        #expect(state.selectionHead == nil, "a set with no gesture behind it names no cursor")

        state.select([card1, card2], in: .board, anchor: card1, head: card2)
        #expect(state.selectionAnchor == card1)
        #expect(state.selectionHead == card2, "an explicit head is kept whatever the count")

        state.clearSelection()
        #expect(state.selectionHead == nil)
        #expect(state.selectionAnchor == nil)
    }

    @Test("A ⇧-gesture moves the head and leaves the anchor — that asymmetry is why both exist")
    func shiftMovesOnlyTheHead() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let snapshot = try load(fixture)

        let outcome = SelectionGrammar.click(
            SelectionTarget(id: card3, kind: .card, container: .board),
            modifier: .shift,
            selection: ItemReferenceSet(ids: [card1], container: .board),
            anchor: card1,
            snapshot: snapshot
        )

        #expect(outcome.selection.ids == [card1, card2, card3])
        #expect(outcome.anchor == card1, "the range origin stays put")
        #expect(outcome.head == card3, "the cursor walks to what was clicked")
    }

    @Test("A vanished head is dropped by the reload, like every other item reference")
    func resolveDropsAVanishedHead() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1, card2], in: .board, anchor: card1, head: card2)

        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Ident.card2)"))
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()

        #expect(store.selection.ids == [card1])
        #expect(store.transient.selectionHead == nil)
        #expect(store.transient.selectionAnchor == card1, "the anchor survived — it is still in the tree")
    }

    @Test("A container crossing is a vanish for the head too")
    func resolveDropsAContainerCrossedHead() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1], in: .board)
        #expect(store.transient.selectionHead == card1)

        try fixture.move("\(Ident.lane1)/\(Ident.card1)", toTrash: Ident.card1)
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()

        // The crossing dropped the head, which is this test's rule. What stands afterwards is
        // 10-accessibility.md's vanishing-focus recovery, layered on top: the selection had emptied
        // and the cursor's card had gone, so focus lands on the card's lane and takes the head with
        // it (`BoardAnnouncerStoreTests`, where the recovery itself is pinned). The card's own head
        // reference is gone either way, which is what "a container crossing is a vanish" claims.
        #expect(store.selection.ids == [lane1])
        #expect(store.transient.selectionHead == lane1)
    }
}

// MARK: - The sort's write

@MainActor
@Suite("BoardStore ▸ sortSelection")
struct SortWriteTests {

    @Test("A step rewrites the two cards that swapped and nothing else")
    func stepWritesTheMinimum() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let untouchedFirst = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)")
        let untouchedFourth = try fixture.indexText("\(Ident.lane1)/\(Ident.card4)")

        store.select([card3], in: .board)
        store.sortSelection(.up)

        #expect(try cardOrder(Ident.lane1, in: fixture) == [card1, card3, card2, card4])
        #expect(
            try fixture.indexText("\(Ident.lane1)/\(Ident.card1)") == untouchedFirst,
            "a card whose position did not change keeps its bytes — no stamp, no commit"
        )
        #expect(try fixture.indexText("\(Ident.lane1)/\(Ident.card4)") == untouchedFourth)
        #expect(store.selection.ids == [card3], "the ids all survive, so the selection is left alone")
        #expect(store.banners.oneShots.isEmpty)
    }

    @Test("A gather collects the block behind its first card, on disk")
    func gatherWrites() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card2, card4], in: .board)
        store.sortSelection(.up)

        #expect(try cardOrder(Ident.lane1, in: fixture) == [card1, card2, card4, card3])
    }

    @Test("A step down moves the block past its following sibling")
    func stepDownWrites() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([card1, card2], in: .board)
        store.sortSelection(.down)

        #expect(try cardOrder(Ident.lane1, in: fixture) == [card3, card1, card2, card4])
    }

    @Test("Duplicate ranks are compacted first, because a permutation cannot outrank a name tie-break")
    func duplicateOrdersRenumberFirst() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
        // Two cards sharing one rank: display order falls to the folder-name tie-break
        // (`Ranks.isOrderedForDisplay`), which card1's `5555…` wins over card2's `6666…`.
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
        try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "1024", title: "Second"))
        let store = try BoardStore(rootURL: fixture.root)

        #expect(try cardOrder(Ident.lane1, in: fixture) == [card1, card2])

        store.select([card2], in: .board)
        store.sortSelection(.up)

        #expect(try cardOrder(Ident.lane1, in: fixture) == [card2, card1])
        #expect(store.banners.oneShots.isEmpty)
    }

    @Test("The plan refuses every case the design calls inert")
    func planRefusals() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.sortPlan(.up) == nil, "nothing selected")

        store.select([lane1], in: .board)
        #expect(store.sortPlan(.up) == nil, "a lane selection — ⌥⌘↑/⌥⌘↓ are inert on lanes")

        store.select([card2, card5], in: .board)
        #expect(store.sortPlan(.up) == nil, "a card selection spanning lanes — cards never change lanes by ⌘-arrow")

        store.select([card1], in: .trash)
        #expect(store.sortPlan(.up) == nil, "a tombstoned selection")

        store.select([card1], in: .board)
        #expect(store.sortPlan(.up) == nil, "already at the top")
        #expect(store.sortPlan(.down) != nil, "but the other direction is live")
    }
}

// MARK: - The lane move's index convention

@MainActor
@Suite("BoardStore ▸ moveLane's one-slot convention")
struct MoveLaneConventionTests {

    /// The index `MoveLaneCommands` passes: the lane's display position among the live lanes, plus
    /// or minus one. `moveLane` counts that position **with the moved lane already removed**, which
    /// is exactly what makes `from ± 1` one slot — and is easy enough to get backwards that it is
    /// pinned here rather than left to the drag path's coverage.
    @Test("from − 1 moves one slot left, from + 1 moves one slot right")
    func oneSlotEachWay() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        let lanes = SelectionGrammar.lanes(in: store.snapshot)
        #expect(lanes == [lane1, lane2, lane3])
        let from = try #require(lanes.firstIndex(of: lane2))

        store.moveLane(lane2, toIndex: from - 1)
        #expect(try load(fixture).lanes.map(\.id) == [lane2, lane1, lane3])
    }

    @Test("A step right hops exactly one lane, never to the end")
    func stepRightHopsOne() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        let lanes = SelectionGrammar.lanes(in: store.snapshot)
        let from = try #require(lanes.firstIndex(of: lane1))

        store.moveLane(lane1, toIndex: from + 1)
        #expect(try load(fixture).lanes.map(\.id) == [lane2, lane1, lane3])
    }
}

// MARK: - LaneMoveTarget's destination predicate

/// `LaneMoveTarget.destination` — the pure predicate behind Board ▸ Move Left/Move Right
/// (`MoveLaneCommands`) and, since 2026-08-09 ▸ "redesign context menu for cards", the card context
/// menu's Navigation submenu (`CardFaceView`). Pinned directly rather than only through
/// `MoveLaneCommands`'s own view, which the suite above already exercises via `store.moveLane`'s
/// convention but never through this gate — the predicate itself was previously untested in
/// isolation.
@MainActor
@Suite("LaneMoveTarget ▸ destination")
struct LaneMoveTargetTests {

    @Test("A sole selected mid-board lane answers both directions")
    func midBoardLaneAnswersBothWays() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let selection = ItemReferenceSet(ids: [lane2], container: .board)

        let left = LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: -1)
        #expect(left?.lane == lane2)
        #expect(left?.index == 0)

        let right = LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: 1)
        #expect(right?.lane == lane2)
        #expect(right?.index == 2)
    }

    @Test("The left wall refuses left and the right wall refuses right")
    func wallsRefuseTheirOwnDirection() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        let atLeftWall = ItemReferenceSet(ids: [lane1], container: .board)
        #expect(LaneMoveTarget.destination(selection: atLeftWall, snapshot: store.snapshot, delta: -1) == nil)
        #expect(LaneMoveTarget.destination(selection: atLeftWall, snapshot: store.snapshot, delta: 1) != nil)

        let atRightWall = ItemReferenceSet(ids: [lane3], container: .board)
        #expect(LaneMoveTarget.destination(selection: atRightWall, snapshot: store.snapshot, delta: 1) == nil)
        #expect(LaneMoveTarget.destination(selection: atRightWall, snapshot: store.snapshot, delta: -1) != nil)
    }

    /// "A card id is in no lane order, so this is also the 'not a lane' test" — the exact clause the
    /// card context menu's Navigation rows lean on to justify their unconditional `.disabled(true)`
    /// (`CardFaceView.boardMenu`'s own doc comment): a card selection can never itself make this
    /// predicate answer `true`, whatever card it names.
    @Test("A card selection answers nil in both directions — a card id is in no lane order")
    func cardSelectionAnswersNil() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let selection = ItemReferenceSet(ids: [card1], container: .board)

        #expect(LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: -1) == nil)
        #expect(LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: 1) == nil)
    }

    @Test("A multi-lane selection answers nil — one slot has no meaning for a discontiguous pair")
    func multiLaneSelectionAnswersNil() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let selection = ItemReferenceSet(ids: [lane1, lane2], container: .board)

        #expect(LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: -1) == nil)
        #expect(LaneMoveTarget.destination(selection: selection, snapshot: store.snapshot, delta: 1) == nil)
    }

    @Test("An empty selection and a trash-container selection both answer nil")
    func emptyAndTrashSelectionsAnswerNil() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(LaneMoveTarget.destination(selection: .empty, snapshot: store.snapshot, delta: 1) == nil)

        let trashSelection = ItemReferenceSet(ids: [lane1], container: .trash)
        #expect(LaneMoveTarget.destination(selection: trashSelection, snapshot: store.snapshot, delta: 1) == nil)
    }
}

// MARK: - CardMoveTarget's destination predicate

/// `CardMoveTarget.destination` — the pure predicate behind the card context menu's Navigation ▸
/// Move Left/Move Right (2026-08-09 ▸ "Give the card context menu's Navigation rows real card-move
/// behavior", card 06322636), `LaneMoveTarget.destination`'s cousin: same board-order lane stepping,
/// but validated against a **card's own current lane** rather than the live selection's lane
/// membership. `hasNeighbor` and `selectionSpansOtherLanes` are the two halves `CardFaceView` hoists
/// to the arrangement level (`LaneView.scrollableCards`) as `hasLeftNeighbor`/`hasRightNeighbor`/
/// `selectionSpansLanes` — pinned here directly, `LaneMoveTargetTests`' own reason.
///
/// Three cards in lane one, one in lane two, one in lane three — enough room for an index that has to
/// clamp landing in a lane, a card in every lane an edge case needs one in, and a third lane to prove
/// a target spanning two of them is refused regardless of which two.
@MainActor
private func makeCardMoveBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.item("", Item.board)
    try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
    try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
    try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
    try fixture.item("\(Ident.lane1)/\(Ident.card3)", Item.rich(order: "3072", title: "Third"))
    try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
    try fixture.item("\(Ident.lane2)/\(Ident.card4)", Item.rich(order: "1024", title: "Fourth"))
    try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Done"))
    try fixture.item("\(Ident.lane3)/\(More.card5)", Item.rich(order: "1024", title: "Fifth"))
    return fixture
}

@MainActor
@Suite("CardMoveTarget ▸ destination")
struct CardMoveTargetTests {

    @Test("The leftmost lane's own card refuses a further-left move")
    func leftmostLaneRefusesLeft() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(CardMoveTarget.destination(
            clicked: card1, targeting: [card1], snapshot: store.snapshot, delta: -1) == nil)
        #expect(CardMoveTarget.hasNeighbor(of: lane1, delta: -1, in: store.snapshot) == false)
    }

    @Test("The rightmost lane's own card refuses a further-right move")
    func rightmostLaneRefusesRight() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(CardMoveTarget.destination(
            clicked: card5, targeting: [card5], snapshot: store.snapshot, delta: 1) == nil)
        #expect(CardMoveTarget.hasNeighbor(of: lane3, delta: 1, in: store.snapshot) == false)
    }

    @Test("A mid-board card's own card answers both directions, landing at its own index")
    func midBoardCardAnswersBothWays() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Fourth is lane two's sole card — index 0 — and lane two sits between lane one (three
        // cards) and lane three (one card), so both directions have somewhere to land.
        let right = CardMoveTarget.destination(
            clicked: card4, targeting: [card4], snapshot: store.snapshot, delta: 1)
        #expect(right?.laneID == lane3)
        #expect(right?.index == 0)

        let left = CardMoveTarget.destination(
            clicked: card4, targeting: [card4], snapshot: store.snapshot, delta: -1)
        #expect(left?.laneID == lane1)
        #expect(left?.index == 0)
    }

    @Test("An index past the destination lane's card count clamps rather than trapping")
    func indexClamps() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Third is index 2 in lane one; lane two holds exactly one card, so the only valid insertion
        // indices there are 0 and 1 — 2 must clamp down to 1, not trap or land past the end silently.
        let target = CardMoveTarget.destination(
            clicked: card3, targeting: [card3], snapshot: store.snapshot, delta: 1)
        #expect(target?.laneID == lane2)
        #expect(target?.index == 1)
    }

    @Test("A widened group lands at the CLICKED member's index, not the group's own extent")
    func groupAnchorsOnTheClickedMember() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let group: Set<ItemID> = [card1, card3]

        // Clicked First (index 0) — the group's own leading edge.
        let clickedFirst = CardMoveTarget.destination(
            clicked: card1, targeting: group, snapshot: store.snapshot, delta: 1)
        #expect(clickedFirst?.laneID == lane2)
        #expect(clickedFirst?.index == 0, "anchored on the clicked card's own index, not the group's")

        // Clicked Third (index 2) — the same group, the other member clicked, clamped the same way
        // `indexClamps` pins for a single-card target.
        let clickedThird = CardMoveTarget.destination(
            clicked: card3, targeting: group, snapshot: store.snapshot, delta: 1)
        #expect(clickedThird?.laneID == lane2)
        #expect(clickedThird?.index == 1)
    }

    @Test("A target reaching outside the clicked card's own lane answers nil — no coherent left for a spread")
    func multiLaneTargetAnswersNil() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let spread: Set<ItemID> = [card1, card5]

        #expect(CardMoveTarget.destination(
            clicked: card1, targeting: spread, snapshot: store.snapshot, delta: 1) == nil)
        #expect(CardMoveTarget.selectionSpansOtherLanes(spread, laneCardIDs: [card1, card2, card3]))
    }

    @Test("An empty target, and a clicked id the board no longer holds, both answer nil")
    func emptyOrUnknownClickedAnswersNil() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(CardMoveTarget.destination(
            clicked: card1, targeting: [], snapshot: store.snapshot, delta: 1) == nil)
        #expect(CardMoveTarget.destination(
            clicked: ItemID(rawValue: Ident.indexless), targeting: [ItemID(rawValue: Ident.indexless)],
            snapshot: store.snapshot, delta: 1) == nil)
    }

    @Test("A single-lane board has no neighbour in either direction")
    func singleLaneHasNoNeighbor() throws {
        let fixture = try WriterFixture()
        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Solo"))
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Only"))
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(CardMoveTarget.hasNeighbor(of: lane1, delta: -1, in: store.snapshot) == false)
        #expect(CardMoveTarget.hasNeighbor(of: lane1, delta: 1, in: store.snapshot) == false)
        #expect(CardMoveTarget.destination(
            clicked: card1, targeting: [card1], snapshot: store.snapshot, delta: -1) == nil)
        #expect(CardMoveTarget.destination(
            clicked: card1, targeting: [card1], snapshot: store.snapshot, delta: 1) == nil)
    }

    @Test("hasNeighbor answers false for a lane id the snapshot no longer holds")
    func hasNeighborAnswersFalseForAnUnknownLane() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let unknown = ItemID(rawValue: Ident.indexless)

        #expect(CardMoveTarget.hasNeighbor(of: unknown, delta: -1, in: store.snapshot) == false)
        #expect(CardMoveTarget.hasNeighbor(of: unknown, delta: 1, in: store.snapshot) == false)
    }

    @Test("selectionSpansOtherLanes: confined selections and the empty selection both answer false")
    func selectionSpansOtherLanesFalseCases() throws {
        #expect(CardMoveTarget.selectionSpansOtherLanes([], laneCardIDs: [card1, card2]) == false)
        #expect(CardMoveTarget.selectionSpansOtherLanes([card1], laneCardIDs: [card1, card2]) == false)
        #expect(CardMoveTarget.selectionSpansOtherLanes([card1, card2], laneCardIDs: [card1, card2]) == false)
    }

    /// **The regression this predicate exists not to reintroduce**: a selection with no member in
    /// this lane at all — the ordinary case for every lane the user did not click into — must answer
    /// `false`, the same as an empty selection, or every untouched lane's faces would read a spread
    /// whenever *anything* elsewhere on the board got selected (caught the hard way, by
    /// `BoardRenderPerformanceTests.selectionStillRepaints` going from 8 faces to 151 on a single
    /// click — see this function's own doc comment).
    @Test("A selection with no member in this lane at all answers false, not true")
    func selectionEntirelyOutsideTheLaneAnswersFalse() throws {
        #expect(CardMoveTarget.selectionSpansOtherLanes([card4], laneCardIDs: [card1, card2, card3]) == false)
    }

    /// **Feeding `destination`'s own answer to `BoardStore.moveCards` lands the card exactly there** —
    /// the reuse the owner ruling asks for ("rank minting through the existing gapped-rank machinery
    /// … reuse, don't reinvent") closes the loop: the predicate's `(laneID, index)` is not merely
    /// shaped like what `moveCards` wants, it actually produces the destination this test reads back
    /// off disk.
    @Test("The predicate's answer, handed to moveCards, actually lands the card there")
    func destinationFeedsMoveCardsCorrectly() throws {
        let fixture = try makeCardMoveBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        let target = try #require(CardMoveTarget.destination(
            clicked: card2, targeting: [card2], snapshot: store.snapshot, delta: 1))
        store.moveCards([card2], toLane: target.laneID, at: target.index)

        let moved = try BoardLoader.load(boardRoot: fixture.root).model
        let sourceLane = try #require(moved.lanes.first { $0.id == lane1 })
        let destinationLane = try #require(moved.lanes.first { $0.id == lane2 })
        #expect(sourceLane.cards.map(\.id) == [card1, card3], "Second really left lane one")
        // Lane two held one card (Fourth) before the move; index 1 among it is "after Fourth",
        // Second's own index (1) carried over from a three-card lane one, clamped by `moveCards`.
        #expect(destinationLane.cards.map(\.id) == [card4, card2], "and landed after Fourth")
        #expect(store.banners.oneShots.isEmpty)
    }
}
