import AppKit
import Testing
@testable import Kanban

// MARK: - Fixtures

/// Two lanes, two cards in the first — enough board for New Card to have a target.
@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.lane2, Item.rich(order: "2048", title: "Doing"))
    return fixture
}

/// A board with no lanes — where "card creation has no target … New Card disables via menu
/// validation until a lane exists" (04-interactions.md ▸ The map).
@MainActor
private func makeEmptyBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.item("", Item.board)
    return fixture
}

private extension Array where Element == ToolbarItemSpec {
    @MainActor
    func spec(_ identifier: NSToolbarItem.Identifier) -> ToolbarItemSpec? {
        first { $0.identifier == identifier }
    }
}

/// A zoom store on a scratch defaults domain — the level is app-wide and persisted, so a suite that
/// used `.standard` would zoom the developer's own boards (`StyleRecents`' injection, for its reason).
@MainActor
private func makeZoom(level: CGFloat = BoardZoom.actualSize) -> BoardZoomStore {
    let name = "dev.rzen.indie.Kanban.toolbar-tests.\(UUID().uuidString)"
    let store = BoardZoomStore(defaults: UserDefaults(suiteName: name)!)
    store.setLevel(level)
    return store
}

/// An appearance store on a scratch defaults domain, apply seam stubbed out — this suite's subject is
/// the toolbar item, not the store itself (`AppearanceTests.swift` owns that), so nothing here should
/// touch `NSApp` (`makeZoom`'s reason, turned toward AppKit).
@MainActor
private func makeAppearance() -> AppearanceStore {
    let name = "dev.rzen.indie.Kanban.toolbar-tests.appearance.\(UUID().uuidString)"
    return AppearanceStore(defaults: UserDefaults(suiteName: name)!, apply: { _ in })
}

/// The board catalog, with the four collaborators every test here supplies the same way: a fresh
/// zoom store, a fresh appearance store, a drag session with nothing in flight, and no undo stack —
/// which is the honest default for a suite whose subject is the *catalog*, and the Undo/Redo pair's
/// own tests pass one (`undoPairCrossesTheSessionsStack`).
@MainActor
private func boardSpecs(
    store: BoardStore,
    search: BoardSearchPresentation = BoardSearchPresentation(),
    zoom: BoardZoomStore? = nil,
    appearance: AppearanceStore? = nil,
    session: DragSession = DragSession(),
    undo: BoardUndoManager? = nil
) -> [ToolbarItemSpec] {
    BoardToolbar.specs(
        store: store,
        search: search,
        zoom: zoom ?? makeZoom(),
        appearance: appearance ?? makeAppearance(),
        session: session,
        undo: undo
    )
}

/// A board stack with a real substrate behind it — the shape `BoardWindowHost` hands the toolbar
/// (`AppModel`'s session composes exactly this pair).
@MainActor
private func makeUndo(isReadOnly: @escaping @MainActor () -> Bool = { false }) -> (BoardUndoManager, NativeHistoryProvider) {
    let provider = NativeHistoryProvider()
    return (BoardUndoManager(history: provider, isReadOnly: isReadOnly), provider)
}

/// One step that records its crossings — `HistoryProviderTests`' synthetic fixture, in the one shape
/// this suite needs: what the toolbar pair must prove is that firing it *reaches* the stack, not what
/// the stack then does to disk.
@MainActor
private func countingStep(_ name: String, crossings: @escaping @MainActor () -> Void) -> HistoryStep {
    HistoryStep(
        name: name,
        undo: { _ in crossings(); return .applied },
        redo: { _ in crossings(); return .applied }
    )
}

// MARK: - The vocabulary

/// **Toolbar item labels are menu titles minus a trailing ellipsis** (03-board-ui.md ▸ Toolbar) —
/// "one vocabulary everywhere, and the customize palette self-documents against the menus".
///
/// Pinned as a function rather than trusted per item because the rule's whole point is that nobody
/// spells a second name by hand: a menu row renamed without its toolbar item is two names for one
/// function, and the palette is exactly where a user compares them.
@Suite("Toolbar ▸ the label vocabulary")
struct ToolbarVocabularyTests {

    @Test("A trailing ellipsis is dropped, in either spelling")
    func trailingEllipsisIsDropped() {
        // 03's own example: "macOS convention: 'Add Attachment…' labels as Add Attachment".
        #expect(ToolbarVocabulary.label(menuTitle: "Add Attachment…") == "Add Attachment")
        #expect(ToolbarVocabulary.label(menuTitle: "Add Attachment...") == "Add Attachment")
        #expect(ToolbarVocabulary.label(menuTitle: "Style…") == "Style")
    }

    @Test("A title with no ellipsis is its own label, verbatim")
    func plainTitlesSurviveUntouched() {
        #expect(ToolbarVocabulary.label(menuTitle: "Show Trash") == "Show Trash")
        #expect(ToolbarVocabulary.label(menuTitle: "New Card") == "New Card")
        #expect(ToolbarVocabulary.label(menuTitle: "Raw Source") == "Raw Source")
    }

    @Test("Only a *trailing* ellipsis is a suffix; one inside the title is part of the name")
    func interiorEllipsisIsNotStripped() {
        #expect(ToolbarVocabulary.label(menuTitle: "Undo Move Card…") == "Undo Move Card")
        #expect(ToolbarVocabulary.label(menuTitle: "Open… Recent") == "Open… Recent")
    }

    @Test("The space a stripped ellipsis leaves behind goes with it")
    func trailingSpaceIsTrimmed() {
        #expect(ToolbarVocabulary.label(menuTitle: "Empty Trash …") == "Empty Trash")
    }
}

// MARK: - The board window's toolbar

/// The board toolbar's shipped defaults, its catalog, and the predicates its items mirror
/// (03-board-ui.md ▸ Toolbar).
@MainActor
@Suite("Toolbar ▸ the board window")
struct BoardToolbarTests {

    @Test("The default set is the search field and Appearance, trailing, and nothing else")
    func defaultsAreTheSearchFieldAndAppearance() {
        // "Board window default: the search field and Appearance — both centered, immediately after
        // the field." The flexible space ahead of them is what keeps the pair off the leading edge, so
        // the *items* in the default set are exactly two.
        #expect(BoardToolbar.defaultItems == [.flexibleSpace, .boardSearch, .boardAppearance])
        #expect(BoardToolbar.defaultItems.filter { $0 != .flexibleSpace } == [.boardSearch, .boardAppearance])
    }

    @Test("The catalog is 03's eight commands plus the field — and the board popover is not in it")
    func catalogIsTheDesignsInventory() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let specs = boardSpecs(store: store)

        // "Catalog (available via Customize): New Card, New Lane, Zoom In, Zoom Out …, Undo, Redo …,
        // Show Trash, Appearance" — plus the search field, which is a catalog item too (a user who
        // removes it can put it back).
        #expect(specs.map(\.identifier) == [
            .boardNewCard,
            .boardNewLane,
            .boardZoomIn,
            .boardZoomOut,
            .boardUndo,
            .boardRedo,
            .boardShowTrash,
            .boardAppearance,
            .boardSearch,
        ])
        // "The board popover deliberately has no toolbar item — the window-title widget is its
        // committed home, and a second entry would muddy it." Absence is a settlement, so it is
        // pinned by the exact-inventory assertion above and stated again here.
        #expect(specs.count == 9)
    }

    /// The zoom pair is catalog-only — "the titlebar's default stays the search field alone"
    /// (03-board-ui.md ▸ Toolbar). Stated separately from the default-set test because this is the
    /// claim a future item is most likely to break by helpfully adding itself.
    @Test("Zoom In and Zoom Out are available but never default")
    func zoomIsCatalogOnly() {
        #expect(!BoardToolbar.defaultItems.contains(.boardZoomIn))
        #expect(!BoardToolbar.defaultItems.contains(.boardZoomOut))
    }

    /// There is deliberately no Actual Size item: it is a menu row only. Two buttons are the whole
    /// of what a toolbar can usefully offer for a ladder with no readout — a third that resets is
    /// titlebar clutter for a chord.
    @Test("Actual Size has no toolbar item")
    func actualSizeIsMenuOnly() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(!boardSpecs(store: store).contains { $0.label == "Actual Size" })
    }

    /// Each button mirrors its menu row's ladder end, which is the same predicate `ZoomCommands`
    /// disables on — one answer, two faces (03-board-ui.md ▸ Toolbar).
    @Test("The zoom buttons disable at their own end of the ladder")
    func zoomButtonsMirrorTheLadderEnds() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Every collaborator a spec's `isEnabled` interrogates lives in a local for the whole
        // test, exactly as `store` already does: the closures capture them weakly — production
        // hands them app-lived objects — so a temporary reads as uniformly disabled.
        let session = DragSession()
        let topZoom = makeZoom(level: BoardZoom.levels.last!)
        let top = boardSpecs(store: store, zoom: topZoom, session: session)
        #expect(try !#require(top.spec(.boardZoomIn)).isEnabled)
        #expect(try #require(top.spec(.boardZoomOut)).isEnabled)

        let bottomZoom = makeZoom(level: BoardZoom.levels.first!)
        let bottom = boardSpecs(store: store, zoom: bottomZoom, session: session)
        #expect(try #require(bottom.spec(.boardZoomIn)).isEnabled)
        #expect(try !#require(bottom.spec(.boardZoomOut)).isEnabled)

        let middleZoom = makeZoom()
        let middle = boardSpecs(store: store, zoom: middleZoom, session: session)
        #expect(try #require(middle.spec(.boardZoomIn)).isEnabled)
        #expect(try #require(middle.spec(.boardZoomOut)).isEnabled)
    }

    /// Both are push buttons, and both go dead while a drag is in flight — the menu rows' guard,
    /// reached through the very same predicate (`ZoomCommands.isEnabled`).
    @Test("The zoom buttons are push buttons, and hold shut mid-drag")
    func zoomButtonsHoldShutMidDrag() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Held in locals for the reason `zoomButtonsMirrorTheLadderEnds` spells out — and the
        // dragging half asks with the same live zoom, so its disabled answer is the drag guard's
        // rather than a dead weak reference's.
        let zoom = makeZoom()
        let restingSession = DragSession()
        let resting = boardSpecs(store: store, zoom: zoom, session: restingSession)
        for identifier in [NSToolbarItem.Identifier.boardZoomIn, .boardZoomOut] {
            let spec = try #require(resting.spec(identifier))
            #expect(spec.isOn == nil, "\(spec.label) is a push button, not a toggle")
            #expect(spec.isEnabled)
        }

        let session = DragSession()
        let folder = store.rootURL.appendingPathComponent(Ident.card1, isDirectory: true)
        session.beginCards([ItemID(rawValue: Ident.card1)], folders: [folder], heights: [44],
                           container: .board, source: store)
        let dragging = boardSpecs(store: store, zoom: zoom, session: session)
        for identifier in [NSToolbarItem.Identifier.boardZoomIn, .boardZoomOut] {
            #expect(try !#require(dragging.spec(identifier)).isEnabled)
        }
    }

    @Test("Every label is its menu row's title, and Undo/Redo keep static ones")
    func labelsMatchTheMenus() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let specs = boardSpecs(store: store)

        #expect(specs.map(\.label) == [
            "New Card", "New Lane", "Zoom In", "Zoom Out", "Undo", "Redo", "Show Trash", "Appearance", "Search",
        ])
        // The one exception 03 names: "the Undo/Redo toolbar items keep static labels — the menu
        // titles are rewritten dynamically ('Undo Move Card…'), which a toolbar label doesn't
        // track". They are ordinary buttons since the command surface became the app's own
        // (13-native-undo.md ▸ Rules ▸ the command-surface bullet, re-ruled 2026-08-08) — the label
        // exception survived the mechanism that motivated it, because it was never about *how* the
        // item fires.
        for identifier in [NSToolbarItem.Identifier.boardUndo, .boardRedo] {
            let spec = try #require(specs.spec(identifier))
            guard case .button = spec.behavior else {
                Issue.record("\(identifier.rawValue) must carry an explicit target over the session's stack")
                return
            }
            #expect(spec.isOn == nil, "\(spec.label) is a push button, not a toggle")
        }
    }

    /// **The pair's predicate is the menu rows' own object** (13-native-undo.md ▸ Rules ▸ the
    /// command-surface bullet, re-ruled 2026-08-08): both surfaces read one `BoardUndoManager`, so
    /// enablement here is `canUndo`/`canRedo` and nothing of the toolbar's own.
    ///
    /// This replaces the nil-target pin the pair carried until that re-ruling — the responder-chain
    /// route it named is unreachable on a SwiftUI window, and what took its place is a predicate this
    /// suite can read directly rather than one only `NSWindow` could answer.
    @Test("Undo and Redo mirror the session's stack, and firing them crosses it")
    func undoPairCrossesTheSessionsStack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let (manager, provider) = makeUndo()
        let specs = boardSpecs(store: store, undo: manager)
        let undo = try #require(specs.spec(.boardUndo))
        let redo = try #require(specs.spec(.boardRedo))

        #expect(!undo.isEnabled, "an empty stack dims it")
        #expect(!redo.isEnabled)

        var crossings: [String] = []
        provider.register(countingStep("Move 3 Cards") { crossings.append("undo") })

        #expect(undo.isEnabled, "a step on the stack lights it")
        #expect(!redo.isEnabled, "and nothing has been crossed yet")

        undo.activate()
        #expect(crossings == ["undo"], "firing the item crosses the board's own stack")
        #expect(!undo.isEnabled, "the stack is empty again")
        #expect(redo.isEnabled, "and the crossed step is on the other one")

        redo.activate()
        #expect(crossings == ["undo", "undo"], "the synthetic step records both halves the same way")
        #expect(undo.isEnabled)
    }

    /// The read-only lock disables the pair with every other mutating command, and it does it in the
    /// one place it is decided — `BoardUndoManager.isReadOnly` (13-native-undo.md ▸ Rules ▸ locks;
    /// 02-architecture.md § "The lock's scope"). The stack is untouched, which is why the items come
    /// back when it clears.
    @Test("The read-only lock dims the pair, steps and all")
    func theLockDimsThePair() throws {
        final class Lock { var isOn = false }
        let lock = Lock()
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let (manager, provider) = makeUndo(isReadOnly: { lock.isOn })
        provider.register(countingStep("Move Card") {})
        provider.register(countingStep("Rename Lane") {})
        provider.undo()

        let specs = boardSpecs(store: store, undo: manager)
        let undo = try #require(specs.spec(.boardUndo))
        let redo = try #require(specs.spec(.boardRedo))
        #expect(undo.isEnabled)
        #expect(redo.isEnabled)

        lock.isOn = true
        #expect(!undo.isEnabled, "disabled with every other mutating command")
        #expect(!redo.isEnabled)
        #expect(provider.canUndo, "an enablement answer, not a clearing — the stack survives")

        lock.isOn = false
        #expect(undo.isEnabled, "and resumes when the lock clears")
        #expect(redo.isEnabled)
    }

    /// A window whose session has gone hands the catalog `nil`, and the pair reads that as an empty
    /// stack rather than as an error — the same quiet the adapter gives a board with no provider
    /// (`BoardUndoManager.history`).
    @Test("With no stack in front the pair is dim, and firing it does nothing")
    func theUndoPairAbstainsWithNoStack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let specs = boardSpecs(store: store, undo: nil)

        for identifier in [NSToolbarItem.Identifier.boardUndo, .boardRedo] {
            let spec = try #require(specs.spec(identifier))
            #expect(!spec.isEnabled)
            spec.activate()
        }
    }

    @Test("Every item's symbol resolves on this system")
    func symbolsResolve() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        for spec in boardSpecs(store: store) {
            guard let symbol = spec.symbol else { continue }
            #expect(ItemSymbol.exists(symbol), "\(spec.label) draws a symbol this OS does not have")
        }
    }

    @Test("New Card mirrors its menu row: no lanes, no target, no item")
    func newCardMirrorsItsRow() throws {
        let populated = try makeBoard()
        defer { populated.tearDown() }
        let empty = try makeEmptyBoard()
        defer { empty.tearDown() }

        let store = try BoardStore(rootURL: populated.root)
        let specs = boardSpecs(store: store)
        let newCard = try #require(specs.spec(.boardNewCard))
        #expect(newCard.isEnabled)
        #expect(newCard.isOn == nil, "New Card is a push button, not a toggle")

        let emptyStore = try BoardStore(rootURL: empty.root)
        let emptySpecs = boardSpecs(store: emptyStore)
        let disabled = try #require(emptySpecs.spec(.boardNewCard))
        #expect(!disabled.isEnabled, "the zero-lane board disables the row, so it disables the item")
        #expect(emptyStore.newCardTarget == nil, "one predicate, read by both")
    }

    @Test("The toolbar is customizable, and every catalog item actually builds")
    func everyItemBuilds() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let search = BoardSearchPresentation()
        let controller = BoardToolbar.controller(store: store, search: search, zoom: makeZoom(), appearance: makeAppearance(), session: DragSession(), undo: nil)

        // 03's three customization sentences: "right-click ▸ Customize Toolbar…, drag to rearrange,
        // system overflow and icon/text display options".
        #expect(controller.toolbar.allowsUserCustomization)
        #expect(controller.toolbar.allowsDisplayModeCustomization)
        #expect(controller.toolbar.autosavesConfiguration, "the arrangement is the user's, and it keeps")

        let allowed = controller.toolbarAllowedItemIdentifiers(controller.toolbar)
        #expect(allowed.contains(.flexibleSpace) && allowed.contains(.space), "the palette's spacers")
        #expect(controller.toolbarDefaultItemIdentifiers(controller.toolbar) == BoardToolbar.defaultItems)

        for spec in boardSpecs(store: store) {
            let item = try #require(controller.toolbar(
                controller.toolbar,
                itemForItemIdentifier: spec.identifier,
                willBeInsertedIntoToolbar: true
            ))
            #expect(item.label == spec.label)
            #expect(item.paletteLabel == spec.label, "one vocabulary, in the palette too")
        }
    }

    @Test("The toolbar's search item is the field's home; the palette's copy is inert")
    func searchItemOwnsTheField() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let search = BoardSearchPresentation()
        let controller = BoardToolbar.controller(store: store, search: search, zoom: makeZoom(), appearance: makeAppearance(), session: DragSession(), undo: nil)

        #expect(search.focusField == nil, "nothing to focus until the item exists")

        // `NSSearchToolbarItem.view` is unavailable — the item owns its layout — so the field is
        // reached through `searchField`, which is also where it is handed in.
        let palette = try #require(controller.toolbar(
            controller.toolbar,
            itemForItemIdentifier: .boardSearch,
            willBeInsertedIntoToolbar: false
        ) as? NSSearchToolbarItem)
        #expect(!palette.searchField.isEnabled, "the palette's copy is a picture, not a second field")
        #expect(search.focusField == nil, "a palette copy must not claim ⌘F's handle")

        let installed = try #require(controller.toolbar(
            controller.toolbar,
            itemForItemIdentifier: .boardSearch,
            willBeInsertedIntoToolbar: true
        ) as? NSSearchToolbarItem)
        let field = installed.searchField
        #expect(field.isEnabled, "the real one takes typing")
        #expect(search.focusField != nil, "the installed item is the field's home")

        // The live field writes through per keystroke, which is the m5 contract this milestone
        // moved rather than changed (`BoardSearchFieldController`).
        field.stringValue = "spec"
        field.delegate?.controlTextDidChange?(Notification(name: NSControl.textDidChangeNotification, object: field))
        #expect(store.searchQuery == "spec")
    }

    @Test("The search item is AppKit's own, configured for grow-on-focus and a staged Escape")
    func searchItemIsTheStockOne() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let controller = BoardToolbar.controller(store: store, search: BoardSearchPresentation(), zoom: makeZoom(), appearance: makeAppearance(), session: DragSession(), undo: nil)

        let item = try #require(controller.toolbar(
            controller.toolbar,
            itemForItemIdentifier: .boardSearch,
            willBeInsertedIntoToolbar: true
        ) as? NSSearchToolbarItem)

        // The em-based figure is the *focused* width: `NSSearchToolbarItem` applies its preferred
        // width "whenever it gets the keyboard focus", and the resting width is the item's own
        // (10-accessibility.md ▸ Text scaling: the number is characters, never points).
        #expect(
            item.preferredWidthForSearchField
                == BoardMetrics.em(17, bodyPointSize: BoardMetrics.bodyPointSize)
        )
        // **Escape is staged** (04-interactions.md ▸ Search, settled: "in a non-empty field it
        // clears the query, focus staying in the field"). AppKit's default cancel button clears
        // *and* resigns, which would collapse that first step into the second.
        #expect(!item.resignsFirstResponderWithCancel)

        // The overflow row is the item's, and assigning here — `nil` included — destroys it. A
        // custom-view item had to supply one; this one must not.
        #expect(item.menuFormRepresentation != nil, "the item ships a live overflow row")
        // Its own priority already sits above `.high`, so the nudge a custom-view item needed
        // would be a demotion.
        #expect(item.visibilityPriority.rawValue > NSToolbarItem.VisibilityPriority.high.rawValue)
    }

    @Test("⌘F answers false for a field no window can give the keyboard to")
    func focusHandleAnswersForAnUnrootedField() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let search = BoardSearchPresentation()
        let controller = BoardToolbar.controller(store: store, search: search, zoom: makeZoom(), appearance: makeAppearance(), session: DragSession(), undo: nil)

        _ = controller.toolbar(
            controller.toolbar,
            itemForItemIdentifier: .boardSearch,
            willBeInsertedIntoToolbar: true
        )

        // The item exists, so the handle does. Nothing has put it in a window — the same shape the
        // system overflow leaves the field in — so ⌘F must fall through to the transient strip
        // rather than claim a focus it did not get (`BoardSearchPresentation.focusField`).
        let focusField = try #require(search.focusField)
        #expect(focusField() == false)

        search.invokeSearch()
        #expect(search.isTransient, "⌘F always summons search")
    }

    @Test("Show Trash is a toggle whose state is the View menu's checkmark")
    func showTrashTogglesTheQuasiLane() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let specs = boardSpecs(store: store)
        let showTrash = try #require(specs.spec(.boardShowTrash))

        #expect(showTrash.isOn == false, "hidden by default, like the menu row's checkmark")

        showTrash.activate()
        #expect(store.transient.isTrashVisible, "the item drives the row's own setter")
        #expect(showTrash.isOn == true)

        showTrash.activate()
        #expect(!store.transient.isTrashVisible)
        #expect(showTrash.isOn == false)
    }

    /// The picker's whole state contract, `showTrashTogglesTheQuasiLane`'s shape turned toward
    /// `.picker`: no single `isOn`, so the checkmark and the write path are the item's own
    /// `selected`/`select` closures rather than `.isOn`/`.activate()`.
    @Test("Appearance is a picker whose selected row tracks the store and whose rows write it")
    func appearanceTracksTheStore() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let appearance = makeAppearance()
        let specs = boardSpecs(store: store, appearance: appearance)
        let item = try #require(specs.spec(.boardAppearance))

        #expect(item.isOn == nil, "a picker has no single on-state")
        #expect(item.isEnabled, "always enabled — no board state gates an appearance override")

        guard case let .picker(options, selected, select) = item.behavior else {
            Issue.record("Appearance is not a picker")
            return
        }
        #expect(options.map(\.title) == ["Auto", "Light", "Dark"])
        #expect(selected() == 0, "Auto by default, like the store's own nil override")

        select(1)
        #expect(appearance.override == .light, "the row drives the store's own setter")
        #expect(selected() == 1)

        select(2)
        #expect(appearance.override == .dark)
        #expect(selected() == 2)

        select(0)
        #expect(appearance.override == nil)
        #expect(selected() == 0)

        // `.activate()` is a no-op for a picker — firing lives in the dropdown's own rows.
        item.activate()
        #expect(appearance.override == nil, "activate() does not move the selection")
    }
}

// MARK: - The card window's toolbar

/// The card toolbar's six items: the trio's two state clauses (Edit Body's on-state and its
/// raw-source disable, Add Attachment staying live in every mode), Show Sidebar, and — since the
/// sidebar's Actions section retired (Pipeline card bcd3b323) — Delete Card and Reveal in Finder.
@MainActor
@Suite("Toolbar ▸ the card window")
struct CardToolbarTests {

    /// The four window-scoped handles a card window's toolbar reads, wired as the host wires them.
    private func makeHandles() -> (CardBodyPresentation, CardRawSourceSession, CardAttachments, CardWindowActions) {
        let body = CardBodyPresentation()
        let raw = CardRawSourceSession()
        raw.read = { .read("---\nschema: 1\norder: 1\n---\nbody\n") }
        raw.apply = { _ in .applied }
        let attachments = CardAttachments()
        attachments.isEditable = true
        attachments.cardFolder = URL(filePath: "/tmp/board/lane/card")
        let actions = CardWindowActions()
        actions.isDeletable = true
        return (body, raw, attachments, actions)
    }

    @Test("The default set is the trio, Delete Card, and Show Sidebar — separated by a flexible space")
    func defaultsAreSeparatedFromDeleteCard() {
        // "Card window default: Edit Body · Raw Source · Add Attachment" joined by Delete Card (after
        // the sidebar Actions section retired), then Show Sidebar (the trailing-sidebar toggle, at the
        // rightmost position per macOS convention), with `.flexibleSpace` between the creation-side
        // defaults and the destructive/sidebar-control cluster — `BoardToolbar.defaultItems`' own
        // leading-spacer pattern, turned trailing here.
        #expect(CardToolbar.defaultItems == [
            .cardEditBody, .cardRawSource, .cardAddAttachment, .flexibleSpace, .cardDeleteCard, .cardShowSidebar,
        ])
        #expect(CardToolbar.defaultItems.filter { $0 != .flexibleSpace } == [
            .cardEditBody, .cardRawSource, .cardAddAttachment, .cardDeleteCard, .cardShowSidebar,
        ])
    }

    @Test("Reveal in Finder is catalog-only — it already has a menu row with no default chord")
    func revealInFinderIsCatalogOnly() {
        #expect(!CardToolbar.defaultItems.contains(.cardRevealInFinder))
    }

    @Test("The catalog is the six items, in this file's order, labeled off their menu titles")
    func catalogIsTheSixItems() {
        let (body, raw, attachments, actions) = makeHandles()
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)

        #expect(specs.map(\.identifier) == [
            .cardEditBody, .cardRawSource, .cardAddAttachment, .cardShowSidebar, .cardDeleteCard, .cardRevealInFinder,
        ])
        #expect(specs.map(\.label) == [
            "Edit Body", "Raw Source", "Add Attachment", "Show Sidebar", "Delete Card", "Reveal in Finder",
        ])
    }

    @Test("Every item's symbol resolves on this system")
    func symbolsResolve() {
        let (body, raw, attachments, actions) = makeHandles()
        for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions) {
            guard let symbol = spec.symbol else { continue }
            #expect(ItemSymbol.exists(symbol), "\(spec.label) draws a symbol this OS does not have")
        }
    }

    @Test("Every catalog item actually builds, and the toolbar is customizable")
    func everyItemBuilds() throws {
        let (body, raw, attachments, actions) = makeHandles()
        let controller = CardToolbar.controller(body: body, rawSource: raw, attachments: attachments, actions: actions)

        #expect(controller.toolbar.allowsUserCustomization)
        #expect(controller.toolbar.allowsDisplayModeCustomization)
        #expect(controller.toolbarDefaultItemIdentifiers(controller.toolbar) == CardToolbar.defaultItems)

        for spec in CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions) {
            let item = try #require(controller.toolbar(
                controller.toolbar,
                itemForItemIdentifier: spec.identifier,
                willBeInsertedIntoToolbar: true
            ))
            #expect(item.label == spec.label)
            #expect(item.paletteLabel == spec.label)
        }
    }

    @Test("Edit Body is a single toggle showing on-state in Edit")
    func editBodyShowsItsMode() {
        let (body, raw, attachments, actions) = makeHandles()
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
        guard let editBody = specs.spec(.cardEditBody) else {
            Issue.record("no Edit Body item")
            return
        }

        #expect(editBody.isOn == false, "Preview is the window's opening mode")

        editBody.activate()
        #expect(body.mode == .edit, "the item drives the same flip the ⌘E row does")
        #expect(editBody.isOn == true)

        editBody.activate()
        #expect(body.mode == .preview)
        #expect(editBody.isOn == false)
    }

    @Test("Raw Source active disables Edit Body — the row's own predicate, mirrored")
    func rawSourceDisablesEditBody() {
        let (body, raw, attachments, actions) = makeHandles()
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
        guard let editBody = specs.spec(.cardEditBody), let rawSource = specs.spec(.cardRawSource) else {
            Issue.record("the card toolbar is missing an item")
            return
        }

        #expect(editBody.isEnabled)
        #expect(rawSource.isOn == false)

        rawSource.activate()

        #expect(raw.isActive, "the item enters source mode exactly as ⌥⌘E does")
        #expect(rawSource.isOn == true, "a toggle showing on-state")
        // "While source mode is active, Edit Body disables (Cancel/Apply own the exits)" — and the
        // predicate is `EditBodyCommand.isEnabled`, handed to the item rather than restated.
        #expect(!editBody.isEnabled)
        #expect(editBody.isEnabled == EditBodyCommand.isEnabled(body: body, rawSource: raw))

        raw.cancel()
        #expect(editBody.isEnabled)
        #expect(rawSource.isOn == false)
    }

    @Test("Add Attachment stays enabled in every mode, including an open raw edit")
    func addAttachmentIsAlwaysAvailable() {
        let (body, raw, attachments, actions) = makeHandles()
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
        guard let addAttachment = specs.spec(.cardAddAttachment) else {
            Issue.record("no Add Attachment item")
            return
        }

        #expect(addAttachment.isEnabled)

        // "Add Attachment stays enabled in every mode — attachment operations never touch
        // `index.md`, so they're safe alongside a raw edit" (03 ▸ Toolbar; 01-storage-format.md
        // makes the same point from the stamping side).
        raw.enter()
        #expect(raw.isActive)
        #expect(addAttachment.isEnabled)

        body.setMode(.edit)
        #expect(addAttachment.isEnabled)

        // The lock is the row's own predicate, and the item inherits it whole.
        attachments.isEditable = false
        #expect(!addAttachment.isEnabled)
        #expect(addAttachment.isEnabled == AddAttachmentCommand.isEnabled(attachments))
    }

    /// **Show Sidebar is always enabled and drives its own injected write path** — the two closures
    /// `CardToolbar.specs` defaults to `AppPreferences.showCardSidebar` / `.setShowCardSidebar`, held
    /// here over a local `Bool` instead so this test never touches the developer's own
    /// `UserDefaults.standard` domain (`CardSessionUndoTests`' quick-style caution, applied to the one
    /// item here with no window-scoped handle to hold a scratch value instead).
    @Test("Show Sidebar toggles its own bit, always enabled, on-state matching the read")
    func showSidebarTogglesItsOwnBit() {
        let (body, raw, attachments, actions) = makeHandles()
        var shown = true
        let specs = CardToolbar.specs(
            body: body,
            rawSource: raw,
            attachments: attachments,
            actions: actions,
            isSidebarShown: { shown },
            setSidebarShown: { shown = $0 }
        )
        guard let showSidebar = specs.spec(.cardShowSidebar) else {
            Issue.record("no Show Sidebar item")
            return
        }

        #expect(showSidebar.isOn == true)
        #expect(showSidebar.isEnabled, "showing or hiding a pane is not a mutation the read-only lock gates")

        showSidebar.activate()
        #expect(shown == false, "the item drives the same bit the injected closures read")
        #expect(showSidebar.isOn == false)

        showSidebar.activate()
        #expect(shown == true)
        #expect(showSidebar.isOn == true)

        // Raw source and the read-only lock are both the *other* items' predicates — Show Sidebar
        // reads neither.
        raw.enter()
        attachments.isEditable = false
        #expect(showSidebar.isEnabled)
    }

    /// **Delete Card is the sidebar Actions button's former write, on a push button** — gated by the
    /// same read-only predicate (`CardWindowActions.isDeletable`, wired from `!store.isReadOnly`
    /// exactly as the sidebar button's `.disabled(store.isReadOnly)` was), and firing calls the same
    /// closure the host wires from `BoardStore.deleteCard(_:)` (`CardWindowHost.configureActions`).
    @Test("Delete Card is a push button, gated by the read-only lock, that fires the wired delete")
    func deleteCardFiresTheWiredDelete() {
        let (body, raw, attachments, actions) = makeHandles()
        var deleted = 0
        actions.deleteCard = { deleted += 1 }
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
        guard let deleteCard = specs.spec(.cardDeleteCard) else {
            Issue.record("no Delete Card item")
            return
        }

        #expect(deleteCard.isOn == nil, "Delete Card is a push button, not a toggle")
        #expect(deleteCard.isEnabled)

        deleteCard.activate()
        #expect(deleted == 1, "the item fires the same write the host wires from BoardStore.deleteCard")

        // The read-only lock is the sidebar button's own predicate, carried over whole.
        actions.isDeletable = false
        #expect(!deleteCard.isEnabled)
        #expect(deleteCard.isEnabled == DeleteCardCommand.isEnabled(actions))
    }

    /// **Reveal in Finder computes exactly what `RevealInFinderCommand`'s card-window branch does** —
    /// same function (`CardAttachments.revealURLs`), so the two surfaces can never disagree. Firing it
    /// is not exercised here, `addAttachmentIsAlwaysAvailable`'s own restraint: both open a real
    /// system surface (a panel there, Finder here), which a unit test does not drive.
    @Test("Reveal in Finder is enabled exactly when the row's own computation finds something to reveal")
    func revealInFinderMirrorsTheRowsComputation() {
        let (body, raw, attachments, actions) = makeHandles()
        let specs = CardToolbar.specs(body: body, rawSource: raw, attachments: attachments, actions: actions)
        guard let reveal = specs.spec(.cardRevealInFinder) else {
            Issue.record("no Reveal in Finder item")
            return
        }

        // `makeHandles()` gives the section a folder and no focus — the card-folder branch.
        #expect(reveal.isEnabled)
        #expect(!CardAttachments.revealURLs(
            cardFolder: attachments.cardFolder, selectedURL: attachments.selectedURL, isSectionFocused: attachments.isFocused
        ).isEmpty)

        // A window on its way out — no folder, nothing to reveal — is the row's own disabled clause.
        attachments.cardFolder = nil
        #expect(!reveal.isEnabled)
    }
}

// MARK: - The toolbar survives a stale dismantle

/// **The malformed-titlebar bug**: toggling a card window between Edit and Raw Source could leave it
/// with no toolbar at all — and losing the toolbar is what collapses AppKit's two-line title-and-
/// subtitle down to its single combined line, the "⟨title⟩ — ⟨board⟩ › ⟨lane⟩" strip a user reported
/// seeing after exactly that toggle.
///
/// The root cause lived in `HostedWindowController.attach`/`detach`, not in the card window's own
/// chrome code: `WindowAccessor`'s own doc comment already recorded that SwiftUI "dismantles and
/// re-makes the background representable" on macOS 26, and a content swap deep in the window's tree
/// (raw source replacing the whole content area is the one this traces to) can trigger exactly that —
/// but the dismantle for the *old* representable and the attach for the *new* one were never
/// guaranteed to arrive in the order they logically pair in. When the stale dismantle landed **after**
/// the fresh attach, an identity-blind `detach()` tore the toolbar (and the titlebar accessory, and the
/// delegate proxy) right back off a window a newer attach had just finished configuring — silently,
/// with nothing to put it back until something else attached again.
///
/// `attach(to:through:)`/`detach(through:)` fix this by tracking *which* `WindowAccessor` view is the
/// current owner and refusing a detach for any other — see `HostedWindowController.attachedThroughView`.
/// These tests drive that seam directly, standing in for the two views SwiftUI would otherwise recreate.
@MainActor
@Suite("Toolbar ▸ survives a stale dismantle")
struct ToolbarStaleDismantleTests {

    private func makeWindow() -> NSWindow {
        NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: true
        )
    }

    /// A real, installable card toolbar — the exact shape `CardWindowHost.configureWindow` hands
    /// `installToolbar`, built fresh rather than borrowed from `CardToolbarTests` (`private` there,
    /// on purpose: a struct's own fixtures are not a second suite's to reach into).
    private func makeToolbar() -> WindowToolbarController {
        CardToolbar.controller(
            body: CardBodyPresentation(),
            rawSource: CardRawSourceSession(),
            attachments: CardAttachments(),
            actions: CardWindowActions()
        )
    }

    @Test("A dismantle for a view a newer attach has already superseded does not remove the toolbar")
    func aStaleDismantleIsRefused() {
        let window = makeWindow()
        let controller = HostedWindowController()
        let toolbarController = makeToolbar()
        controller.installToolbar(toolbarController)

        let firstView = NSView()
        let secondView = NSView()

        // The normal-order half: the first mount installs the toolbar.
        controller.attach(to: window, through: firstView)
        #expect(window.toolbar === toolbarController.toolbar)

        // A second `WindowAccessor` instance mounts over the *same* window — the racy content-swap
        // case, standing in for SwiftUI recreating the representable without the window itself
        // changing. It becomes the new owner.
        controller.attach(to: window, through: secondView)
        #expect(window.toolbar === toolbarController.toolbar, "the still-current window keeps its toolbar")

        // The first view's teardown arrives *after* the second view's attach — the ordering
        // `WindowAccessor`'s own doc comment says is not guaranteed. A stale detach for a superseded
        // view must not undo what the fresher attach just did.
        controller.detach(through: firstView)
        #expect(window.toolbar === toolbarController.toolbar, "a stale dismantle must not remove the toolbar")
        #expect(window.delegate === controller, "nor hand the delegate back to SwiftUI's own")
    }

    @Test("The legitimate owner's dismantle still tears the toolbar down")
    func theCurrentOwnersDismantleStillWorks() {
        let window = makeWindow()
        let controller = HostedWindowController()
        let toolbarController = makeToolbar()
        controller.installToolbar(toolbarController)

        let view = NSView()
        controller.attach(to: window, through: view)
        #expect(window.toolbar === toolbarController.toolbar)

        controller.detach(through: view)
        #expect(window.toolbar == nil, "the view that actually owns the attachment can still tear it down")
    }

    @Test("The normal order — detach, then attach — reinstalls the toolbar exactly as before")
    func theOrdinaryOrderStillSelfHeals() {
        let window = makeWindow()
        let controller = HostedWindowController()
        let toolbarController = makeToolbar()
        controller.installToolbar(toolbarController)

        let firstView = NSView()
        controller.attach(to: window, through: firstView)
        controller.detach(through: firstView)
        #expect(window.toolbar == nil)

        let secondView = NSView()
        controller.attach(to: window, through: secondView)
        #expect(window.toolbar === toolbarController.toolbar, "the next attach reinstalls it")
    }

    @Test("A caller that names no view keeps the original, unconditional attach/detach")
    func viewlessCallsAreUnaffected() {
        // `BoardChromeTests` and every other direct caller in this test target attaches and detaches
        // without a view — the shape `attach(to:)`/`detach()` always had. That pair must keep working
        // exactly as before: the view-identity guard only ever engages when both sides name one.
        let window = makeWindow()
        let controller = HostedWindowController()
        let toolbarController = makeToolbar()
        controller.installToolbar(toolbarController)

        controller.attach(to: window)
        #expect(window.toolbar === toolbarController.toolbar)

        controller.detach()
        #expect(window.toolbar == nil)
    }
}

// MARK: - ⌘F and the search field's two homes

/// "⌘F always summons search: with the field removed from the toolbar, invoking it surfaces the
/// field transiently until the search clears" (03-board-ui.md ▸ Toolbar).
///
/// The decision and the dismissal are pure functions on `BoardSearchPresentation` precisely so this
/// clause is testable without a toolbar, a window, or a first responder — none of which a unit test
/// can stand up honestly.
@MainActor
@Suite("Toolbar ▸ ⌘F's transient fallback")
struct BoardSearchSurfacingTests {

    @Test("Installed, ⌘F focuses the toolbar's field; removed, it surfaces the transient one")
    func invocationFollowsTheField() {
        #expect(
            BoardSearchPresentation.invocation(isInstalledInToolbar: true, isTransient: false)
                == .focusToolbarField
        )
        #expect(
            BoardSearchPresentation.invocation(isInstalledInToolbar: true, isTransient: true)
                == .focusToolbarField,
            "the item is the field's home whenever it exists"
        )
        #expect(
            BoardSearchPresentation.invocation(isInstalledInToolbar: false, isTransient: false)
                == .surfaceTransiently
        )
        #expect(
            BoardSearchPresentation.invocation(isInstalledInToolbar: false, isTransient: true)
                == .focusTransientField,
            "a second ⌘F re-focuses rather than re-surfacing"
        )
    }

    @Test("The transient surface lasts until the search clears — and never mid-edit")
    func transientLifetime() {
        // A query still filtering the board keeps it, focused or not: Tab is the keep-filter path,
        // and the field has to stay reachable while the filter stands (04 § Search).
        #expect(BoardSearchPresentation.transientPersists(query: "spec", isFocused: false))
        #expect(BoardSearchPresentation.transientPersists(query: "spec", isFocused: true))
        // An emptied field the user is still typing in keeps it too — otherwise deleting back to
        // nothing would yank the field out from under the caret.
        #expect(BoardSearchPresentation.transientPersists(query: "", isFocused: true))
        // Cleared and unfocused: the search is over.
        #expect(!BoardSearchPresentation.transientPersists(query: "", isFocused: false))
    }

    @Test("With the item installed, ⌘F focuses it and raises nothing")
    func installedFieldIsFocusedInPlace() {
        let presentation = BoardSearchPresentation()
        var focused = 0
        presentation.focusField = {
            focused += 1
            return true
        }

        presentation.invokeSearch()

        #expect(focused == 1)
        #expect(!presentation.isTransient, "the titlebar stays the field's home")
    }

    @Test("With the item removed, ⌘F raises the strip and the arriving field claims the keyboard")
    func removedFieldSurfacesTransiently() {
        let presentation = BoardSearchPresentation()
        presentation.isInstalledInToolbar = false
        presentation.focusField = {
            Issue.record("the toolbar has no field to focus")
            return false
        }

        presentation.invokeSearch()

        #expect(presentation.isTransient)
        // The field does not exist yet — the host renders it on the next update — so the focus is a
        // claim the field takes as it appears, exactly once.
        #expect(presentation.consumeFocusOnAppear())
        #expect(!presentation.consumeFocusOnAppear())

        // Now it exists: a second ⌘F focuses it in place.
        var focused = 0
        presentation.focusTransientField = { focused += 1 }
        presentation.invokeSearch()
        #expect(focused == 1)
        #expect(presentation.isTransient)
    }

    @Test("The strip goes when the search clears, and stays while it has not")
    func transientDismissal() {
        let presentation = BoardSearchPresentation()
        presentation.isInstalledInToolbar = false
        presentation.invokeSearch()
        presentation.focusTransientField = {}
        #expect(presentation.isTransient)

        presentation.isFocused = true
        presentation.dismissTransientIfCleared(query: "")
        #expect(presentation.isTransient, "the keyboard is still in it")

        presentation.isFocused = false
        presentation.dismissTransientIfCleared(query: "spec")
        #expect(presentation.isTransient, "the filter is still standing")

        presentation.dismissTransientIfCleared(query: "")
        #expect(!presentation.isTransient)
        #expect(presentation.focusTransientField == nil, "the handle goes with the field")
    }

    @Test("An installed field that cannot take the keyboard falls back to the strip")
    func overflowedFieldFallsBackToTheStrip() {
        // The item is installed but in the system overflow, where it is in no window and cannot
        // become first responder. "⌘F always summons search", so the strip stands in — an item the
        // user cannot type into is a removal by another name.
        let presentation = BoardSearchPresentation()
        presentation.focusField = { false }

        presentation.invokeSearch()

        #expect(presentation.isTransient)
        #expect(presentation.consumeFocusOnAppear())
    }

    @Test("A field in the toolbar has no transient surface to dismiss")
    func installedFieldIgnoresDismissal() {
        let presentation = BoardSearchPresentation()
        #expect(presentation.isInstalledInToolbar, "the shipped default")
        presentation.focusField = { true }

        presentation.invokeSearch()
        presentation.dismissTransientIfCleared(query: "")

        #expect(!presentation.isTransient)
    }
}

// MARK: - The field's two intercepted keys

/// An `NSSearchToolbarItem` that counts the two interactions the field drives — the only way to see
/// grow-on-focus and its undo without a window, since both are AppKit's own animation underneath.
private final class RecordingSearchItem: NSSearchToolbarItem {

    var interactionsBegun = 0
    var interactionsEnded = 0

    override func beginSearchInteraction() {
        interactionsBegun += 1
        super.beginSearchInteraction()
    }

    override func endSearchInteraction() {
        interactionsEnded += 1
        super.endSearchInteraction()
    }
}

/// **Escape is staged and Return is swallowed** (04-interactions.md ▸ Search, settled) — the two keys
/// the field gives meanings of its own, and the only two: "every key with the field focused acts on
/// the field — stock `NSSearchField` behavior, no pass-throughs".
@MainActor
@Suite("Toolbar ▸ the search field's two keys")
struct BoardSearchFieldKeyTests {

    /// The field as its toolbar home builds it, adopted by an item that records what it is asked to
    /// do — `BoardToolbar`'s wiring, with the item swapped for one that can be read.
    private func makeToolbarField(
        store: BoardStore,
        presentation: BoardSearchPresentation
    ) -> (NSSearchField, RecordingSearchItem) {
        let field = BoardSearchFieldController.makeField(
            store: store,
            presentation: presentation,
            home: .toolbar
        )
        let item = RecordingSearchItem(itemIdentifier: .boardSearch)
        item.searchField = field
        BoardSearchFieldController.adopt(item, presentation: presentation)
        return (field, item)
    }

    private func send(_ selector: Selector, to field: NSSearchField) -> Bool? {
        field.delegate?.control?(field, textView: NSTextView(), doCommandBy: selector)
    }

    @Test("A non-empty field takes Escape as a clear, and keeps the keyboard")
    func escapeClearsBeforeItLeaves() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let presentation = BoardSearchPresentation()
        var handedBack = 0
        presentation.focusBoard = { handedBack += 1 }
        let (field, item) = makeToolbarField(store: store, presentation: presentation)

        field.stringValue = "spec"
        field.delegate?.controlTextDidChange?(
            Notification(name: NSControl.textDidChangeNotification, object: field)
        )
        #expect(store.searchQuery == "spec")

        #expect(send(#selector(NSResponder.cancelOperation(_:)), to: field) == true)

        // "In a non-empty field it clears the query, focus staying in the field."
        #expect(store.searchQuery.isEmpty)
        #expect(field.stringValue.isEmpty)
        #expect(handedBack == 0, "one press, one layer")
        #expect(item.interactionsEnded == 0, "the field keeps the keyboard, so it keeps its width")
    }

    @Test("An empty field takes Escape as an exit — the board gets the keyboard, the field its width")
    func escapeHandsTheKeyboardBack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let presentation = BoardSearchPresentation()
        var handedBack = 0
        presentation.focusBoard = { handedBack += 1 }
        let (field, item) = makeToolbarField(store: store, presentation: presentation)

        #expect(send(#selector(NSResponder.cancelOperation(_:)), to: field) == true)

        // "In an empty field it returns focus to the board" — and the grown field settles back with
        // the keyboard it just gave up.
        #expect(handedBack == 1)
        #expect(item.interactionsEnded == 1)
    }

    @Test("The transient home takes the same Escape with no item to collapse")
    func escapeInTheStripHasNoWidthToUndo() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let presentation = BoardSearchPresentation()
        var handedBack = 0
        presentation.focusBoard = { handedBack += 1 }
        let field = BoardSearchFieldController.makeField(
            store: store,
            presentation: presentation,
            home: .transient
        )

        #expect(send(#selector(NSResponder.cancelOperation(_:)), to: field) == true)
        #expect(handedBack == 1, "one implementation of the staircase, whichever home it is in")
    }

    @Test("Return is a swallowed no-op, and every other key falls through to the field editor")
    func returnIsSwallowedAndTheRestPassThrough() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let presentation = BoardSearchPresentation()
        let (field, _) = makeToolbarField(store: store, presentation: presentation)

        field.stringValue = "spec"
        field.delegate?.controlTextDidChange?(
            Notification(name: NSControl.textDidChangeNotification, object: field)
        )

        // "The filter is live, there is nothing to submit — it never reaches the board's
        // rename/create grammar."
        #expect(send(#selector(NSResponder.insertNewline(_:)), to: field) == true)
        #expect(store.searchQuery == "spec", "a swallowed key changes nothing")

        // "Stock NSSearchField behavior, no pass-throughs": the field editor keeps everything else,
        // caret motion and text selection included.
        #expect(send(#selector(NSResponder.moveLeft(_:)), to: field) == false)
        #expect(send(#selector(NSResponder.deleteBackward(_:)), to: field) == false)
        #expect(send(#selector(NSResponder.moveUp(_:)), to: field) == false)
    }

    @Test("⌘F expands and focuses in one call — and claims nothing when the field is in no window")
    func focusHandleIsTheItemsInteraction() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let presentation = BoardSearchPresentation()
        let (_, item) = makeToolbarField(store: store, presentation: presentation)

        let focusField = try #require(presentation.focusField)
        #expect(focusField() == false, "a field in no window cannot take the keyboard")
        #expect(item.interactionsBegun == 0, "and must not report a focus it never took")
    }
}
