import AppKit
import Foundation
import Testing
@testable import Kanban

/// The provider seam and the board stack behind it (12-editions.md ▸ The provider seam;
/// 13-native-undo.md).
///
/// The steps here are **synthetic** on purpose: what this milestone builds is the stack and the
/// seam, and the real inverses arrive at the Writer boundary in the next one. A step that records
/// its own crossing is therefore the exact fixture — it proves the stack's grammar (one gesture one
/// step, undo flips to redo, a stale step falls through) without needing a board to move cards
/// around on.

// MARK: - Synthetic steps

/// Records every crossing, in order, and answers with whatever outcome the test asked for.
@MainActor
private final class StepLog {

    private(set) var crossings: [String] = []

    /// The direction each crossing was told it was — the argument the skip banner's verb comes from.
    private(set) var directions: [HistoryDirection] = []

    /// A step that applies in both directions — the ordinary case.
    func step(_ name: String) -> HistoryStep {
        step(name, undo: .applied, redo: .applied)
    }

    /// A step whose inverse declines as stale — 13's skip, without needing a foreign writer.
    func staleStep(_ name: String) -> HistoryStep {
        step(name, undo: .skipped, redo: .applied)
    }

    /// A step whose inverse could not be written — the disk-error fate, which is not staleness.
    func failingStep(_ name: String) -> HistoryStep {
        step(name, undo: .failed, redo: .applied)
    }

    func step(_ name: String, undo: HistoryStepOutcome, redo: HistoryStepOutcome) -> HistoryStep {
        HistoryStep(
            name: name,
            undo: { [weak self] direction in
                self?.crossings.append("undo \(name)")
                self?.directions.append(direction)
                return undo
            },
            redo: { [weak self] direction in
                self?.crossings.append("redo \(name)")
                self?.directions.append(direction)
                return redo
            }
        )
    }
}

// MARK: - The native stack

@MainActor
@Suite("History ▸ the native stack")
struct NativeHistoryProviderTests {

    @Test("A fresh provider has nothing to cross and nothing to say about it")
    func emptyStack() {
        let provider = NativeHistoryProvider()
        #expect(provider.canUndo == false)
        #expect(provider.canRedo == false)
        #expect(provider.undoActionName == nil)
        #expect(provider.redoActionName == nil)
    }

    @Test("Registering a step arms Undo and names it — and runs nothing")
    func registerArmsUndo() {
        let log = StepLog()
        let provider = NativeHistoryProvider()

        provider.register(log.step("Move Card"))

        #expect(provider.canUndo)
        #expect(provider.canRedo == false)
        #expect(provider.undoActionName == "Move Card")
        #expect(provider.redoActionName == nil)
        #expect(log.crossings.isEmpty, "registration is not application")
    }

    @Test("Undo runs the inverse once and flips the step onto Redo, keeping its name")
    func undoRunsTheInverseAndFlipsToRedo() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move 3 Cards"))

        provider.undo()

        #expect(log.crossings == ["undo Move 3 Cards"])
        #expect(provider.canUndo == false)
        #expect(provider.canRedo)
        // The phrase names the gesture, not the direction — "Undo Move 3 Cards" becomes
        // "Redo Move 3 Cards".
        #expect(provider.redoActionName == "Move 3 Cards")
        #expect(provider.undoActionName == nil)
    }

    @Test("Redo replays the write and arms Undo again — the classic dance, both ways")
    func redoReplaysAndFlipsBack() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Rename Lane"))

        provider.undo()
        provider.redo()

        #expect(log.crossings == ["undo Rename Lane", "redo Rename Lane"])
        #expect(provider.canUndo)
        #expect(provider.canRedo == false)
        #expect(provider.undoActionName == "Rename Lane")

        provider.undo()
        #expect(log.crossings == ["undo Rename Lane", "redo Rename Lane", "undo Rename Lane"])
    }

    @Test("One register call is one step — two registrations are two crossings, newest first")
    func oneRegistrationIsOneStep() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        // Back to back, in one turn of the run loop: `groupsByEvent` must not fold these into one.
        provider.register(log.step("Move Card"))
        provider.register(log.step("Rename Card"))

        #expect(provider.undoActionName == "Rename Card")
        provider.undo()
        #expect(log.crossings == ["undo Rename Card"])
        #expect(provider.undoActionName == "Move Card")

        provider.undo()
        #expect(log.crossings == ["undo Rename Card", "undo Move Card"])
        #expect(provider.canUndo == false)
    }

    @Test("A new step clears the redo stack — classic behaviour")
    func registeringClearsRedo() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move Card"))
        provider.undo()
        #expect(provider.canRedo)

        provider.register(log.step("Delete Card"))

        #expect(provider.canRedo == false)
        #expect(provider.redoActionName == nil)
        #expect(provider.undoActionName == "Delete Card")
    }

    @Test("Undo on an empty stack does nothing at all")
    func undoOnAnEmptyStackIsInert() {
        let log = StepLog()
        let provider = NativeHistoryProvider()

        provider.undo()
        provider.redo()

        #expect(log.crossings.isEmpty)
        #expect(provider.canUndo == false)
        #expect(provider.canRedo == false)
    }

    @Test("A stale step is skipped, not applied — and ⌘Z falls through to the next one")
    func aStaleStepIsSkippedAndFallsThrough() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move Card"))
        provider.register(log.staleStep("Rename Card"))

        provider.undo()

        // Both were reached in one ⌘Z: the stale one declined and was dropped, the next one applied.
        #expect(log.crossings == ["undo Rename Card", "undo Move Card"])
        #expect(provider.canUndo == false)
        // Only the step that actually ran is redoable — a skipped step leaves nothing behind.
        #expect(provider.canRedo)
        #expect(provider.redoActionName == "Move Card")
    }

    @Test("A step whose write failed stays on the stack, and the crossing stops there")
    func aFailedStepStaysAndStopsTheCrossing() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move Card"))
        provider.register(log.failingStep("Rename Card"))

        provider.undo()

        // It was reached and it declined — and unlike a stale step it is still there to retry, with
        // the step below it untouched underneath.
        #expect(log.crossings == ["undo Rename Card"], "no fall-through: a refused disk is not a reason to try more")
        #expect(provider.canUndo)
        #expect(provider.undoActionName == "Rename Card")
        #expect(provider.canRedo == false, "nothing landed, so nothing is redoable")

        provider.undo()
        #expect(log.crossings == ["undo Rename Card", "undo Rename Card"], "⌘Z can retry it")
    }

    @Test("A failed redo leaves the redo stack alone too")
    func aFailedRedoStays() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move Card", undo: .applied, redo: .failed))

        provider.undo()
        provider.redo()

        #expect(provider.canRedo, "still there to retry")
        #expect(provider.redoActionName == "Move Card")
        #expect(provider.canUndo == false)
    }

    @Test("A step is told which command it is being crossed by, not which half is running")
    func stepsAreToldTheDirection() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Rename Card"))

        provider.undo()
        provider.redo()
        provider.undo()

        // The third crossing runs the *undo* half again, and the second runs the half registered as
        // `redo` — what each is told is ⌘Z, ⇧⌘Z, ⌘Z, which is what the skip banner has to say.
        #expect(log.directions == [.undo, .redo, .undo])
    }

    @Test("A stack of nothing but stale steps empties itself and stops")
    func anEntirelyStaleStackEmptiesItself() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.staleStep("Move Card"))
        provider.register(log.staleStep("Rename Card"))

        provider.undo()

        #expect(log.crossings == ["undo Rename Card", "undo Move Card"])
        #expect(provider.canUndo == false)
        #expect(provider.canRedo == false)
    }

    @Test("Clearing drops both directions — the session-only rule's one call")
    func clearEmptiesBothStacks() {
        let log = StepLog()
        let provider = NativeHistoryProvider()
        provider.register(log.step("Move Card"))
        provider.undo()
        provider.register(log.step("Delete Card"))

        provider.clear()

        #expect(provider.canUndo == false)
        #expect(provider.canRedo == false)
        #expect(provider.undoActionName == nil)
        #expect(provider.redoActionName == nil)
        provider.undo()
        #expect(log.crossings == ["undo Move Card"], "nothing crossed after the clear")
    }

    @Test("Two providers are two stacks — undo is board-local by construction")
    func providersAreIndependent() {
        let log = StepLog()
        let one = NativeHistoryProvider()
        let other = NativeHistoryProvider()

        one.register(log.step("Move Card"))

        #expect(one.canUndo)
        #expect(other.canUndo == false)

        other.undo()
        #expect(log.crossings.isEmpty, "the other board's ⌘Z crosses nothing of this board's")
        #expect(one.canUndo)
    }
}

// MARK: - The AppKit adapter

/// A provider with no `NSUndoManager` anywhere in it — which is the point: `BoardUndoManager` is
/// tested against *this* rather than against the native stack, because what has to be true is that
/// the adapter works for any implementation of the seam (Pro's git provider binds the same protocol
/// in pro-m1).
@MainActor
private final class FakeHistoryProvider: HistoryProviding {

    var canUndo = false
    var canRedo = false
    var undoActionName: String?
    var redoActionName: String?
    private(set) var registered: [String] = []
    private(set) var undoCount = 0
    private(set) var redoCount = 0
    private(set) var clearCount = 0

    func register(_ step: HistoryStep) { registered.append(step.name) }
    func undo() { undoCount += 1 }
    func redo() { redoCount += 1 }
    func clear() { clearCount += 1 }
}

@MainActor
@Suite("History ▸ the AppKit adapter")
struct BoardUndoManagerTests {

    @Test("Enablement is the provider's answer, not a stack of the adapter's own")
    func enablementMirrorsTheProvider() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)

        #expect(manager.canUndo == false)
        #expect(manager.canRedo == false)

        provider.canUndo = true
        provider.canRedo = true

        #expect(manager.canUndo)
        #expect(manager.canRedo)
    }

    @Test("The menu titles are the platform's composition over the step's own phrase")
    func menuTitlesComposeFromTheStepName() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)

        // Nothing to cross: the bare verb, with no trailing space where the name would go.
        #expect(manager.undoMenuItemTitle == "Undo")
        #expect(manager.redoMenuItemTitle == "Redo")

        provider.canUndo = true
        provider.undoActionName = "Move 3 Cards"
        provider.canRedo = true
        provider.redoActionName = "Rename Lane"

        #expect(manager.undoActionName == "Move 3 Cards")
        #expect(manager.undoMenuItemTitle == "Undo Move 3 Cards")
        #expect(manager.redoMenuItemTitle == "Redo Rename Lane")
    }

    @Test("A read-only board disables both directions, whatever the stack holds")
    func theLockDisablesEnablement() {
        final class Lock { var isOn = false }
        let lock = Lock()
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider, isReadOnly: { lock.isOn })
        provider.canUndo = true
        provider.canRedo = true
        #expect(manager.canUndo)

        lock.isOn = true

        #expect(manager.canUndo == false, "disabled with every other mutating command")
        #expect(manager.canRedo == false)
        #expect(provider.canUndo, "an enablement answer, not a clearing — the stack survives")

        lock.isOn = false
        #expect(manager.canUndo, "and resumes when the lock clears")
    }

    @Test("Crossing forwards to the provider — what ⌘Z and the toolbar item actually reach")
    func crossingForwards() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)

        manager.undo()
        manager.undo()
        manager.redo()

        #expect(provider.undoCount == 2)
        #expect(provider.redoCount == 1)
    }

    @Test("A stray registration into the adapter can never be crossed or shown")
    func theAdaptersOwnStackStaysInert() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        let sink = FakeHistoryProvider()

        manager.registerUndo(withTarget: sink) { $0.canUndo = true }

        #expect(manager.canUndo == false, "the provider is the only source of truth")
        #expect(manager.undoMenuItemTitle == "Undo")
        manager.undo()
        #expect(sink.canUndo == false, "the stray action was never run")
        #expect(provider.undoCount == 1)
    }

    @Test("Clearing the adapter's own stack leaves the board's alone")
    func removeAllActionsDoesNotClearTheBoard() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        provider.canUndo = true

        manager.removeAllActions()

        #expect(provider.clearCount == 0)
        #expect(manager.canUndo)
    }
}

// MARK: - Routing

@MainActor
@Suite("History ▸ undo routing")
struct BoardUndoRoutingTests {

    @Test("A text view — field editors included — is a text-editing surface; a plain view is not")
    func textEditingClassification() {
        #expect(BoardUndoRouting.isTextEditing(NSTextView()))
        #expect(BoardUndoRouting.isTextEditing(NSTextField()) == false, "focused, not yet editing")
        #expect(BoardUndoRouting.isTextEditing(NSView()) == false)
        #expect(BoardUndoRouting.isTextEditing(NSWindow()) == false)
        #expect(BoardUndoRouting.isTextEditing(nil) == false)
    }

    @Test("With focus outside every text surface, a board window answers with the board's stack")
    func boardFocusAnswersTheBoardStack() {
        let board = BoardUndoManager(history: FakeHistoryProvider())
        let fallback = UndoManager()

        let answer = BoardUndoRouting.undoManager(isTextEditing: false, board: board, textFallback: fallback)

        #expect(answer === board)
    }

    @Test("A focused field editor answers with the window's text manager, never the board's")
    func fieldEditorFocusAnswersTheTextManager() {
        let board = BoardUndoManager(history: FakeHistoryProvider())
        let fallback = UndoManager()

        let answer = BoardUndoRouting.undoManager(isTextEditing: true, board: board, textFallback: fallback)

        #expect(answer === fallback)
    }

    @Test("A window with no board answers with the text manager either way")
    func windowsWithNoBoardFallBack() {
        let fallback = UndoManager()

        #expect(BoardUndoRouting.undoManager(isTextEditing: false, board: nil, textFallback: fallback) === fallback)
        #expect(BoardUndoRouting.undoManager(isTextEditing: true, board: nil, textFallback: fallback) === fallback)
    }
}

// MARK: - The session that owns the stack

/// A board on disk, and an `AppModel` whose registry file is in temp rather than in the test host's
/// real Application Support directory — `AppModelTests`' two helpers, in the shape this suite needs.
@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"))
    return fixture
}

@MainActor
private func makeModel() throws -> (model: AppModel, tearDown: () -> Void) {
    let folder = FileManager.default.temporaryDirectory
        .appendingPathComponent("HistoryProviderTests-\(UUID().uuidString)", isDirectory: true)
    try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
    let model = AppModel(
        registryStorageURL: folder.appendingPathComponent("board-registry.json"),
        clipboardStagingRoot: folder.appendingPathComponent("Clipboard", isDirectory: true)
    )
    return (model, { try? FileManager.default.removeItem(at: folder) })
}

@MainActor
@discardableResult
private func openBoard(_ model: AppModel, at url: URL) throws -> BoardWindowRef {
    let ref = BoardWindowRef(url: url)
    let recordID = model.boardRegistry.recordOpen(of: url)
    let store = try model.storeRegistry.acquire(url)
    model.boardRegistry.setOpenNow(id: recordID)
    model.beginSession(ref: ref, store: store, recordID: recordID, access: nil)
    return ref
}

@MainActor
@Suite("History ▸ the board session's stack")
struct BoardSessionHistoryTests {

    @Test("A session is born with a stack, and its adapter is a face for that same stack")
    func aSessionOwnsOneStack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))
        let log = StepLog()

        #expect(session.undoManager.canUndo == false)
        session.history.register(log.step("Move Card"))

        // The window hands AppKit the adapter; the adapter is answering from the session's provider.
        #expect(session.undoManager.canUndo)
        #expect(session.undoManager.undoMenuItemTitle == "Undo Move Card")

        session.undoManager.undo()
        #expect(log.crossings == ["undo Move Card"])
        #expect(session.undoManager.canRedo)
    }

    @Test("Two open boards are two stacks — never one another's")
    func sessionsAreIsolated() throws {
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let firstRef = try openBoard(model, at: first.root)
        let secondRef = try openBoard(model, at: second.root)
        let log = StepLog()

        let firstSession = try #require(model.session(for: firstRef))
        let secondSession = try #require(model.session(for: secondRef))
        #expect(firstSession.history !== secondSession.history)

        firstSession.history.register(log.step("Move Card"))

        #expect(firstSession.history.canUndo)
        #expect(secondSession.history.canUndo == false)

        secondSession.undoManager.undo()
        #expect(log.crossings.isEmpty)
        #expect(firstSession.history.canUndo, "the other board's ⌘Z left this one's stack alone")
    }

    @Test("Closing a board empties its stack — session-only persistence")
    func closingClearsTheStack() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))
        let history = session.history
        let log = StepLog()
        history.register(log.step("Move Card"))
        #expect(history.canUndo)

        await model.closeBoard(ref: ref, cause: .userClose)

        #expect(model.session(for: ref) == nil)
        #expect(history.canUndo == false, "reopening the board starts empty")
        #expect(history.canRedo == false)
        #expect(log.crossings.isEmpty)
    }

    @Test("The session's manager answers this board's own read-only lock")
    func theSessionWiresTheLock() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))
        session.history.register(StepLog().step("Move Card"))
        #expect(session.undoManager.canUndo)

        session.store.enterVanishedRootLock()
        #expect(session.undoManager.canUndo == false)
        #expect(session.history.canUndo, "the stack itself survives the lock")

        session.store.handleWatcherEvent(.treeChanged(.appMediated))
        await session.store.awaitQuiescence()
        #expect(session.undoManager.canUndo, "and resumes when it clears")
    }

    @Test("The composition root decides which provider a session gets")
    func theProviderIsBoundAtComposition() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let bound = FakeHistoryProvider()
        model.makeHistoryProvider = { _, _ in bound }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))

        #expect(session.history === bound)
        session.undoManager.undo()
        #expect(bound.undoCount == 1, "the window's manager reaches whatever the root bound")
    }

    @Test("The tier reaches the composition root, and the session records what it composed under")
    func theTierIsAComposedFact() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        // 12-editions.md ▸ The entitlement: the tier is read at composition, once, and handed to the
        // root that binds the provider. Both tiers bind the native stack until pro-m1 — what this
        // pins is that the *argument arrives*, so the milestone that switches on it is a closure body.
        var seen: [Tier] = []
        model.currentTier = { .pro }
        model.makeHistoryProvider = { _, tier in
            seen.append(tier)
            return NativeHistoryProvider()
        }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))

        #expect(seen == [.pro])
        #expect(session.tier == .pro, "the session carries the fact it composed under")
    }

    @Test("A tier change never reaches a session that is already open")
    func aLapseNeverRebindsAnOpenSession() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        model.currentTier = { .pro }
        let ref = try openBoard(model, at: fixture.root)
        #expect(try #require(model.session(for: ref)).tier == .pro)

        // The subscription lapses mid-session — the one thing 12 ▸ The entitlement says must not
        // disturb a board that is already on screen: "an open board finishes with the provider it
        // composed; the next open composes the native stack over inert `.git`".
        model.currentTier = { .free }

        #expect(try #require(model.session(for: ref)).tier == .pro)
    }

    @Test("The purchase flow's reopen ends every session, because that is what recomposing means")
    func reopeningEndsTheSessions() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let ref = try openBoard(model, at: fixture.root)
        #expect(model.hasOpenBoards)

        // 12 ▸ The entitlement: "Subscribe takes effect at each board's next open ... The purchase
        // flow offers to reopen open boards." There is no rebinding-in-place to test for, and that
        // is the finding: reopening *is* ending the session and composing a new one, so what this
        // pins is the ending. The reopen half needs SwiftUI's window actions, which this host has
        // none of — the URLs simply buffer until an opener exists (`AppModel.openBoard`), which is
        // the same path a cold-launch Finder open already takes.
        await model.reopenOpenBoards()

        #expect(model.session(for: ref) == nil)
        #expect(model.hasOpenBoards == false)
        #expect(model.storeRegistry.openBoardCount == 0, "the store and its watcher went with the session")
    }
}

// MARK: - The command surface

/// **What the Edit menu's Undo/Redo rows and the toolbar's pair actually do** — driven through the
/// platform machinery they ride on rather than described (11-command-nexus.md ▸ Menu commands, the
/// M− row; 03-board-ui.md ▸ Toolbar; 13-native-undo.md ▸ Rules).
///
/// ### The app writes none of this, which is exactly why it is tested
///
/// There is no custom Undo/Redo menu code anywhere: the rows are the system's own nil-target
/// `undo:`/`redo:`, and the toolbar's two items carry the same selectors with the same nil target
/// (`BoardToolbar`). Every claim the design makes about them — they enable on a stack with steps,
/// they dim under the read-only lock, the *menu* rows retitle themselves to "Undo Move 3 Cards"
/// while the *toolbar* labels stay static — is therefore a claim about `NSWindow`'s own validation
/// reading the manager this app's window delegate hands back. Nothing here would fail loudly if the
/// wiring came undone; it would just quietly stop working, which is what these tests are for.
///
/// ### What a headless run can and cannot reach
///
/// `NSWindow.validateMenuItem(_:)` and `NSWindow.validateUserInterfaceItem(_:)` are the two methods
/// AppKit calls once a nil-target lookup has resolved to the window, and both answer fully in a test
/// process — which is the half this app owns and the half that can break. The lookup *itself*
/// (`NSApp.target(forAction:to:from:)`) needs a **key window**, and a unit-test host has none, so
/// "the board window is what the chain resolves to when it is key" is the one link these tests
/// cannot close; it is standard responder-chain behaviour with no code of this app's in it.
@MainActor
@Suite("History ▸ the command surface")
struct UndoCommandSurfaceTests {

    /// A scratch window fronted by the app's own delegate proxy, answering with `manager` — the
    /// board window's wiring exactly (`BoardWindowHost` sets the same closure).
    private func hostedWindow(_ manager: UndoManager?) -> (NSWindow, HostedWindowController) {
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
            styleMask: [.titled, .closable],
            backing: .buffered,
            defer: true
        )
        let controller = HostedWindowController()
        controller.boardUndoManager = { manager }
        controller.attach(to: window)
        return (window, controller)
    }

    private func menuItem(_ selector: String) -> NSMenuItem {
        NSMenuItem(title: selector == "undo:" ? "Undo" : "Redo", action: NSSelectorFromString(selector), keyEquivalent: "")
    }

    // MARK: The menu rows

    @Test("The Edit menu's rows read the board's stack, and retitle themselves from its step names")
    func theMenuRowsReadTheBoardsStack() {
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        let (window, controller) = hostedWindow(manager)
        defer { controller.detach() }
        let undoRow = menuItem("undo:")
        let redoRow = menuItem("redo:")

        #expect(window.undoManager === manager, "the window's answer is the session's adapter")

        // Nothing to cross: both rows disabled, and titled with the bare verbs.
        #expect(window.validateMenuItem(undoRow) == false)
        #expect(window.validateMenuItem(redoRow) == false)
        #expect(undoRow.title == "Undo")
        #expect(redoRow.title == "Redo")

        provider.canUndo = true
        provider.undoActionName = "Move 3 Cards"

        // 13's "the 06 vocabulary supplies menu titles ('Undo Move 3 Cards'), via NSUndoManager's
        // dynamic retitling": the app never writes that string — the platform composes it from the
        // bare phrase the seam vends, and validation is when it lands on the row.
        #expect(window.validateMenuItem(undoRow))
        #expect(undoRow.title == "Undo Move 3 Cards")
        #expect(window.validateMenuItem(redoRow) == false)
        #expect(redoRow.title == "Redo")

        provider.canRedo = true
        provider.redoActionName = "Rename Lane"
        #expect(window.validateMenuItem(redoRow))
        #expect(redoRow.title == "Redo Rename Lane")

        // And it tracks the stack, rather than being set once: crossing a step renames the row.
        provider.undoActionName = "Delete Card"
        #expect(window.validateMenuItem(undoRow))
        #expect(undoRow.title == "Undo Delete Card")
    }

    @Test("The read-only lock dims both rows and leaves their names standing")
    func theLockDimsTheRowsWithoutRenamingThem() {
        final class Lock { var isOn = false }
        let lock = Lock()
        let provider = FakeHistoryProvider()
        provider.canUndo = true
        provider.undoActionName = "Move Card"
        provider.canRedo = true
        provider.redoActionName = "Rename Card"
        let manager = BoardUndoManager(history: provider, isReadOnly: { lock.isOn })
        let (window, controller) = hostedWindow(manager)
        defer { controller.detach() }
        let undoRow = menuItem("undo:")
        let redoRow = menuItem("redo:")

        #expect(window.validateMenuItem(undoRow))
        #expect(window.validateMenuItem(redoRow))

        lock.isOn = true

        #expect(window.validateMenuItem(undoRow) == false, "disabled with every other mutating command")
        #expect(window.validateMenuItem(redoRow) == false)
        #expect(undoRow.title == "Undo Move Card", "a disabled row keeps its name — the stack survives the lock")
        #expect(redoRow.title == "Redo Rename Card")

        lock.isOn = false
        #expect(window.validateMenuItem(undoRow), "and resumes when it clears")
    }

    @Test("A window with no board has nothing to undo, and says so with the bare verb")
    func aBoardlessWindowHasNothingToCross() {
        let (window, controller) = hostedWindow(nil)
        defer { controller.detach() }
        let undoRow = menuItem("undo:")

        #expect(window.validateMenuItem(undoRow) == false)
        #expect(undoRow.title == "Undo")
    }

    @Test("Every window over one board answers with that board's one stack")
    func cardWindowsShareTheBoardsStack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (model, tearDown) = try makeModel()
        defer { tearDown() }

        let ref = try openBoard(model, at: fixture.root)
        let session = try #require(model.session(for: ref))
        // The board window and one of its card windows, wired the way their hosts wire them.
        let (boardWindow, boardController) = hostedWindow(session.undoManager)
        defer { boardController.detach() }
        let (cardWindow, cardController) = hostedWindow(session.undoManager)
        defer { cardController.detach() }
        let boardRow = menuItem("undo:")
        let cardRow = menuItem("undo:")

        session.history.register(StepLog().step("Move 3 Cards"))

        #expect(boardWindow.validateMenuItem(boardRow))
        #expect(cardWindow.validateMenuItem(cardRow), "one stack per board, not per window")
        #expect(boardRow.title == "Undo Move 3 Cards")
        #expect(cardRow.title == boardRow.title)
    }

    // MARK: The toolbar twins

    @Test("The toolbar pair validates identically to the menu rows — and keeps its static labels")
    func theToolbarPairMatchesTheMenuRows() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let controller = BoardToolbar.controller(store: store, search: BoardSearchPresentation())
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        let (window, hosted) = hostedWindow(manager)
        defer { hosted.detach() }

        /// The real items, built by the real delegate — nil target, `undo:`/`redo:` actions.
        func item(_ identifier: NSToolbarItem.Identifier) throws -> NSToolbarItem {
            try #require(controller.toolbar(
                controller.toolbar,
                itemForItemIdentifier: identifier,
                willBeInsertedIntoToolbar: true
            ))
        }
        let undoItem = try item(.boardUndo)
        let redoItem = try item(.boardRedo)
        #expect(undoItem.target == nil, "nil target: the chain resolves it, exactly as the menu row's is")
        #expect(redoItem.target == nil)

        // `validateUserInterfaceItem` is what `NSToolbarItem.validate()` asks its resolved target,
        // and `validateMenuItem` is what a menu row's asks — one predicate, two doors.
        #expect(window.validateUserInterfaceItem(undoItem) == false)
        #expect(window.validateUserInterfaceItem(redoItem) == false)

        provider.canUndo = true
        provider.undoActionName = "Move 3 Cards"
        provider.canRedo = true
        provider.redoActionName = "Rename Lane"

        let undoRow = menuItem("undo:")
        let redoRow = menuItem("redo:")
        #expect(window.validateUserInterfaceItem(undoItem) == window.validateMenuItem(undoRow))
        #expect(window.validateUserInterfaceItem(redoItem) == window.validateMenuItem(redoRow))
        #expect(window.validateUserInterfaceItem(undoItem))
        #expect(window.validateUserInterfaceItem(redoItem))

        // 03's one exception to the label rule, proven rather than asserted: validation rewrote the
        // *menu* row's title and left the toolbar item's label exactly where it was.
        #expect(undoRow.title == "Undo Move 3 Cards")
        #expect(undoItem.label == "Undo")
        #expect(redoItem.label == "Redo")
        #expect(undoItem.paletteLabel == "Undo", "the customize palette shows the static label too")
    }

    @Test("A toolbar item's own validation lands on the board's answer, lock included")
    func theToolbarItemValidatesThroughTheWindow() 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 toolbar = BoardToolbar.controller(store: store, search: BoardSearchPresentation())
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider, isReadOnly: { lock.isOn })
        let (window, hosted) = hostedWindow(manager)
        defer { hosted.detach() }

        let item = try #require(toolbar.toolbar(
            toolbar.toolbar,
            itemForItemIdentifier: .boardUndo,
            willBeInsertedIntoToolbar: true
        ))
        // The one link a headless run cannot make: `NSToolbarItem.validate()` resolves its target
        // through the key window, and a test host has none. Standing the window in as the target is
        // that lookup's *answer* — which is what nil-target means when a board window is key — so
        // what this asserts is the item's own validation path, end to end from `validate()`.
        item.target = window
        #expect(item.autovalidates, "AppKit revalidates it on user events; the observation covers the rest")

        item.validate()
        #expect(item.isEnabled == false, "an empty stack dims it")

        provider.canUndo = true
        item.validate()
        #expect(item.isEnabled)

        lock.isOn = true
        item.validate()
        #expect(item.isEnabled == false, "the lock disables the toolbar pair with the menu rows")

        lock.isOn = false
        item.validate()
        #expect(item.isEnabled)
        #expect(item.label == "Undo", "no crossing of validation ever moves the label")
    }

    @Test("The pair's enablement is deliberately not a predicate of the toolbar's own")
    func theSpecsAbstainFromEnablement() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let specs = BoardToolbar.specs(store: store, search: BoardSearchPresentation())

        // Every other item mirrors its menu row's predicate; these two mirror the *mechanism*. A
        // spec-level `isEnabled` here would be a second answer able to disagree with the responder
        // chain's — and it would have to read a stack the toolbar has no route to, since the board's
        // window is what owns that answer.
        for identifier in [NSToolbarItem.Identifier.boardUndo, .boardRedo] {
            let spec = try #require(specs.first { $0.identifier == identifier })
            #expect(spec.isEnabled, "abstention, not enablement: AppKit's own validation decides")
            #expect(spec.isOn == nil)
        }
    }
}
