import Foundation
import Testing
@testable import Kanban

/// `BoardRegistry` is app-private state about the user's boards, and its two hardest promises are
/// negative ones: it must match a board that moved (so settings survive a rename), and it must
/// never write a byte into a board folder (so files-first stays absolute). Both are tested here
/// against real directories and a real storage file in temp — a fake filesystem would prove neither,
/// since both are claims about file identity and about what is on disk.
///
/// The third promise is that nothing it does can take the app down: a missing file, a corrupt file,
/// and a bookmark that no longer resolves all have expected, boring outcomes, and each has a test.

// MARK: - Fixtures

/// A temp folder holding the registry's JSON file, kept separate from every board folder so the
/// quarantine test can see exactly what the registry put next to it.
@MainActor
private struct RegistryStorage {
    let folder: URL

    var url: URL { folder.appendingPathComponent("board-registry.json", isDirectory: false) }

    init() throws {
        folder = FileManager.default.temporaryDirectory
            .appendingPathComponent("BoardRegistryTests-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
    }

    func tearDown() {
        try? FileManager.default.removeItem(at: folder)
    }

    func entryNames() throws -> [String] {
        try FileManager.default.contentsOfDirectory(atPath: folder.path).sorted()
    }
}

/// A small real board — the registry never reads inside one, but a folder with contents is what
/// makes the files-first test able to notice a single stray byte.
@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"))
    return fixture
}

/// Every path under `root` (the root itself included, hidden entries included) with its
/// modification date — the shape "the registry touched nothing" takes as an assertion.
private func treeSnapshot(of root: URL) throws -> [String: Date] {
    var snapshot: [String: Date] = [:]

    func modified(_ url: URL) throws -> Date {
        try url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate ?? .distantPast
    }

    snapshot["."] = try modified(root)
    let walker = FileManager.default.enumerator(
        at: root,
        includingPropertiesForKeys: [.contentModificationDateKey],
        options: []
    )
    while let url = walker?.nextObject() as? URL {
        let relative = url.path.replacingOccurrences(of: root.path + "/", with: "")
        snapshot[relative] = try modified(url)
    }
    return snapshot
}

private func ids(_ recents: [RecentBoard]) -> [UUID] {
    recents.map(\.record.id)
}

// MARK: - Tests

@MainActor
@Suite("BoardRegistry")
struct BoardRegistryTests {

    // MARK: Matching on open

    @Test("A second open of the same board updates its record; a rename does not fool it")
    func recordOpenMatchesByIdentity() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
        let created = try #require(registry.record(id: id))
        #expect(!created.bookmark.isEmpty)
        #expect(created.displayName == "Todo Board")
        #expect(created.lastKnownPath == fixture.root.path)
        #expect(created.laneCount == nil, "counts are stamped at close, never guessed at open")
        #expect(created.pushOnCommit == false)
        #expect(created.remoteLocationWarned == false)
        #expect(registry.recents().count == 1)

        try await Task.sleep(for: .milliseconds(5))
        let again = registry.recordOpen(of: fixture.root, displayName: "Renamed In Title")
        #expect(again == id, "the same folder is the same board")
        #expect(registry.recents().count == 1, "a second open updates a record, it does not add one")
        let updated = try #require(registry.record(id: id))
        #expect(updated.displayName == "Renamed In Title")
        #expect(updated.lastOpened > created.lastOpened)

        // The done-when criterion: a Finder rename, then an open through the new path. Only file
        // identity gets this right — every path-keyed answer produces a second record here.
        let renamed = fixture.root
            .deletingLastPathComponent()
            .appendingPathComponent("renamed-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.moveItem(at: fixture.root, to: renamed)
        defer { try? FileManager.default.removeItem(at: renamed) }

        let afterRename = registry.recordOpen(of: renamed, displayName: "Todo Board")
        #expect(afterRename == id, "a renamed board keeps its record, and so keeps its settings")
        #expect(registry.recents().count == 1)
        #expect(registry.record(id: id)?.lastKnownPath == renamed.path)
    }

    @Test("Recording an open with no displayName gives a brand-new record the folder name")
    func recordOpenWithNoDisplayNameUsesTheFolderName() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        // The "record before load" call (02-architecture.md § Per-board app state): nothing
        // authoritative is known yet, so the provisional name is the folder's own — "the folder
        // name is the Finder document name the user just picked."
        let id = registry.recordOpen(of: fixture.root)
        let record = try #require(registry.record(id: id))
        #expect(record.displayName == fixture.root.deletingPathExtension().lastPathComponent)
        #expect(!record.bookmark.isEmpty, "the bookmark still mints on the before-load call")
        #expect(record.icon == nil)
        #expect(record.iconColor == nil)
    }

    @Test("Recording an open with no displayName never regresses an existing record's cached name")
    func recordOpenWithNoDisplayNamePreservesAnExistingRecord() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        // A first, successful-looking open: real values, as a caller with a loaded snapshot passes.
        let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board", icon: "star", iconColor: "fern")
        let firstOpened = try #require(registry.record(id: id)).lastOpened
        try await Task.sleep(for: .milliseconds(5))

        // A later open attempt whose load has not run yet — or never gets the chance to, because it
        // fails fail-fast. Either way, this call alone must not know that, so the cached title and
        // style survive untouched; only the bits every open refreshes regardless move.
        let again = registry.recordOpen(of: fixture.root)
        #expect(again == id)
        let record = try #require(registry.record(id: id))
        #expect(record.displayName == "Todo Board", "the last successful open's title is still the best information this row has")
        #expect(record.icon == "star")
        #expect(record.iconColor == "fern")
        #expect(record.lastOpened > firstOpened, "the bookmark and lastOpened still refresh on every open attempt")
        #expect(!record.bookmark.isEmpty)
    }

    // MARK: Counts

    @Test("Counts are stamped at close and read back without a scan")
    func closeStampsCountsAndRecentsNeverScans() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
        registry.recordClose(id: id, displayName: "Todo Board", laneCount: 2, cardCount: 7)

        // The board grows after the close — an agent filing cards, a colleague's pull. A welcome
        // window that scanned would notice; this one must not, because scanning is what makes
        // welcome slow on a big board and hangs it on an unavailable one.
        try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
        try fixture.item("\(Ident.lane2)/\(Ident.card2)", Item.rich(order: "1024", title: "Second"))
        try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "2048", title: "Third"))

        let rows = registry.recents()
        #expect(rows.count == 1)
        guard case let .available(record, at: url) = rows[0] else {
            Issue.record("expected the board to be available, got \(rows[0])")
            return
        }
        #expect(record.laneCount == 2, "the stamped count, not the tree's count")
        #expect(record.cardCount == 7)
        #expect(FileIdentity(of: url) == FileIdentity(of: fixture.root))
    }

    // MARK: Title, icon, and iconColor

    @Test("Icon and iconColor are stamped at open and re-stamped at close, alongside the title")
    func openAndCloseStampIconAndIconColor() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board", icon: "star", iconColor: "fern")
        let opened = try #require(registry.record(id: id))
        #expect(opened.icon == "star")
        #expect(opened.iconColor == "fern")

        // The board is restyled while open — an in-app change or a foreign edit, it makes no
        // difference here since `recordClose` re-stamps from whatever the store last held.
        registry.recordClose(
            id: id,
            displayName: "Todo Board",
            laneCount: 2,
            cardCount: 7,
            icon: "heart",
            iconColor: "carnation"
        )
        let closed = try #require(registry.record(id: id))
        #expect(closed.icon == "heart")
        #expect(closed.iconColor == "carnation")

        // A board can also lose its override entirely — `nil` closes back over a previously
        // stamped value rather than being mistaken for "leave it alone".
        registry.recordClose(id: id, displayName: "Todo Board", laneCount: 2, cardCount: 7)
        #expect(registry.record(id: id)?.icon == nil)
        #expect(registry.record(id: id)?.iconColor == nil)
    }

    @Test("The live write-through updates title, icon, and iconColor when they differ from the cached record")
    func syncDisplayStateWritesThroughOnlyWhatChanged() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Todo Board")
        #expect(registry.record(id: id)?.icon == nil, "no override yet")

        // An in-app rename/restyle's reload lands new values — the seam `BoardStore`'s
        // `displayStateDelegate` calls into.
        registry.syncDisplayState(id: id, title: "Renamed", icon: "star.fill", iconColor: "deep-sky-blue")
        let afterFirstSync = try #require(registry.record(id: id))
        #expect(afterFirstSync.displayName == "Renamed")
        #expect(afterFirstSync.icon == "star.fill")
        #expect(afterFirstSync.iconColor == "deep-sky-blue")

        // A later reload whose display state is unchanged from the cached record leaves it exactly
        // as it was — the no-op-skip rule every other setter in this file already keeps.
        registry.syncDisplayState(id: id, title: "Renamed", icon: "star.fill", iconColor: "deep-sky-blue")
        #expect(registry.record(id: id) == afterFirstSync)

        // The values persist like any other mutation here.
        let reloaded = BoardRegistry(storageURL: storage.url)
        #expect(reloaded.record(id: id)?.displayName == "Renamed")
        #expect(reloaded.record(id: id)?.icon == "star.fill")
        #expect(reloaded.record(id: id)?.iconColor == "deep-sky-blue")
    }

    @Test("The live write-through on an id with no record is a no-op, not a crash")
    func syncDisplayStateOnUnknownIDIsANoOp() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        registry.syncDisplayState(id: UUID(), title: "Ghost", icon: "star", iconColor: "fern")

        #expect(registry.recents().isEmpty, "a window that outlived its record must not resurrect one")
    }

    @Test("A registry file written before the icon/iconColor keys existed still decodes, with both nil")
    func oldRegistryFilesDecodeWithoutIconKeys() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        // Byte-for-byte the shape this file had before icon/iconColor existed — the same evolution
        // rule `oldRegistryFilesDecodeWithoutTheOpenNowKey` pins for `isOpenNow`: a key added here
        // must be optional, or every existing user's recents empties on upgrade.
        let id = UUID()
        let garbage = Data("not a bookmark".utf8).base64EncodedString()
        let json = """
        [
          {
            "bookmark" : "\(garbage)",
            "cardCount" : 9,
            "displayName" : "Archive",
            "id" : "\(id.uuidString)",
            "laneCount" : 4,
            "lastKnownPath" : "/Volumes/Archive/Boards/Archive",
            "lastOpened" : "2026-01-01T09:00:00.000Z",
            "pushOnCommit" : true,
            "remoteLocationWarned" : true
          }
        ]
        """
        try Data(json.utf8).write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        #expect(registry.recents().count == 1, "the file decoded; nothing was quarantined")
        #expect(registry.record(id: id)?.icon == nil, "a missing key reads as 'no override'")
        #expect(registry.record(id: id)?.iconColor == nil)

        // And both keys write through from here on.
        registry.syncDisplayState(id: id, title: "Archive", icon: "archivebox", iconColor: "aluminum")
        let updated = BoardRegistry(storageURL: storage.url).record(id: id)
        #expect(updated?.icon == "archivebox")
        #expect(updated?.iconColor == "aluminum")
    }

    @Test("A record with no bookmark key at all is a born orphan, not a quarantine")
    func aRecordWithNoBookmarkKeyDecodesAsAnOrphan() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        // `bookmark` carries a decoding default like every key past the four founding ones, and this
        // is why: a record the system refused to mint a key for is a recents row with Forget — the
        // born-orphaned case `recordOpen` already writes — and quarantining the user's whole list
        // over one keyless entry would be the cure being worse than the disease.
        let id = UUID()
        let json = """
        [
          {
            "displayName" : "Ghost",
            "id" : "\(id.uuidString)",
            "lastKnownPath" : "/Volumes/Gone/Ghost.kanban",
            "lastOpened" : "2026-01-01T09:00:00.000Z"
          }
        ]
        """
        try Data(json.utf8).write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        let rows = registry.recents()
        #expect(rows.count == 1, "the file decoded; nothing was quarantined")
        guard case .unavailable = rows[0] else {
            Issue.record("expected unavailable, got \(rows[0])")
            return
        }
        #expect(registry.record(id: id)?.bookmark.isEmpty == true)
        // The other defaulted keys came through as their defaults too, rather than as a throw.
        #expect(registry.record(id: id)?.isOpenNow == false)
        #expect(registry.record(id: id)?.pushOnCommit == false)
        #expect(registry.record(id: id)?.remoteLocationWarned == false)
    }

    @Test("A registry file with icon and iconColor present decodes them")
    func registryFileWithIconKeysDecodes() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        let id = UUID()
        let garbage = Data("not a bookmark".utf8).base64EncodedString()
        let json = """
        [
          {
            "bookmark" : "\(garbage)",
            "cardCount" : 9,
            "displayName" : "Archive",
            "icon" : "archivebox",
            "iconColor" : "aluminum",
            "id" : "\(id.uuidString)",
            "laneCount" : 4,
            "lastKnownPath" : "/Volumes/Archive/Boards/Archive",
            "lastOpened" : "2026-01-01T09:00:00.000Z",
            "pushOnCommit" : true,
            "remoteLocationWarned" : true
          }
        ]
        """
        try Data(json.utf8).write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        #expect(registry.record(id: id)?.icon == "archivebox")
        #expect(registry.record(id: id)?.iconColor == "aluminum")
    }

    // MARK: Orphaning

    @Test("A deleted board is orphaned in recents and can be forgotten")
    func deletedBoardIsOrphaned() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Doomed")
        registry.recordClose(id: id, displayName: "Doomed", laneCount: 1, cardCount: 1)
        fixture.tearDown()

        let rows = registry.recents()
        #expect(rows.count == 1)
        guard case let .unavailable(record) = rows[0] else {
            Issue.record("expected an orphan, got \(rows[0])")
            return
        }
        #expect(record.id == id)
        #expect(record.displayName == "Doomed", "an orphan still renders — with Forget, not nothing")
        #expect(record.lastKnownPath == fixture.root.path)

        registry.forget(id: id)
        #expect(registry.recents().isEmpty)
        #expect(registry.record(id: id) == nil)
        #expect(BoardRegistry(storageURL: storage.url).recents().isEmpty, "forgetting persists")
    }

    @Test("A bookmark that cannot resolve at all classifies as unavailable")
    func unresolvableBookmarkIsUnavailable() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        // Hand-written rather than produced by the registry: deleting a folder *ought* to make its
        // bookmark stop resolving, but "ought to" is the filesystem's opinion, and this rule needs a
        // case that cannot resolve by construction. It doubles as the only place the on-disk shape
        // is pinned literally — including the fractional-seconds timestamp format.
        let id = UUID()
        let garbage = Data("not a bookmark".utf8).base64EncodedString()
        let json = """
        [
          {
            "bookmark" : "\(garbage)",
            "cardCount" : 9,
            "displayName" : "Archive",
            "id" : "\(id.uuidString)",
            "laneCount" : 4,
            "lastKnownPath" : "/Volumes/Archive/Boards/Archive",
            "lastOpened" : "2026-01-01T09:00:00.000Z",
            "pushOnCommit" : true,
            "remoteLocationWarned" : true
          }
        ]
        """
        try Data(json.utf8).write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        let rows = registry.recents()
        #expect(rows.count == 1, "an unreadable bookmark is an orphaned row, not a decoding failure")
        guard case let .unavailable(record) = rows[0] else {
            Issue.record("expected an orphan, got \(rows[0])")
            return
        }
        #expect(record.id == id)
        #expect(record.laneCount == 4, "an orphan still shows the counts it was closed with")
        #expect(record.pushOnCommit)
        #expect(record.remoteLocationWarned)
        #expect(record.lastKnownPath == "/Volumes/Archive/Boards/Archive")

        registry.forget(id: id)
        #expect(registry.recents().isEmpty)
    }

    // MARK: Clear Menu

    @Test("Clear Menu empties the registry, and persists")
    func forgetAllEmptiesTheRegistry() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }

        let registry = BoardRegistry(storageURL: storage.url)
        registry.recordOpen(of: first.root, displayName: "First")
        registry.recordOpen(of: second.root, displayName: "Second")
        #expect(registry.recents().count == 2)

        registry.forgetAll()

        #expect(registry.recents().isEmpty)
        #expect(BoardRegistry(storageURL: storage.url).recents().isEmpty, "Clear Menu persists")
    }

    /// The equivalence 11-command-nexus.md's Clear Menu rests on: Finder clears a *menu*, and here
    /// the registry **is** the menu — so clearing it can only mean forgetting every record, and must
    /// leave the file in precisely the state that forgetting them one at a time would.
    @Test("Clear Menu is Forget applied to every row — same result, same file")
    func forgetAllMatchesForgettingEachRow() async throws {
        let wholesale = try RegistryStorage()
        defer { wholesale.tearDown() }
        let piecemeal = try RegistryStorage()
        defer { piecemeal.tearDown() }
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }

        func populate(_ storage: RegistryStorage) -> BoardRegistry {
            let registry = BoardRegistry(storageURL: storage.url)
            registry.recordOpen(of: first.root, displayName: "First")
            registry.recordOpen(of: second.root, displayName: "Second")
            return registry
        }

        let bulk = populate(wholesale)
        bulk.forgetAll()

        let oneByOne = populate(piecemeal)
        for row in oneByOne.recents() {
            oneByOne.forget(id: row.record.id)
        }

        #expect(bulk.recents().isEmpty)
        #expect(oneByOne.recents().isEmpty)
        #expect(
            try Data(contentsOf: wholesale.url) == Data(contentsOf: piecemeal.url),
            "the two paths leave byte-identical files — there is no state Clear Menu skips"
        )
    }

    @Test("Clear Menu on an empty registry writes nothing")
    func forgetAllOnEmptyRegistryIsANoOp() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        let registry = BoardRegistry(storageURL: storage.url)
        registry.forgetAll()

        #expect(try storage.entryNames().isEmpty, "an empty registry has nothing to clear and no file to write")
    }

    // MARK: Persistence

    @Test("Every mutation survives a reload of the file, dates included")
    func mutationsPersistAcrossInstances() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }

        let registry = BoardRegistry(storageURL: storage.url)
        let firstID = registry.recordOpen(of: first.root, displayName: "First")
        try await Task.sleep(for: .milliseconds(5))
        let secondID = registry.recordOpen(of: second.root, displayName: "Second")
        registry.recordClose(id: firstID, displayName: "First", laneCount: 3, cardCount: 11)
        registry.updateWindowFrame(id: firstID, frame: WindowFrame(x: 120, y: 60, width: 1440, height: 900))
        registry.setPushOnCommit(id: firstID, true)
        registry.setRemoteLocationWarned(id: firstID)

        let reloaded = BoardRegistry(storageURL: storage.url)

        // Equality of the whole record, not a field-by-field approximation: `lastOpened` is stamped
        // at the resolution the file records, so the reloaded value is the *same* date rather than
        // one within a second of it.
        #expect(reloaded.record(id: firstID) == registry.record(id: firstID))
        #expect(reloaded.record(id: secondID) == registry.record(id: secondID))

        let restored = try #require(reloaded.record(id: firstID))
        #expect(restored.laneCount == 3)
        #expect(restored.cardCount == 11)
        #expect(restored.windowFrame == WindowFrame(x: 120, y: 60, width: 1440, height: 900))
        #expect(restored.pushOnCommit)
        #expect(restored.remoteLocationWarned)
        #expect(restored.lastOpened == registry.record(id: firstID)?.lastOpened)

        #expect(ids(reloaded.recents()) == ids(registry.recents()), "order survives too")
    }

    // MARK: Corruption

    @Test("A corrupt file is quarantined, not deleted, and the registry carries on empty")
    func corruptFileIsQuarantined() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }

        let garbage = Data("{ this is not the registry you are looking for".utf8)
        try garbage.write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        #expect(registry.recents().isEmpty, "app-private convenience state never takes the app down")

        let quarantined = try storage.entryNames().filter { $0 != storage.url.lastPathComponent }
        #expect(quarantined.count == 1)
        let quarantinedName = try #require(quarantined.first)
        #expect(quarantinedName.contains("corrupt"))
        #expect(quarantinedName.hasSuffix(".json"))
        let preserved = try Data(contentsOf: storage.folder.appendingPathComponent(quarantinedName))
        #expect(preserved == garbage, "renamed aside, never deleted — it may be the only trace of the user's boards")

        // And the registry is usable from here: the next save writes a clean file over the hole the
        // quarantine left.
        let id = registry.recordOpen(of: fixture.root, displayName: "Fresh Start")
        #expect(BoardRegistry(storageURL: storage.url).record(id: id)?.displayName == "Fresh Start")
    }

    // MARK: Files-first

    @Test("Nothing the registry does touches the board folder")
    func theBoardFolderIsNeverTouched() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }

        let before = try treeSnapshot(of: fixture.root)

        let registry = BoardRegistry(storageURL: storage.url)
        let id = registry.recordOpen(of: fixture.root, displayName: "Untouched")
        registry.recordClose(id: id, displayName: "Untouched", laneCount: 1, cardCount: 1)
        registry.updateWindowFrame(id: id, frame: WindowFrame(x: 0, y: 0, width: 800, height: 600))
        registry.setPushOnCommit(id: id, true)
        registry.setRemoteLocationWarned(id: id)
        _ = registry.recents()
        _ = BoardRegistry(storageURL: storage.url).recents()

        let after = try treeSnapshot(of: fixture.root)
        #expect(after == before, "no sidecar, no frontmatter key, no xattr — nothing app-private goes in the board")
        #expect(!storage.url.path.hasPrefix(fixture.root.path), "and the file itself lives elsewhere entirely")
    }

    // MARK: Ordering

    @Test("Recents is this registry sorted by last-opened, and an open bumps a board to the top")
    func recentsOrderFollowsLastOpened() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }
        let third = try makeBoard()
        defer { third.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let firstID = registry.recordOpen(of: first.root, displayName: "First")
        try await Task.sleep(for: .milliseconds(5))
        let secondID = registry.recordOpen(of: second.root, displayName: "Second")
        try await Task.sleep(for: .milliseconds(5))
        let thirdID = registry.recordOpen(of: third.root, displayName: "Third")

        #expect(ids(registry.recents()) == [thirdID, secondID, firstID])

        try await Task.sleep(for: .milliseconds(5))
        _ = registry.recordOpen(of: first.root, displayName: "First")
        #expect(ids(registry.recents()) == [firstID, thirdID, secondID])
    }

    // MARK: The open-now marker

    @Test("Opening flags a board, a user close unflags it, and quit deliberately leaves it standing")
    func openNowTracksWindowsAndSurvivesQuit() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let id = registry.recordOpen(of: fixture.root, displayName: "Work")
        #expect(registry.record(id: id)?.isOpenNow == false, "recording an open is not opening a window")
        #expect(registry.restorables().isEmpty)

        registry.setOpenNow(id: id)
        #expect(registry.record(id: id)?.isOpenNow == true)
        #expect(ids(registry.restorables()) == [id])

        // A user close. The flag goes, and with it the board's place in the next launch.
        registry.clearOpenNow(id: id)
        #expect(registry.record(id: id)?.isOpenNow == false)
        #expect(registry.restorables().isEmpty)

        // A quit. The teardown stamps counts and does *not* clear the flag — that omission is the
        // whole restoration mechanism, so it is asserted rather than assumed, and asserted across a
        // reload of the file because a relaunch is what consumes it.
        registry.setOpenNow(id: id)
        registry.recordClose(id: id, displayName: "Work", laneCount: 2, cardCount: 5)

        let afterRelaunch = BoardRegistry(storageURL: storage.url)
        #expect(afterRelaunch.record(id: id)?.isOpenNow == true, "the flags describe what was open at quit")
        #expect(ids(afterRelaunch.restorables()) == [id])
        #expect(afterRelaunch.record(id: id)?.laneCount == 2)
    }

    @Test("Restorables are the flagged records only, oldest first")
    func restorablesReopenInLastOpenedOrder() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }
        let first = try makeBoard()
        defer { first.tearDown() }
        let second = try makeBoard()
        defer { second.tearDown() }
        let third = try makeBoard()
        defer { third.tearDown() }
        let registry = BoardRegistry(storageURL: storage.url)

        let firstID = registry.recordOpen(of: first.root, displayName: "First")
        try await Task.sleep(for: .milliseconds(5))
        let secondID = registry.recordOpen(of: second.root, displayName: "Second")
        try await Task.sleep(for: .milliseconds(5))
        let thirdID = registry.recordOpen(of: third.root, displayName: "Third")

        registry.setOpenNow(id: firstID)
        registry.setOpenNow(id: thirdID)

        // Ascending, the exact inverse of `recents()` — these are reopened in order, so the board
        // opened last at quit opens last again and ends up frontmost.
        #expect(ids(registry.restorables()) == [firstID, thirdID])
        #expect(ids(registry.recents()) == [thirdID, secondID, firstID], "recents is unchanged, and still newest first")
        #expect(!ids(registry.restorables()).contains(secondID), "a board that was not open does not restore")

        // A flagged board whose folder has gone still comes back — classified, not dropped. The
        // launch flow renders it as a failed restoration rather than pretending it was never open.
        third.tearDown()
        let rows = registry.restorables()
        #expect(ids(rows) == [firstID, thirdID])
        guard case .unavailable = rows[1] else {
            Issue.record("expected the deleted board to classify unavailable, got \(rows[1])")
            return
        }
    }

    @Test("A registry file written before the open-now key existed still decodes")
    func oldRegistryFilesDecodeWithoutTheOpenNowKey() async throws {
        let storage = try RegistryStorage()
        defer { storage.tearDown() }

        // Byte-for-byte the shape this file had one milestone ago: no `isOpenNow` anywhere. The
        // struct's evolution rule says a new key must be optional precisely so this file survives —
        // a required key would have failed to decode, quarantined the file, and emptied the user's
        // recents on upgrade.
        let id = UUID()
        let garbage = Data("not a bookmark".utf8).base64EncodedString()
        let json = """
        [
          {
            "bookmark" : "\(garbage)",
            "cardCount" : 9,
            "displayName" : "Archive",
            "id" : "\(id.uuidString)",
            "laneCount" : 4,
            "lastKnownPath" : "/Volumes/Archive/Boards/Archive",
            "lastOpened" : "2026-01-01T09:00:00.000Z",
            "pushOnCommit" : true,
            "remoteLocationWarned" : true
          }
        ]
        """
        try Data(json.utf8).write(to: storage.url)

        let registry = BoardRegistry(storageURL: storage.url)
        #expect(registry.recents().count == 1, "the file decoded; nothing was quarantined")
        #expect(registry.record(id: id)?.isOpenNow == false, "a missing key reads as 'not open'")
        #expect(registry.restorables().isEmpty)
        #expect(registry.record(id: id)?.laneCount == 4, "and every other field survived")

        // And the key writes through from here on.
        registry.setOpenNow(id: id)
        #expect(BoardRegistry(storageURL: storage.url).record(id: id)?.isOpenNow == true)
    }

    // MARK: Bookmarks in a sandboxed host

    @Test("A bookmark is always produced, and resolves back to the same folder")
    func bookmarkCreationAndResolutionRoundTrip() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }

        // The contract the fallback exists for: *some* bookmark is always available. Which flavor is
        // the sandbox's call, and production's answer is the security-scoped one — every board URL
        // there arrives through NSOpenPanel or a drag and already carries access.
        let bookmark = try #require(BoardRegistry.makeBookmark(for: fixture.root))
        print("BoardRegistryTests: bookmark flavor in this test host = \(bookmark.isSecurityScoped ? "security-scoped" : "plain")")

        let resolution = try #require(BoardRegistry.resolve(bookmark.data))
        let target = try #require(FileIdentity(of: fixture.root))
        let resolved = BoardRegistry.withScopedAccess(to: resolution.url) { FileIdentity(of: $0) }
        #expect(resolved == target, "a bookmark of either flavor names the file, not the path")

        // Resolution's own fallback, forced: whichever flavor this host hands out, `resolve` must
        // also cope with a plain bookmark, because a registry file written by a non-sandboxed debug
        // build (or on a host where the security-scoped attempt failed) is full of them. Nothing
        // else in this suite reaches that branch when the sandbox is cooperating.
        let plain = try #require(try? fixture.root.bookmarkData(options: []))
        let plainResolution = try #require(BoardRegistry.resolve(plain))
        #expect(FileIdentity(of: plainResolution.url) == target)
    }
}
