import Foundation
import Testing
@testable import Kanban

/// **The `labels` key, activated** (2026-08-09, Pipeline cards a4462d28 and 28c79ffe) — the schema's
/// reading and writing, the rules of a list of names, the board-wide used-labels universe, the menu's
/// ranking, and the store write that lands it.
///
/// The key's own story is `FrontmatterKeys.labels`: it was a reserved tracker key beside `assignees`,
/// `due` and `remote` until the owner claimed it for first-party use, which is the design event these
/// tests exist to pin. Everything here is a pure seam except the last suite, which writes real bytes.

// MARK: - The name rules

@Suite("Labels ▸ one name")
struct CardLabelNameTests {

    @Test("A name is trimmed, and nothing is not a name")
    func normalization() {
        #expect(CardLabels.normalized("bug") == "bug")
        #expect(CardLabels.normalized("  bug  ") == "bug")
        #expect(CardLabels.normalized("needs review") == "needs review", "interior spaces are the author's")
        #expect(CardLabels.normalized("") == nil)
        #expect(CardLabels.normalized("   ") == nil)
        #expect(CardLabels.normalized("\n\t") == nil)
    }

    @Test("Identity is case-insensitive; display is not")
    func caseFolding() {
        #expect(CardLabels.canonical("Bug") == CardLabels.canonical("bug"))
        #expect(CardLabels.contains("BUG", in: ["bug"]))
        #expect(CardLabels.contains("bug", in: ["Bug"]))
        #expect(!CardLabels.contains("bugs", in: ["bug"]))
        #expect(!CardLabels.contains("  ", in: ["bug"]))
    }
}

// MARK: - The list rules

@Suite("Labels ▸ one list")
struct CardLabelListTests {

    @Test("Duplicates collapse case-insensitively, and the first spelling holds its place")
    func deduplication() {
        #expect(CardLabels.deduplicated(["Bug", "ui", "bug"]) == ["Bug", "ui"])
        #expect(CardLabels.deduplicated([" bug ", "BUG"]) == ["bug"])
        #expect(CardLabels.deduplicated(["a", "", "  ", "b"]) == ["a", "b"])
        #expect(CardLabels.deduplicated([]) == [])
    }

    @Test("Adding appends — never sorts, never respells an existing label")
    func adding() {
        #expect(CardLabels.adding("ui", to: ["bug"]) == ["bug", "ui"])
        // Alphabetically `a` belongs first; the user put it last, so it goes last.
        #expect(CardLabels.adding("a", to: ["z"]) == ["z", "a"])
        #expect(CardLabels.adding("Bug", to: ["bug"]) == ["bug"], "the existing spelling wins")
        #expect(CardLabels.adding("  ", to: ["bug"]) == ["bug"])
    }

    @Test("Removing takes every case-variant, and toggling is the pair")
    func removingAndToggling() {
        #expect(CardLabels.removing("BUG", from: ["bug", "ui"]) == ["ui"])
        #expect(CardLabels.removing("nope", from: ["bug"]) == ["bug"])
        #expect(CardLabels.toggling("ui", in: ["bug"]) == ["bug", "ui"])
        #expect(CardLabels.toggling("UI", in: ["bug", "ui"]) == ["bug"])
        #expect(CardLabels.toggling("ui", in: CardLabels.toggling("ui", in: ["bug"])) == ["bug"], "twice is a no-op")
    }
}

// MARK: - Reading

@Suite("Labels ▸ the schema's reading")
struct CardLabelReadingTests {

    private func document(_ frontmatter: String) throws -> FrontmatterDocument {
        try FrontmatterDocument.parse("---\nschema: 1\n\(frontmatter)---\nBody.\n")
    }

    @Test("Both list spellings read the same")
    func listForms() throws {
        #expect(try document("labels: [bug, ui]\n").labels == .valid(["bug", "ui"]))
        #expect(try document("labels: [\"bug\", \"needs review\"]\n").labels == .valid(["bug", "needs review"]))
        #expect(try document("labels:\n  - bug\n  - ui\n").labels == .valid(["bug", "ui"]))
        #expect(try document("labels: []\n").labels == .valid([]))
    }

    @Test("A bare scalar coerces to one label")
    func scalarCoercion() throws {
        #expect(try document("labels: bug\n").labels == .valid(["bug"]))
        #expect(try document("labels: \"needs review\"\n").labels == .valid(["needs review"]))
        #expect(try document("labels: \"  bug  \"\n").labels == .valid(["bug"]), "trimmed like any other name")
    }

    @Test("Absent, and an explicit null, are both no labels")
    func absence() throws {
        #expect(try document("").labels.isMissing)
        #expect(try document("labels:\n").labels.isMissing)
        #expect(try document("labels: null\n").labels.isMissing)
    }

    @Test("A mapping — and a non-string scalar — have no list reading at all")
    func malformedShapes() throws {
        #expect(try document("labels: {a: 1}\n").labels.isMalformed)
        // Deliberately not coerced to `["3"]`: see `CardLabels.name(of:)`. A card is not labelled
        // with a number because somebody wrote one.
        #expect(try document("labels: 3\n").labels.isMalformed)
        #expect(try document("labels: true\n").labels.isMalformed)
    }

    @Test("Entries with no name reading are skipped, not fatal")
    func nonStringEntries() throws {
        #expect(try document("labels: [bug, 3, {a: 1}, ui]\n").labels == .valid(["bug", "ui"]))
        // A list of nothing but unreadable entries is a list, and a list with no names in it is
        // `.valid([])` — the author wrote the right shape.
        #expect(try document("labels: [{a: 1}]\n").labels == .valid([]))
    }

    @Test("The reading deduplicates case-insensitively, first spelling winning")
    func readingDeduplicates() throws {
        #expect(try document("labels: [Bug, ui, bug]\n").labels == .valid(["Bug", "ui"]))
    }

    @Test("A shape with no reading leaves the coerce tier's trace")
    func coerceTrace() throws {
        let coerced = try document("labels: {a: 1}\n").coercedFields
        #expect(coerced.contains { $0.key == FrontmatterKeys.labels })
        #expect(try document("labels: [bug]\n").coercedFields.isEmpty)
    }

    @Test("`labels` is schema-owned, so Details never draws a second copy of it")
    func schemaOwnership() throws {
        let parsed = try document("labels: [bug]\nproject: lanework\n")
        #expect(FrontmatterKeys.schemaOwned.contains(FrontmatterKeys.labels))
        #expect(parsed.unknownFields.map(\.key) == ["project"])
        #expect(CardDetails.rows(of: parsed).map(\.key) == ["project"])
        // The three still-reserved tracker keys are untouched by the reversal.
        for reserved in [FrontmatterKeys.remote, "assignees", "due"] {
            #expect(!FrontmatterKeys.schemaOwned.contains(reserved), "\(reserved) is still reserved")
        }
    }

    @Test("The snapshot carries the reading on the card itself")
    func theCardCarriesIt() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, "---\nschema: 1\norder: 1024\n---\n")
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 1\nkind: card\norder: 1024\nlabels: [bug, ui]\n---\n")

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes.first?.cards.first?.labels == .valid(["bug", "ui"]))
    }
}

// MARK: - Writing

@Suite("Labels ▸ the canonical write")
struct CardLabelWriteTests {

    private func rewritten(_ frontmatter: String, to names: [String]) throws -> String {
        var document = try FrontmatterDocument.parse("---\nschema: 1\n\(frontmatter)---\nBody.\n")
        document.setLabels(names)
        return document.serialized()
    }

    @Test("Names land as a quoted flow list, in the order given")
    func canonicalForm() throws {
        #expect(try rewritten("", to: ["bug", "ui"]).contains("labels: [\"bug\", \"ui\"]\n"))
        // No sort: the caller's order is the user's order.
        #expect(try rewritten("", to: ["z", "a"]).contains("labels: [\"z\", \"a\"]\n"))
        // Quoting is what makes a name with a comma safe inside a flow collection.
        #expect(try rewritten("", to: ["a, b"]).contains("labels: [\"a, b\"]\n"))
    }

    @Test("A rewrite replaces the value in place and touches nothing else")
    func byteSurgical() throws {
        let after = try rewritten(
            "title: Fix login\nlabels: [old]\nproject: lanework       # agent overlay\n",
            to: ["new"]
        )
        #expect(after == """
        ---
        schema: 1
        title: Fix login
        labels: ["new"]
        project: lanework       # agent overlay
        ---
        Body.

        """)
    }

    @Test("The last label removes the key — the remove-at-default family")
    func emptyRemovesTheKey() throws {
        let after = try rewritten("labels: [bug]\nproject: lanework\n", to: [])
        #expect(!after.contains("labels"))
        #expect(after.contains("project: lanework\n"))
        // And a document that never had the key does not grow one.
        #expect(try rewritten("", to: []) == "---\nschema: 1\n---\nBody.\n")
    }

    @Test("Entries with no name reading ride through, at the tail")
    func preservedEntries() throws {
        #expect(try rewritten("labels: [bug, 3, {a: 1}]\n", to: ["ui"])
            .contains("labels: [\"ui\", 3, {a: 1}]\n"))
        // And they keep the key alive even when every name is gone.
        #expect(try rewritten("labels: [bug, 3]\n", to: []).contains("labels: [3]\n"))
    }

    @Test("A malformed value is replaced outright — the malformed-value-cleared posture")
    func malformedIsReplaced() throws {
        #expect(try rewritten("labels: {a: 1}\n", to: ["bug"]).contains("labels: [\"bug\"]\n"))
        #expect(try rewritten("labels: bug\n", to: ["bug", "ui"]).contains("labels: [\"bug\", \"ui\"]\n"))
    }

    @Test("The write applies the list rules, so no caller can land a duplicate")
    func writeDeduplicates() throws {
        #expect(try rewritten("", to: ["Bug", "bug", "  ", "ui"]).contains("labels: [\"Bug\", \"ui\"]\n"))
    }

    @Test("Write, read, write — the canonical form is a fixed point")
    func roundTrip() throws {
        var document = try FrontmatterDocument.parse("---\nschema: 1\nlabels:\n  - bug\n  - ui\n---\nBody.\n")
        let read = try #require(document.labels.value)
        document.setLabels(read)
        let once = document.serialized()
        var again = try FrontmatterDocument.parse(once)
        again.setLabels(try #require(again.labels.value))
        #expect(again.serialized() == once)
        #expect(again.labels == .valid(["bug", "ui"]))
    }
}

// MARK: - The universe

@Suite("Labels ▸ the used-labels universe")
struct LabelIndexTests {

    private func board(_ labelsPerCard: [[String]], trash: [[String]] = []) throws -> BoardModel {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, "---\nschema: 1\nkind: lane\norder: 1024\n---\n")
        func text(_ labels: [String], order: Int) -> String {
            let list = labels.map { "\"\($0)\"" }.joined(separator: ", ")
            let key = labels.isEmpty ? "" : "labels: [\(list)]\n"
            return "---\nschema: 1\nkind: card\norder: \(order)\n\(key)---\n"
        }
        for (offset, labels) in labelsPerCard.enumerated() {
            try fixture.item("\(Ident.lane1)/\(Self.identifier(offset))", text(labels, order: (offset + 1) * 1024))
        }
        for (offset, labels) in trash.enumerated() {
            try fixture.item(".trash/\(Self.identifier(100 + offset))", text(labels, order: (offset + 1) * 1024))
        }
        return try BoardLoader.load(boardRoot: fixture.root).model
    }

    private static func identifier(_ n: Int) -> String {
        let hex = String(format: "%012x", n)
        return "aaaaaaaa-aaaa-4aaa-8aaa-\(hex)"
    }

    @Test("A board with no labels has an empty universe")
    func empty() throws {
        #expect(LabelIndex.derive(from: try board([[], []])) == LabelIndex.empty)
        #expect(LabelIndex.empty.isEmpty)
        #expect(LabelIndex.empty.alphabetical.isEmpty)
    }

    @Test("Every live card contributes, and frequency is a card count")
    func counting() throws {
        let index = LabelIndex.derive(from: try board([["bug", "ui"], ["bug"], ["bug", "ops"]]))
        #expect(index.names == ["bug", "ops", "ui"], "frequency first, then alphabetical")
        #expect(index.tallies.map(\.count) == [3, 1, 1])
    }

    @Test("The trash counts — deleting the last card carrying a label does not evict it")
    func theTrashCounts() throws {
        let index = LabelIndex.derive(from: try board([["bug"]], trash: [["spike"], ["bug"]]))
        #expect(index.names == ["bug", "spike"])
        #expect(index.tallies.first?.count == 2)
    }

    @Test("Two cards spelling one label two ways are one label, in the first spelling seen")
    func spellingIsBoardOrder() throws {
        let index = LabelIndex.derive(from: try board([["Bug"], ["bug"], ["bug"]]))
        #expect(index.names == ["Bug"])
        #expect(index.tallies.first?.count == 3)
        #expect(index.contains("BUG"))
        #expect(index.canonicalSpelling(of: "bug") == "Bug")
        #expect(index.canonicalSpelling(of: "nope") == nil)
    }

    @Test("The derivation is a total order, so two runs agree")
    func deterministic() throws {
        let model = try board([["b", "a"], ["a", "b"], ["c"]])
        #expect(LabelIndex.derive(from: model) == LabelIndex.derive(from: model))
        #expect(LabelIndex.derive(from: model).names == ["a", "b", "c"])
    }

    @Test("The dialog's listing is alphabetical, not frequency-ordered")
    func alphabetical() throws {
        let index = LabelIndex.derive(from: try board([["zebra", "apple"], ["zebra"]]))
        #expect(index.names == ["zebra", "apple"], "frequency for the menu")
        #expect(index.alphabetical == ["apple", "zebra"], "lookup order for the dialog")
    }
}

// MARK: - The ranking

@Suite("Labels ▸ the menu's twelve")
struct LabelRankingTests {

    private func index(_ pairs: [(String, Int)]) -> LabelIndex {
        LabelIndex(tallies: pairs.map { LabelTally(name: $0.0, count: $0.1) }
            .sorted { $0.count != $1.count ? $0.count > $1.count : $0.name < $1.name })
    }

    @Test("Frequency leads")
    func frequencyLeads() {
        let ranked = LabelRanking.ranked(index([("rare", 1), ("common", 9)]), recents: ["rare"])
        #expect(ranked == ["common", "rare"], "recency does not outrank frequency")
    }

    @Test("Recency breaks a frequency tie, most recent first")
    func recencyBreaksTies() {
        let tied = index([("a", 2), ("b", 2), ("c", 2)])
        #expect(LabelRanking.ranked(tied, recents: ["c", "b"]) == ["c", "b", "a"])
        #expect(LabelRanking.ranked(tied, recents: []) == ["a", "b", "c"], "alphabetical with no MRU")
    }

    @Test("A label the MRU has never seen sorts behind every label it has")
    func unseenSortsLast() {
        let tied = index([("known", 1), ("unknown", 1)])
        #expect(LabelRanking.ranked(tied, recents: ["known"]) == ["known", "unknown"])
    }

    @Test("MRU entries naming labels this board does not use are inert")
    func foreignRecentsAreIgnored() {
        let tied = index([("a", 1), ("b", 1)])
        #expect(LabelRanking.ranked(tied, recents: ["from-another-board", "b"]) == ["b", "a"])
    }

    @Test("Recency matching is case-insensitive, like every other label comparison")
    func recencyFolds() {
        let tied = index([("Bug", 1), ("ui", 1)])
        #expect(LabelRanking.ranked(tied, recents: ["BUG"]) == ["Bug", "ui"])
    }

    @Test("The limit is twelve, and it truncates the ranking rather than the universe")
    func theLimit() {
        let many = index((0 ..< 20).map { (String(format: "label%02d", $0), 20 - $0) })
        #expect(LabelRanking.menuLimit == 12)
        #expect(LabelRanking.ranked(many, recents: []).count == 12)
        #expect(LabelRanking.ranked(many, recents: []).first == "label00", "the most used")
        #expect(LabelRanking.ranked(many, recents: [], limit: 0).isEmpty)
        #expect(LabelRanking.ranked(LabelIndex.empty, recents: ["a"]).isEmpty)
    }

    /// The consequence the ranking's own doc comment names out loud, pinned so it cannot change
    /// silently: a brand-new label does **not** jump the queue on a board with a full menu.
    @Test("A freshly invented label waits its turn behind twelve busier ones")
    func freshLabelsDoNotJumpTheQueue() {
        var pairs = (0 ..< 12).map { (String(format: "busy%02d", $0), 5) }
        pairs.append(("brand-new", 1))
        let ranked = LabelRanking.ranked(index(pairs), recents: ["brand-new"])
        #expect(!ranked.contains("brand-new"))
        #expect(ranked.count == 12)
    }
}

// MARK: - The MRU

@Suite("Labels ▸ the recents list")
struct LabelRecentsTests {

    /// `@MainActor` for the same reason `StyleRecents`' own rule is reached that way: the function is
    /// pure, but it lives on a `@MainActor` class, so the isolation rides along.
    @MainActor
    @Test("Move to front, deduped case-insensitively, capped")
    func theListRule() {
        #expect(LabelRecents.updated([], with: "bug") == ["bug"])
        #expect(LabelRecents.updated(["a", "b"], with: "b") == ["b", "a"])
        #expect(LabelRecents.updated(["Bug", "a"], with: "bug") == ["bug", "a"], "the new spelling lands")
        #expect(LabelRecents.updated(["a"], with: "  ") == ["a"])
        #expect(LabelRecents.updated((0 ..< 30).map(String.init), with: "x", cap: 3) == ["x", "0", "1"])
    }

    @MainActor
    @Test("Recording persists, and an unchanged list writes nothing observable")
    func persistence() {
        let suite = "LabelRecentsTests-\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suite)!
        defer { defaults.removePersistentDomain(forName: suite) }

        let recents = LabelRecents(defaults: defaults)
        #expect(recents.labels.isEmpty)
        recents.record("bug")
        recents.record("ui")
        #expect(recents.labels == ["ui", "bug"])
        recents.record("ui")
        #expect(recents.labels == ["ui", "bug"], "already at the front")
        #expect(LabelRecents(defaults: defaults).labels == ["ui", "bug"], "read back from the domain")
    }

    @MainActor
    @Test("A hand-broken preference is an empty list, never a crash")
    func lenientRead() {
        let suite = "LabelRecentsTests-\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suite)!
        defer { defaults.removePersistentDomain(forName: suite) }
        defaults.set(42, forKey: AppPreferences.labelRecentsKey)
        #expect(LabelRecents(defaults: defaults).labels.isEmpty)
    }
}

// MARK: - The write vocabulary

@Suite("Labels ▸ the write operation")
struct LabelWriteOperationTests {

    @Test("`.relabel` is its own word, enriched with the card's title, and it stamps")
    func theOperation() {
        #expect(WriteOperation.relabel(title: nil).withTitle("Fix login") == .relabel(title: "Fix login"))
        #expect(!WriteOperation.relabel(title: "Fix login").rewritesOrderOnly, "a labels change is content")
        #expect(WriteOperation.relabel(title: "Fix login").description == "relabel 'Fix login'")
        #expect(WriteOperation.relabel(title: nil).description == "relabel")
    }

    @Test("The undo step's field compares the whole list")
    func theExpectedField() throws {
        let document = try FrontmatterDocument.parse("---\nschema: 1\nlabels: [\"bug\", \"ui\"]\n---\n")
        #expect(HistoryStaleness.matches(.labels(["bug", "ui"]), in: document))
        #expect(!HistoryStaleness.matches(.labels(["bug"]), in: document), "a foreign retag stales it")
        #expect(!HistoryStaleness.matches(.labels(nil), in: document))

        let bare = try FrontmatterDocument.parse("---\nschema: 1\n---\n")
        #expect(HistoryStaleness.matches(.labels(nil), in: bare))
        #expect(!HistoryStaleness.matches(.labels([]), in: bare), "an absent key and an empty list differ")

        let broken = try FrontmatterDocument.parse("---\nschema: 1\nlabels: {a: 1}\n---\n")
        #expect(!HistoryStaleness.matches(.labels(nil), in: broken), "a malformed value matches nothing")
    }

    @Test("The menu row and the change journal say the same word")
    func thePhrase() {
        #expect(HistoryPhrase.name(.relabel, kind: .card) == "Relabel Card")
        #expect(HistoryPhrase.Verb.relabel.rawValue == "Relabel")
    }
}

// MARK: - The store write

@MainActor
@Suite("Labels ▸ the store write")
struct LabelStoreWriteTests {

    private func labels(of card: String, lane: String, in fixture: WriterFixture) throws -> FieldValue<[String]> {
        try FrontmatterDocument.parse(fixture.indexText("\(lane)/\(card)")).labels
    }

    /// The reload between two writes is not ceremony: the no-op guard reads the **snapshot**, which is
    /// one reload behind every write the app makes (the one-way flow) — `SetAsHeroTests`' own note.
    private func settle(_ store: BoardStore) async {
        store.handleWatcherEvent(.treeChanged(.appMediated))
        await store.awaitQuiescence()
    }

    @Test("Setting a list writes the canonical form; emptying it takes the key")
    func setThenEmpty() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.setLabels(["bug", "ui"], onCard: clipboardCard1))
        #expect(try labels(of: Ident.card1, lane: Ident.lane1, in: fixture) == .valid(["bug", "ui"]))
        #expect(try fixture.indexText("\(Ident.lane1)/\(Ident.card1)").contains("labels: [\"bug\", \"ui\"]"))

        await settle(store)
        #expect(store.setLabels([], onCard: clipboardCard1))
        #expect(try labels(of: Ident.card1, lane: Ident.lane1, in: fixture).isMissing)
        #expect(store.banners.oneShots.isEmpty)
    }

    @Test("Writing the list a card already has is a no-op — no write, no step")
    func redundantWritesAreFree() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // `Item.rich` ships `labels: [a, b, c]`, so the card already reads exactly this.
        #expect(store.labels(ofCard: clipboardCard1) == ["a", "b", "c"])
        let before = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)")
        #expect(!store.setLabels(["a", "b", "c"], onCard: clipboardCard1))
        #expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == before)

        // And the normalization runs before the comparison, so a differently-spelled no-op is one too.
        #expect(!store.setLabels(["a", "b", "c", "A"], onCard: clipboardCard1))
        #expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == before)
    }

    @Test("A write stamps `modified` and clears foreign attribution, like any content write")
    func itStamps() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.setLabels(["bug"], onCard: clipboardCard1))
        let document = try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Ident.card1)"))
        #expect(abs(try #require(document.modified.value).timeIntervalSinceNow) < 60)
        #expect(document.modifiedBy == .missing, "`Item.rich` wrote `modified-by: claude`")
        #expect(document.title == .valid("First"), "and nothing else moved")
    }

    @Test("The guards are the hero's: the board container, and cards only")
    func theGuards() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(!store.setLabels(["bug"], onCard: clipboardCard3), "in the trash")
        #expect(!store.setLabels(["bug"], onCard: clipboardLane1), "a lane has no labels")
        #expect(!store.setLabels(["bug"], onCard: ItemID(rawValue: Ident.indexless)), "names nothing")
    }

    @Test("The read seam sees both containers, even where the write refuses")
    func readingSpansContainers() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.labels(ofCard: clipboardCard1) == ["a", "b", "c"])
        #expect(store.labels(ofCard: clipboardCard3).isEmpty, "the trash fixture carries no labels key")
        #expect(store.labels(ofCard: ItemID(rawValue: Ident.indexless)).isEmpty)
    }

    @Test("The cached universe is derived at open and re-derived on a landing")
    func theCachedUniverse() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        // Every `Item.rich` card ships `labels: [a, b, c]`; the trash resident ships none.
        #expect(store.labelIndex.names == ["a", "b", "c"])
        #expect(store.labelIndex.tallies.first?.count == 3, "the three `Item.rich` cards; the trash resident has none")

        #expect(store.setLabels(["a", "b", "c", "spike"], onCard: clipboardCard1))
        await settle(store)
        #expect(store.labelIndex.contains("spike"))
        #expect(store.labelIndex.names.last == "spike", "one card carries it, so it ranks last")
    }

    @Test("⌘Z puts the previous list back, and ⇧⌘Z the new one")
    func undoAndRedo() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        #expect(store.setLabels(["bug"], onCard: clipboardCard1))
        #expect(try labels(of: Ident.card1, lane: Ident.lane1, in: fixture) == .valid(["bug"]))
        #expect(history.undoActionName == "Relabel Card")

        history.undo()
        #expect(try labels(of: Ident.card1, lane: Ident.lane1, in: fixture) == .valid(["a", "b", "c"]))

        history.redo()
        #expect(try labels(of: Ident.card1, lane: Ident.lane1, in: fixture) == .valid(["bug"]))
    }

    @Test("Undoing the write that removed the last label restores it; undoing a first write removes the key")
    func undoAcrossTheKeysBoundary() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        // `card4` is `Item.rich` too, so start by clearing it, then undo across the key's removal.
        #expect(store.setLabels([], onCard: clipboardCard4))
        #expect(try labels(of: Ident.card4, lane: Ident.lane2, in: fixture).isMissing)
        history.undo()
        #expect(try labels(of: Ident.card4, lane: Ident.lane2, in: fixture) == .valid(["a", "b", "c"]))
    }
}

// MARK: - The More… dialog's session

@MainActor
@Suite("Labels ▸ the More… dialog's lifecycle")
struct LabelEditorSessionTests {

    @Test("Begin aims it at one card; a second Begin re-aims rather than stacking")
    func beginAndDiscard() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.transient.labelEditor == nil)
        store.transient.beginLabelEditor(forCard: clipboardCard1)
        #expect(store.transient.labelEditor?.cardID == clipboardCard1)
        store.transient.beginLabelEditor(forCard: clipboardCard2)
        #expect(store.transient.labelEditor?.cardID == clipboardCard2, "one dialog, re-aimed")
        store.transient.discardLabelEditor()
        #expect(store.transient.labelEditor == nil)
    }

    @Test("The reload rule takes it down when its card is no longer a live board card")
    func theReloadRule() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let snapshot = try BoardLoader.load(boardRoot: fixture.root).model

        #expect(LabelEditorSession(cardID: clipboardCard1).resolved(against: snapshot) != nil)
        // A trashed card resolves nowhere on the board side, which is exactly right: the write the
        // dialog offers (`BoardStore.setLabels`) refuses a trashed card.
        #expect(LabelEditorSession(cardID: clipboardCard3).resolved(against: snapshot) == nil)
        #expect(LabelEditorSession(cardID: ItemID(rawValue: Ident.indexless))
            .resolved(against: snapshot) == nil)
    }

    @Test("A card that leaves the board while the dialog is open dismisses it")
    func aVanishedCardDismisses() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        store.transient.beginLabelEditor(forCard: clipboardCard1)

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

        #expect(store.transient.labelEditor == nil)
        // And the style session's own lifecycle is untouched by the new neighbour.
        #expect(store.transient.styleEditor == nil)
    }

    @Test("The two sessions are independent — neither `begin` clears the other")
    func theTwoSessionsAreIndependent() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.transient.beginStyleEditor(for: .items([clipboardCard1]))
        store.transient.beginLabelEditor(forCard: clipboardCard1)
        #expect(store.transient.styleEditor != nil)
        #expect(store.transient.labelEditor != nil)
        store.transient.discardLabelEditor()
        #expect(store.transient.styleEditor != nil, "discarding one leaves the other")
    }
}

// MARK: - The menu's funnel

@MainActor
@Suite("Labels ▸ the command funnel")
struct LabelCommandTests {

    private func settle(_ store: BoardStore) async {
        store.handleWatcherEvent(.treeChanged(.appMediated))
        await store.awaitQuiescence()
    }

    @Test("Toggle adds what is missing and removes what is there")
    func toggling() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let recents = LabelRecents(defaults: UserDefaults(suiteName: "LabelCommandTests-\(UUID().uuidString)")!)

        #expect(LabelCommand.toggle("spike", onCard: clipboardCard1, in: store, recents: recents))
        await settle(store)
        #expect(store.labels(ofCard: clipboardCard1) == ["a", "b", "c", "spike"])

        #expect(LabelCommand.toggle("SPIKE", onCard: clipboardCard1, in: store, recents: recents))
        await settle(store)
        #expect(store.labels(ofCard: clipboardCard1) == ["a", "b", "c"], "case-insensitive, like everywhere")
    }

    @Test("An apply records the MRU; a removal deliberately does not")
    func recording() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let recents = LabelRecents(defaults: UserDefaults(suiteName: "LabelCommandTests-\(UUID().uuidString)")!)

        LabelCommand.toggle("spike", onCard: clipboardCard1, in: store, recents: recents)
        #expect(recents.labels == ["spike"])
        await settle(store)

        LabelCommand.toggle("spike", onCard: clipboardCard1, in: store, recents: recents)
        #expect(recents.labels == ["spike"], "taking a label off is not reaching for it")

        // And an add onto a card that already carries it still records — the user reached for it.
        await settle(store)
        LabelCommand.add("a", onCard: clipboardCard1, in: store, recents: recents)
        #expect(recents.labels == ["a", "spike"])
    }

    @Test("A typed name lands in the board's own spelling")
    func spellingResolution() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let recents = LabelRecents(defaults: UserDefaults(suiteName: "LabelCommandTests-\(UUID().uuidString)")!)

        // The board already spells it `a`; typing `A` must not mint a second variant.
        #expect(LabelCommand.resolved("A", in: store) == "a")
        #expect(LabelCommand.resolved("brand-new", in: store) == "brand-new", "a new name is the typist's")
        #expect(LabelCommand.resolved("   ", in: store) == nil)

        LabelCommand.add("A", onCard: clipboardCard2, in: store, recents: recents)
        await settle(store)
        #expect(store.labelIndex.names.allSatisfy { $0 != "A" }, "no second spelling entered the universe")
        #expect(recents.labels == ["a"])
    }
}

// MARK: - The sidebar's picker

@Suite("Labels ▸ the sidebar's add field")
struct CardLabelsPickerTests {

    private let universe = ["bug", "ui", "backend", "needs review"]

    @Test("An empty query offers the board's own vocabulary")
    func emptyQuery() {
        #expect(CardLabelsPicker.suggestions(for: "", universe: universe, existing: []) == universe)
        #expect(CardLabelsPicker.suggestions(for: "   ", universe: universe, existing: []) == universe)
    }

    @Test("Matching is a case-insensitive substring, board search's own rule")
    func matching() {
        #expect(CardLabelsPicker.suggestions(for: "U", universe: universe, existing: []) == ["bug", "ui"])
        #expect(CardLabelsPicker.suggestions(for: "review", universe: universe, existing: []) == ["needs review"])
        #expect(CardLabelsPicker.suggestions(for: "zzz", universe: universe, existing: []).isEmpty)
    }

    @Test("A label the card already carries is never offered")
    func existingIsExcluded() {
        #expect(CardLabelsPicker.suggestions(for: "", universe: universe, existing: ["BUG"])
            == ["ui", "backend", "needs review"])
    }

    @Test("The list is capped")
    func theCap() {
        let many = (0 ..< 30).map { "label\($0)" }
        #expect(CardLabelsPicker.suggestions(for: "label", universe: many, existing: []).count
            == CardLabelsPicker.suggestionLimit)
        #expect(CardLabelsPicker.suggestions(for: "", universe: many, existing: [], limit: 2).count == 2)
    }

    @Test("The create hint fires only for a name the board has never spelled")
    func theCreateHint() {
        #expect(CardLabelsPicker.createsNewLabel("spike", universe: universe))
        #expect(!CardLabelsPicker.createsNewLabel("BUG", universe: universe), "that is an apply, not a create")
        #expect(!CardLabelsPicker.createsNewLabel("  ", universe: universe))
        #expect(!CardLabelsPicker.createsNewLabel("", universe: universe))
    }
}
