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 (the git provider bound the same protocol,
/// in pro-m1, before app-managed git was excised entirely, `strategy/01-git-excision.md`).
@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 == true)
        #expect(secondSession.history?.canUndo == false)

        secondSession.undoManager.undo()
        #expect(log.crossings.isEmpty)
        #expect(firstSession.history?.canUndo == true, "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 = try #require(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 == true, "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")
    }
}

// MARK: - The command surface

/// **What a window hands AppKit, and what the board toolbar's Undo/Redo pair reaches** — the two
/// halves of the command surface that are still platform-shaped, driven through the machinery they
/// ride on rather than described (03-board-ui.md ▸ Toolbar; 13-native-undo.md ▸ Rules).
///
/// ### What this suite covers since the surface became the app's own
///
/// **The rows are no longer here.** Edit ▸ Undo/Redo are `CommandGroup(replacing: .undoRedo)` rows
/// the app writes and routes itself (13 ▸ Rules ▸ the command-surface bullet, re-ruled 2026-08-08;
/// `UndoCommands.swift`, pinned by `UndoCommandsTests.swift`), because the nil-target route they used
/// to ride is unreachable on a SwiftUI window: `NSWindow` latches an empty undo manager during
/// creation, before `HostedWindowController` installs, so `windowWillReturnUndoManager` is never
/// consulted for them. These tests never reproduced that — they attach the delegate before the
/// window's first read, which is exactly the ordering a real SwiftUI window denies — and that is the
/// diagnosis, not a gap to close: the hook works when it is asked, and the app stopped depending on
/// it being asked.
///
/// What is pinned below is therefore what remains true and load-bearing:
///
/// - **The delegate hook itself**, which stays wired because it is the right answer wherever *AppKit*
///   asks a window's delegate for a manager (`HostedWindowController.windowWillReturnUndoManager`).
///   `NSWindow.validateMenuItem(_:)` is the sharpest instrument a headless run has for reading what
///   that hook returned — enablement, the lock, the composed title, the two levels — so the menu
///   rows still appear here as the *probe*, not as the subject.
/// - **The toolbar pair's explicit target** (`BoardToolbar`), which validates and fires against the
///   session's `BoardUndoManager` directly. That path is the app's own end to end, and it is the one
///   a headless run can close completely.
@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.windowUndoManager = { manager }
        controller.attach(to: window)
        return (window, controller)
    }

    /// A row carrying the platform's own `undo:`/`redo:`, used here as a **probe** rather than as a
    /// shipped surface: validating one against the window is how a test reads back the manager
    /// `windowWillReturnUndoManager` returned, title composition and all. The app's own rows carry no
    /// selector at all (`UndoCommands.swift`).
    private func menuItem(_ selector: String) -> NSMenuItem {
        NSMenuItem(title: selector == "undo:" ? "Undo" : "Redo", action: NSSelectorFromString(selector), keyEquivalent: "")
    }

    // MARK: What the delegate hands back

    @Test("A window's delegate hands back the board's stack, titles composing 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 vocabulary supplies menu titles ('Undo Move 3 Cards'), via dynamic retitling":
        // the app never writes that string — the platform composes it from the bare phrase the seam
        // vends (`BoardUndoManager.undoMenuItemTitle`), and this is where it lands. The app's own
        // rows read the very same property, one step further out (`UndoCommandRouting.undoTitle`).
        #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("A board window answers with the board's stack; a card window answers with its own")
    func eachWindowAnswersWithItsOwnStack() throws {
        // Realigned 2026-07-31 with the two-level model (13-native-undo.md ▸ Rules): this pinned the
        // shared-stack wiring, which is exactly what the session-coarsening re-ruling replaced —
        // "a card window owns its own stack ... and `window.undoManager` answers with it".
        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
        // (`BoardWindowHost.configureWindow`, `CardWindowHost.configureWindow`).
        let card = CardWindowUndo()
        let (boardWindow, boardController) = hostedWindow(session.undoManager)
        defer { boardController.detach() }
        let (cardWindow, cardController) = hostedWindow(card.manager)
        defer { cardController.detach() }
        let boardRow = menuItem("undo:")
        let cardRow = menuItem("undo:")

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

        #expect(boardWindow.validateMenuItem(boardRow))
        #expect(boardRow.title == "Undo Move 3 Cards")

        // **No fall-through** (13-native-undo.md ▸ Undo routing): the card window's own stack is
        // empty, so its row is disabled and ⌘Z beeps — it never reaches the board's step. The rows
        // enforce it the same way now, by reading one focused value that is one window's or the
        // other's (`FocusedValues.undoStack`) and never both.
        #expect(cardWindow.validateMenuItem(cardRow) == false)
        #expect(cardRow.title == "Undo")

        card.stack.register(StepLog().step("Comment"))
        #expect(cardWindow.validateMenuItem(cardRow))
        #expect(cardRow.title == "Undo Comment")
        #expect(boardRow.title == "Undo Move 3 Cards", "and the board's row is untouched by it")
    }

    // MARK: The toolbar twins

    /// A board toolbar wired the way `BoardWindowHost` wires one, over `undo`.
    private func boardToolbar(store: BoardStore, undo: BoardUndoManager?) -> WindowToolbarController {
        let domain = "dev.rzen.indie.Kanban.history-tests.\(UUID().uuidString)"
        return BoardToolbar.controller(
            store: store,
            search: BoardSearchPresentation(),
            zoom: BoardZoomStore(defaults: UserDefaults(suiteName: domain)!),
            appearance: AppearanceStore(defaults: UserDefaults(suiteName: domain + ".appearance")!, apply: { _ in }),
            session: DragSession(),
            undo: undo
        )
    }

    /// One real item, built by the real delegate.
    private func toolbarItem(
        _ controller: WindowToolbarController,
        _ identifier: NSToolbarItem.Identifier
    ) throws -> NSToolbarItem {
        try #require(controller.toolbar(
            controller.toolbar,
            itemForItemIdentifier: identifier,
            willBeInsertedIntoToolbar: true
        ))
    }

    /// **The pair carries an explicit target now** (13-native-undo.md ▸ Rules ▸ the command-surface
    /// bullet, re-ruled 2026-08-08) — the toolbar controller, over the session's `BoardUndoManager`,
    /// where until that ruling both items carried nil targets and `undo:`/`redo:` selectors for the
    /// responder chain to resolve.
    ///
    /// This replaces the pin that read the pair's validation *through the window* against the menu
    /// rows'. The claim it was making — one answer on both surfaces — is unchanged and now stronger:
    /// they are not two validations that agree, they are one object both of them read
    /// (`bothSurfacesReadOneManager`).
    @Test("The toolbar pair targets the session's stack — and keeps its static labels")
    func theToolbarPairCarriesAnExplicitTarget() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        let controller = boardToolbar(store: store, undo: manager)

        let undoItem = try toolbarItem(controller, .boardUndo)
        let redoItem = try toolbarItem(controller, .boardRedo)
        #expect(undoItem.target === controller, "the app's own target, not the responder chain's lookup")
        #expect(redoItem.target === controller)

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

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

        undoItem.validate()
        redoItem.validate()
        #expect(undoItem.isEnabled)
        #expect(redoItem.isEnabled)

        // 03's one exception to the label rule, proven rather than asserted: the phrase the *menu*
        // composes ("Undo Move 3 Cards") never reaches a toolbar label, whatever validation does.
        #expect(manager.undoMenuItemTitle == "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 theToolbarItemValidatesThroughTheManager() 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 provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider, isReadOnly: { lock.isOn })
        let controller = boardToolbar(store: store, undo: manager)

        let item = try toolbarItem(controller, .boardUndo)
        // The link a headless run no longer has to fake: validation used to resolve a nil target
        // through the key window, which a test host does not have, so the window stood in as the
        // answer. The item is handed its target at construction now, so this is the shipped path
        // end to end from `validate()`.
        #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("Clicking the item crosses the board's stack, through the target it was given")
    func theToolbarItemCrossesTheStack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let provider = FakeHistoryProvider()
        provider.canUndo = true
        provider.canRedo = true
        let manager = BoardUndoManager(history: provider)
        let controller = boardToolbar(store: store, undo: manager)

        let undoItem = try toolbarItem(controller, .boardUndo)
        let redoItem = try toolbarItem(controller, .boardRedo)
        let target = try #require(undoItem.target as? NSObject)
        let action = try #require(undoItem.action)

        target.perform(action, with: undoItem)
        #expect(provider.undoCount == 1, "the click reaches the session's stack, not a responder's")

        let redoTarget = try #require(redoItem.target as? NSObject)
        redoTarget.perform(try #require(redoItem.action), with: redoItem)
        #expect(provider.redoCount == 1)
    }

    /// **One manager, two faces** — what "the toolbar mirrors the menu" means for this pair now that
    /// neither of them goes through the responder chain (13-native-undo.md ▸ Rules ▸ the
    /// command-surface bullet, re-ruled 2026-08-08; `BoardToolbar`'s header).
    ///
    /// This is where the retired `theSpecsAbstainFromEnablement` pin went. Its claim — that the pair
    /// must never grow a second answer able to disagree with the menu's — is the same claim, made
    /// the only way it can be now that the items *do* carry a predicate: the predicate and the row
    /// are reading one object, so they cannot come apart.
    @Test("The toolbar pair and the Edit menu's rows read one and the same manager")
    func bothSurfacesReadOneManager() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let provider = FakeHistoryProvider()
        let manager = BoardUndoManager(history: provider)
        let specs = BoardToolbar.specs(
            store: store,
            search: BoardSearchPresentation(),
            zoom: BoardZoomStore(defaults: UserDefaults(suiteName: "dev.rzen.indie.Kanban.history-tests.\(UUID().uuidString)")!),
            appearance: AppearanceStore(defaults: UserDefaults(suiteName: "dev.rzen.indie.Kanban.history-tests.\(UUID().uuidString)")!, apply: { _ in }),
            session: DragSession(),
            undo: manager
        )

        // The row's side of it, derived exactly as `UndoMenuRow` derives it: the focused stack, with
        // nothing text-shaped holding the keyboard.
        func rowIsEnabled() -> Bool {
            UndoCommandRouting.canUndo(
                UndoCommandRouting.routedManager(stack: manager, firstResponder: nil)
            )
        }

        for state in [false, true, false] {
            provider.canUndo = state
            let spec = try #require(specs.first { $0.identifier == .boardUndo })
            #expect(spec.isEnabled == state)
            #expect(spec.isEnabled == rowIsEnabled(), "one object answers both surfaces")
        }
    }
}
