import Foundation
import Testing
@testable import Kanban

// Golden fixture-board suite (01-storage-format.md, 02-architecture.md § Testing): real
// on-disk folder trees under `Fixtures/`, one board per tolerated/valid case and one per
// fail-fast case, asserted against `BoardLoader`. Complements `BoardLoaderTests.swift`'s
// synthetic smoke coverage — this suite is the comprehensive, hand-authored, disk-backed
// counterpart.

// MARK: - Bundle resource resolution

/// A tiny anchor class purely so `Bundle(for:)` can find the test bundle — there is no
/// `Bundle.module` in an xcodeproj target (that's an SPM-only convenience).
private final class FixtureBundleAnchor {}

/// The `Fixtures/` folder reference, copied into the test bundle's resources verbatim
/// (`project.yml`'s `KanbanTests` target). Real directories on disk, not synthesized strings.
private func fixturesRoot() -> URL {
    guard let resources = Bundle(for: FixtureBundleAnchor.self).resourceURL else {
        fatalError("test bundle has no resourceURL")
    }
    return resources.appendingPathComponent("Fixtures", isDirectory: true)
}

private func fixtureBoard(_ relativePath: String) -> URL {
    fixturesRoot().appendingPathComponent(relativePath, isDirectory: true)
}

private func loadFixture(_ relativePath: String) throws -> LoadResult {
    try BoardLoader.load(boardRoot: fixtureBoard(relativePath))
}

/// Every `index.md` beneath `root`, found by walking the real tree — used by the round-trip
/// assertions, which don't want to hardcode which files exist.
private func allIndexMdFiles(under root: URL) throws -> [URL] {
    guard let enumerator = FileManager.default.enumerator(
        at: root,
        includingPropertiesForKeys: [.isRegularFileKey],
        options: [.skipsHiddenFiles]
    ) else {
        return []
    }
    var results: [URL] = []
    for case let url as URL in enumerator where url.lastPathComponent == "index.md" {
        results.append(url)
    }
    return results
}

private func iso8601(_ text: String) -> Date {
    guard let date = ISO8601DateFormatter().date(from: text) else {
        fatalError("bad test fixture: '\(text)' is not ISO-8601")
    }
    return date
}

private func expectFixtureFailure(
    _ relativePath: String,
    path: String,
    reasonDescription: String,
    _ matches: (BoardLoadError.Reason) -> Bool
) {
    do {
        _ = try loadFixture(relativePath)
        Issue.record("expected \(relativePath) to fail with \(reasonDescription) at '\(path)', but it loaded")
    } catch let failure as BoardLoadFailure {
        // Each `Malformed/` board is minimal — "one broken thing" — so the aggregate holds exactly
        // one defect, and asserting the count is what keeps that authoring rule true.
        #expect(failure.defects.count == 1, "\(relativePath): expected one defect, got \(failure.defects)")
        let error = failure.primary
        #expect(error.path == path, "\(relativePath): wrong path in error")
        #expect(matches(error.reason), "\(relativePath): expected \(reasonDescription), got \(error.reason)")
    } catch {
        Issue.record("\(relativePath): expected a BoardLoadFailure, got \(error)")
    }
}

// MARK: - Valid/rich-board.kanban

private enum RichBoard {
    static let laneDoing = "10000000-0000-4000-8000-000000000001"
    static let laneDone = "20000000-0000-4000-8000-000000000002"
    static let cardTaxonomy = "30000000-0000-4000-8000-000000000003"
    static let cardSecond = "40000000-0000-4000-8000-000000000004"
    static let cardShip = "50000000-0000-4000-8000-000000000005"
}

struct FixtureRichBoardTests {
    @Test func loadsFullShapeWithStylingAndNoWarnings() throws {
        let result = try loadFixture("Valid/rich-board.kanban")
        let model = result.model

        #expect(model.title.value == "Rich Demo Board")
        #expect(model.background.value == "#1E1E1E")
        #expect(model.icon.value == "rectangle.stack.fill")
        #expect(model.iconColor.value == "purple")
        #expect(model.modifiedBy.value == "claude")
        if case let .mapping(pairs) = model.template {
            #expect(pairs.count == 1)
        } else {
            Issue.record("expected board template to be a mapping, got \(String(describing: model.template))")
        }

        #expect(model.lanes.map(\.id.rawValue) == [RichBoard.laneDoing, RichBoard.laneDone])
        #expect(result.warnings.isEmpty)

        let doing = try #require(model.lanes.first { $0.id.rawValue == RichBoard.laneDoing })
        #expect(doing.title.value == "Doing")
        #expect(doing.width.value == 2)
        #expect(doing.background.value == "#3478F6")
        #expect(doing.cards.map(\.id.rawValue) == [RichBoard.cardTaxonomy, RichBoard.cardSecond])

        let taxonomy = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardTaxonomy })
        #expect(taxonomy.title.value == "Design the fixture taxonomy")
        #expect(taxonomy.modifiedBy.value == "claude")
        #expect(taxonomy.body.contains("attachments/ holds a sketch"))

        let done = try #require(model.lanes.first { $0.id.rawValue == RichBoard.laneDone })
        #expect(done.cards.map(\.id.rawValue) == [RichBoard.cardShip])
        #expect(done.cards[0].title.value == "Ship v1")
    }

    /// `attachments/` is **flat** (01-storage-format.md § Attachments): the card's listing is its
    /// top-level regular files and nothing else. This card's folder holds all four shapes on real
    /// disk — two ordinary files, a hidden one, and a subfolder with a file in it — so the rule is
    /// asserted against a filesystem rather than against a mock.
    ///
    /// The excluded three are excluded for three different reasons and only one of them is stated
    /// in the design doc: subfolders are "tolerated, preserved verbatim … and not surfaced"; the
    /// hidden file is the loader's uniform `.skipsHiddenFiles` stance (a `.DS_Store` is not
    /// anyone's attachment); symlinks are the loader's never-resolve stance, covered in
    /// `BoardLoaderTests` because git cannot carry that shape into a fixture reliably.
    @Test func aCardListsOnlyTheTopLevelFilesOfItsAttachmentsFolder() throws {
        let result = try loadFixture("Valid/rich-board.kanban")
        let doing = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDoing })
        let taxonomy = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardTaxonomy })

        #expect(taxonomy.attachments == ["notes.txt", "sketch.png"])
    }

    /// The overwhelmingly common shape: no `attachments/` folder at all. It reads as an empty
    /// listing, never as a warning or a failure — nothing has been attached yet is an ordinary
    /// state, and it is what makes the face's paperclip indicator absent by default.
    @Test func cardsWithoutAnAttachmentsFolderListNothing() throws {
        let result = try loadFixture("Valid/rich-board.kanban")
        let doing = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDoing })
        let second = try #require(doing.cards.first { $0.id.rawValue == RichBoard.cardSecond })
        let done = try #require(result.model.lanes.first { $0.id.rawValue == RichBoard.laneDone })

        #expect(second.attachments.isEmpty)
        #expect(done.cards.map(\.attachments) == [[]])
        #expect(result.warnings.isEmpty)
    }

    @Test func boardUnknownAndReservedKeysPreserveOrder() throws {
        let result = try loadFixture("Valid/rich-board.kanban")
        // schema-owned keys (schema, title, created, modified, modified-by, background, icon,
        // iconColor) are filtered out; only the agent-overlay and reserved keys remain, in the
        // order they were written.
        #expect(result.model.document.unknownFields.map(\.key) == ["project", "sphere", "template"])
        // `labels` is schema-owned since 2026-08-09 (`FrontmatterKeys.labels`) — filtered out of the
        // list above at every level, and preserved in the document exactly as it always was.
        #expect(result.model.document.rawValue(for: FrontmatterKeys.labels) != nil)
    }

    /// The whole-tree round-trip guarantee (01-storage-format.md § Fractal layout: "the app
    /// never reformats a body it didn't change"): every `index.md` under the rich board parses
    /// and re-serializes to its original bytes, untouched.
    @Test func everyIndexMdInTheTreeRoundTripsByteIdentically() throws {
        let root = fixtureBoard("Valid/rich-board.kanban")
        let files = try allIndexMdFiles(under: root)
        #expect(files.count == 7) // board + 2 lanes + 3 cards + the one comment folder's index.md

        for file in files {
            let text = try String(contentsOf: file, encoding: .utf8)
            let document = try FrontmatterDocument.parse(text)
            #expect(document.serialized() == text, "\(file.path) did not round-trip byte-identically")
        }
    }
}

// MARK: - Valid/interrupted-create.kanban

struct FixtureInterruptedCreateTests {
    @Test func indexlessFoldersAreSkippedNotFailed() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let laneInterrupted = "20000000-0000-4000-8000-000000000002"
        let card = "30000000-0000-4000-8000-000000000003"
        let cardInterrupted = "40000000-0000-4000-8000-000000000004"

        let result = try loadFixture("Valid/interrupted-create.kanban")
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card])

        #expect(result.warnings.count == 2)
        #expect(result.warnings.contains(.missingIndex(path: laneInterrupted)))
        #expect(result.warnings.contains(.missingIndex(path: "\(lane)/\(cardInterrupted)")))
    }
}

// MARK: - Valid/non-uuid-strays.kanban

struct FixtureNonUUIDStraysTests {
    @Test func nonUUIDFoldersAreStraysAtEveryDepthRegardlessOfIndex() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "50000000-0000-4000-8000-000000000005"

        let result = try loadFixture("Valid/non-uuid-strays.kanban")
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card])

        #expect(result.warnings.count == 4)
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "todo-notes")))
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "scratch")))
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/draft")))
        #expect(result.warnings.contains(.nonUUIDFolderIgnored(path: "\(lane)/wip")))
    }
}

// MARK: - Valid/stray-files.kanban

struct FixtureStrayFilesTests {
    @Test func strayFilesEverywhereProduceNoWarningsAndDontAffectTheModel() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "20000000-0000-4000-8000-000000000002"

        let result = try loadFixture("Valid/stray-files.kanban")
        #expect(result.warnings.isEmpty)
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card])
    }

    /// The board's card-level `scratch.md` is the **one** stray this fixture holds that is not
    /// tolerated: the loose-file carve-out (01-storage-format.md § Fractal layout ▸ Rules, settled
    /// 2026-07-28) says a regular file beside a card's `index.md` belongs in `attachments/`. It is
    /// reported on its own channel — never as a `warning`, which is the *tolerance* vocabulary —
    /// and the board-level and lane-level strays around it stay exactly as tolerated as they were.
    ///
    /// **Detection does not mutate**: this is the loader, over a fixture that lives in git, and the
    /// assertion that the file is still there afterwards is the read-only claim stated on the one
    /// tree where a stray write would be visible in `git status`.
    @Test func aCardLevelLooseFileIsReportedForRelocationWithoutBeingTouched() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "20000000-0000-4000-8000-000000000002"

        let result = try loadFixture("Valid/stray-files.kanban")
        #expect(result.warnings.isEmpty)
        #expect(result.looseCardFiles == [
            LooseCardFiles(
                laneID: ItemID(rawValue: lane),
                cardID: ItemID(rawValue: card),
                title: result.model.lanes[0].cards[0].title.value,
                fileNames: ["scratch.md"]
            ),
        ])

        let scratch = fixtureBoard("Valid/stray-files.kanban")
            .appendingPathComponent("\(lane)/\(card)/scratch.md")
        #expect(FileManager.default.fileExists(atPath: scratch.path))
        #expect(!FileManager.default.fileExists(
            atPath: fixtureBoard("Valid/stray-files.kanban")
                .appendingPathComponent("\(lane)/\(card)/attachments").path
        ))
    }
}

// MARK: - Valid/tombstones.kanban

struct FixtureTombstonesTests {
    @Test func tombstonedLaneAndCardStayInTheSnapshotFlagged() throws {
        let laneLive = "10000000-0000-4000-8000-000000000001"
        let laneDead = "20000000-0000-4000-8000-000000000002"
        let cardLive = "30000000-0000-4000-8000-000000000003"
        let cardDead = "40000000-0000-4000-8000-000000000004"
        let cardUnderDeadLane = "50000000-0000-4000-8000-000000000005"

        let result = try loadFixture("Valid/tombstones.kanban")
        // The lane's key is the tolerate tier's since 2026-07-29 — ignored, logged, and the lane
        // loads live (01 § Deletion, lane clause).
        #expect(result.warnings == [.laneLevelDeletedIgnored(path: laneDead)])
        #expect(result.model.lanes.map(\.id.rawValue) == [laneLive, laneDead])

        let live = try #require(result.model.lanes.first { $0.id.rawValue == laneLive })
        #expect(live.isDeleted == false)
        #expect(live.cards.map(\.id.rawValue) == [cardLive, cardDead])
        #expect(live.cards.map(\.isDeleted) == [false, true])

        let dead = try #require(result.model.lanes.first { $0.id.rawValue == laneDead })
        #expect(dead.isDeleted == true)
        #expect(dead.cards.map(\.id.rawValue) == [cardUnderDeadLane])
        // A tombstoned lane doesn't propagate deletion onto its children's own flag — the
        // loader is structural, not recursive; hiding an ancestor's tombstoned subtree is a
        // rendering concern, not a load-time one.
        #expect(dead.cards[0].isDeleted == false)
    }
}

// MARK: - Valid/duplicate-order-tie-break.kanban

struct FixtureDuplicateOrderTieBreakTests {
    @Test func tiedLanesAndTiedCardsBreakByFolderNameAscending() throws {
        let laneA = "10000000-0000-4000-8000-000000000001"
        let laneAAA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
        let laneBBB = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
        // Distinct from `laneA` on purpose: a lane and one of its own cards sharing a UUID is a
        // duplicate id board-wide, which the loader's dedupe withholds (01-storage-format.md
        // § Fractal layout ▸ Rules). It sorts before `cardY` either way, which is all this fixture
        // ever needed of it.
        let cardX = "10000000-0000-4000-8000-00000000000a"
        let cardY = "20000000-0000-4000-8000-000000000002"
        let cardZ = "30000000-0000-4000-8000-000000000003"

        let result = try loadFixture("Valid/duplicate-order-tie-break.kanban")
        // laneA (order 1024) first; laneAAA and laneBBB tie at 2048, broken 'a' < 'b'.
        #expect(result.model.lanes.map(\.id.rawValue) == [laneA, laneAAA, laneBBB])

        let lane = try #require(result.model.lanes.first { $0.id.rawValue == laneA })
        #expect(lane.cards.map(\.id.rawValue) == [cardX, cardY, cardZ])
        #expect(Set(lane.cards.map(\.order)) == [1024])
    }
}

// MARK: - Valid/unknown-key-order.kanban

struct FixtureUnknownKeyOrderTests {
    @Test func unknownKeysPreserveDocumentOrderAtEveryLevel() throws {
        let result = try loadFixture("Valid/unknown-key-order.kanban")
        let model = result.model
        #expect(model.document.unknownFields.map(\.key) == ["project", "sphere", "template", "custom-note"])

        let lane = try #require(model.lanes.first)
        #expect(lane.document.unknownFields.map(\.key) == ["remote-state", "assignees", "due", "custom"])

        let card = try #require(lane.cards.first)
        #expect(card.document.unknownFields.map(\.key) == ["assignees", "due", "remote", "agent-scratch"])
        // `labels` is schema-owned since 2026-08-09 — filtered from the list, read as a field, and
        // still sitting exactly where the file put it.
        #expect(card.document.labels.value != nil)
    }

    @Test func everyIndexMdRoundTripsByteIdentically() throws {
        let root = fixtureBoard("Valid/unknown-key-order.kanban")
        for file in try allIndexMdFiles(under: root) {
            let text = try String(contentsOf: file, encoding: .utf8)
            #expect(try FrontmatterDocument.parse(text).serialized() == text)
        }
    }
}

// MARK: - Valid/coercion.kanban

struct FixtureCoercionTests {
    @Test func wrongTypeScalarsCoerceOrFallBackToDefaultPerField() throws {
        let lane1 = "10000000-0000-4000-8000-000000000001"
        let lane2 = "20000000-0000-4000-8000-000000000002"
        let cardTitleInt = "30000000-0000-4000-8000-000000000003"
        let cardTitleSeq = "40000000-0000-4000-8000-000000000004"
        let cardIconColorInt = "50000000-0000-4000-8000-000000000005"
        let cardBackgroundMap = "60000000-0000-4000-8000-000000000006"
        let cardBackgroundScalar = "70000000-0000-4000-8000-000000000007"
        let cardDeletedBad = "80000000-0000-4000-8000-000000000008"

        let result = try loadFixture("Valid/coercion.kanban")
        let model = result.model

        let laneWidthCoerces = try #require(model.lanes.first { $0.id.rawValue == lane1 })
        #expect(laneWidthCoerces.width == .valid(3)) // width: "3" (quoted string) coerces

        let laneWidthMalformed = try #require(model.lanes.first { $0.id.rawValue == lane2 })
        #expect(laneWidthMalformed.width == .malformed(raw: "1.5")) // non-integer, no sensible width

        func card(_ id: String) throws -> Card {
            try #require(laneWidthCoerces.cards.first { $0.id.rawValue == id })
        }

        #expect(try card(cardTitleInt).title == .valid("2048"))
        #expect(try card(cardTitleSeq).title == .malformed(raw: "[a, b]"))
        #expect(try card(cardIconColorInt).iconColor == .valid("42"))
        // **`background` is a mapping and only a mapping** (01-storage-format.md § Frontmatter,
        // ruled 2026-08-06). So the two background cards say opposite things about one key:
        // `{x: 1}` is a perfectly legal mapping that simply names neither subkey — no colour, no
        // trace, the unknown subkey riding along like any unknown key — while the bare scalar
        // `12345` has no reading at all, which is the golden pin on the retired shape.
        #expect(try card(cardBackgroundMap).background == .missing)
        #expect(try card(cardBackgroundScalar).background == .malformed(raw: "12345"))
        #expect(try card(cardBackgroundScalar).background.rawText == "12345")

        let deletedBad = try card(cardDeletedBad)
        #expect(deletedBad.deleted == .malformed(raw: "definitely-not-a-date"))
        // Presence outranks validity: an unusable timestamp still tombstones.
        #expect(deletedBad.isDeleted == true)
    }
}

// MARK: - Valid/duplicate-top-level-keys.kanban

struct FixtureDuplicateTopLevelKeysTests {
    @Test func lastOccurrenceWinsAtBoardLaneAndCardLevel() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "20000000-0000-4000-8000-000000000002"

        let result = try loadFixture("Valid/duplicate-top-level-keys.kanban")
        #expect(result.warnings.isEmpty) // NOT a fail-fast case, per the newer design-doc rule

        #expect(result.model.title == .valid("Final Name"))

        let loadedLane = try #require(result.model.lanes.first { $0.id.rawValue == lane })
        #expect(loadedLane.order == 4096) // duplicated 'order' — a strict field — still last-wins

        let loadedCard = try #require(loadedLane.cards.first { $0.id.rawValue == card })
        #expect(loadedCard.title == .valid("Second Title"))
    }

    /// Earlier occurrences of a duplicated key are invisible to every read but still preserved
    /// verbatim on disk (01-storage-format.md § Frontmatter) — proven by round-tripping every
    /// file in this board, not just asserting the winning value.
    @Test func earlierOccurrencesSurviveOnDiskViaRoundTrip() throws {
        let root = fixtureBoard("Valid/duplicate-top-level-keys.kanban")
        let files = try allIndexMdFiles(under: root)
        #expect(files.count == 3)
        for file in files {
            let text = try String(contentsOf: file, encoding: .utf8)
            #expect(try FrontmatterDocument.parse(text).serialized() == text)
        }
    }
}

// MARK: - Valid/board-level-deleted.kanban

struct FixtureBoardLevelDeletedTests {
    @Test func boardLevelDeletedIsIgnoredButRestOfBoardLoadsNormally() throws {
        let lane = "10000000-0000-4000-8000-000000000001"
        let card = "20000000-0000-4000-8000-000000000002"

        let result = try loadFixture("Valid/board-level-deleted.kanban")
        #expect(result.warnings.contains(.boardLevelDeletedIgnored))
        #expect(result.model.deleted == .valid(iso8601("2026-01-01T00:00:00Z")))

        // Meaningless at board level, but never blanks the board — the rest loads as usual.
        #expect(result.model.lanes.map(\.id.rawValue) == [lane])
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card])
    }
}

// MARK: - Valid/optional-keys.kanban

/// **The optional-key ruling's golden board** (01-storage-format.md § Frontmatter and § Ordering,
/// re-ruled 2026-07-31): below the board root `order` and `schema` are optional, a missing or
/// unusable `order` reads as append-at-end, and a missing `schema` reads as 1. Every shape that used
/// to have its own board under `Malformed/` lives here instead, as a coercion case.
private enum OptionalKeys {
    static let rankedLane = "10000000-0000-4000-8000-000000000001"
    static let schemalessLane = "40000000-0000-4000-8000-000000000002"
    static let orderlessLane = "30000000-0000-4000-8000-000000000003"

    static let rankedCard = "20000000-0000-4000-8000-000000000001"
    static let minimumCard = "20000000-0000-4000-8000-000000000002"
    static let nullOrderCard = "20000000-0000-4000-8000-000000000003"
    static let nonNumericCard = "20000000-0000-4000-8000-000000000004"
    static let nonFiniteCard = "20000000-0000-4000-8000-000000000005"
}

struct FixtureOptionalKeysTests {

    /// The strip: ranked lanes first in ascending order, then the order-less one — and the
    /// `schema`-less lane is an ordinary ranked lane, since only its `schema` was absent.
    @Test func orderlessLaneSortsAfterEveryRankedOne() throws {
        let model = try loadFixture("Valid/optional-keys.kanban").model
        #expect(model.lanes.map(\.id.rawValue) == [
            OptionalKeys.rankedLane, OptionalKeys.schemalessLane, OptionalKeys.orderlessLane,
        ])
        #expect(model.lanes.map(\.order) == [1024, 2048, 3072])
    }

    /// Four order-less cards behind one ranked one, in folder-name order — the tie-break the ruling
    /// states the reading in, and the accepted cost it names ("two order-less siblings sort by UUID
    /// rather than by intent until touched").
    @Test func orderlessCardsAppendInFolderNameOrder() throws {
        let model = try loadFixture("Valid/optional-keys.kanban").model
        let lane = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.rankedLane })
        #expect(lane.cards.map(\.id.rawValue) == [
            OptionalKeys.rankedCard,
            OptionalKeys.minimumCard,
            OptionalKeys.nullOrderCard,
            OptionalKeys.nonNumericCard,
            OptionalKeys.nonFiniteCard,
        ])
        // `append`'s own arithmetic, which is what makes the reading stampable verbatim.
        #expect(lane.cards.map(\.order) == [1024, 2048, 3072, 4096, 5120])
    }

    /// A missing `schema` below the root reads as 1 — at both levels.
    @Test func missingSchemaBelowTheRootReadsAsOne() throws {
        let model = try loadFixture("Valid/optional-keys.kanban").model
        let lane = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.schemalessLane })
        #expect(lane.schema == 1)

        let ranked = try #require(model.lanes.first { $0.id.rawValue == OptionalKeys.rankedLane })
        let minimum = try #require(ranked.cards.first { $0.id.rawValue == OptionalKeys.minimumCard })
        #expect(minimum.schema == 1)
        #expect(minimum.title == .valid("Minimum Agent Card"))
    }

    /// Every reading leaves a coerce-tier trace: field, path, and the text as written — an absent
    /// key having none to record (01-storage-format.md § Frontmatter, the family posture).
    @Test func everyReadingIsRecordedAsACoercion() throws {
        let result = try loadFixture("Valid/optional-keys.kanban")
        let byPath = Dictionary(
            uniqueKeysWithValues: result.coercedFrontmatter.map { ($0.path, $0.fields) })

        func fields(_ path: String) throws -> [CoercedField] {
            try #require(byPath[path], "no coercion recorded for \(path)")
        }

        #expect(try fields("\(OptionalKeys.orderlessLane)/index.md") == [CoercedField(key: "order", raw: "")])
        #expect(try fields("\(OptionalKeys.schemalessLane)/index.md") == [CoercedField(key: "schema", raw: "")])

        let lane = OptionalKeys.rankedLane
        #expect(try fields("\(lane)/\(OptionalKeys.minimumCard)/index.md") == [
            CoercedField(key: "schema", raw: ""), CoercedField(key: "order", raw: ""),
        ])
        #expect(try fields("\(lane)/\(OptionalKeys.nullOrderCard)/index.md")
            == [CoercedField(key: "order", raw: "")])
        #expect(try fields("\(lane)/\(OptionalKeys.nonNumericCard)/index.md")
            == [CoercedField(key: "order", raw: "banana")])
        #expect(try fields("\(lane)/\(OptionalKeys.nonFiniteCard)/index.md")
            == [CoercedField(key: "order", raw: ".nan")])

        // Coerce-tier means read-side only: nothing here is work, so nothing carries a heal class.
        #expect(result.defects.allSatisfy { $0.healClass == nil })
    }

    /// The whole board loads clean — no warnings, no fail-fast, and every file byte-identical after
    /// a parse/serialize round-trip: the bytes are preserved verbatim, which is the coerce tier's
    /// other half.
    @Test func loadsWithoutWarningsAndRoundTrips() throws {
        let result = try loadFixture("Valid/optional-keys.kanban")
        #expect(result.warnings.isEmpty)

        for file in try allIndexMdFiles(under: fixtureBoard("Valid/optional-keys.kanban")) {
            let text = try String(contentsOf: file, encoding: .utf8)
            #expect(try FrontmatterDocument.parse(text).serialized() == text)
        }
    }
}

// MARK: - Malformed/*.kanban — fail-fast cases

struct FixtureMalformedTests {
    @Test func unparseableYAML() {
        expectFixtureFailure("Malformed/unparseable-yaml.kanban", path: "index.md", reasonDescription: "unparseableYAML") {
            if case .unparseableYAML = $0 { true } else { false }
        }
    }

    /// **The board root's own `schema` is still required** (re-ruled 2026-07-31 — the
    /// this-really-is-a-board gate). Its below-the-root twin is `FixtureOptionalKeysTests`, where
    /// the same absence reads as 1.
    @Test func missingSchema() {
        expectFixtureFailure("Malformed/missing-schema.kanban", path: "index.md", reasonDescription: "missingSchema") {
            $0 == .missingSchema
        }
    }

    @Test func schemaNewerThanApp() {
        expectFixtureFailure(
            "Malformed/schema-newer-than-app.kanban", path: "index.md", reasonDescription: "schemaNewerThanApp(2)"
        ) {
            $0 == .schemaNewerThanApp(found: 2)
        }
    }

    @Test func boardRootMissingIndex() {
        expectFixtureFailure(
            "Malformed/board-root-missing-index.kanban", path: "index.md", reasonDescription: "boardRootMissingIndex"
        ) {
            $0 == .boardRootMissingIndex
        }
    }
}

// MARK: - Malformed/many-defects.kanban — the collect-all board

/// **The loader collects every fail-fast defect in the walk rather than stopping at the first**
/// (01-storage-format.md § Malformed input, settled 2026-07-31 — the decision surface's whole
/// premise: "one aggregated surface presents them all", never a chain of modals).
///
/// Three classes on one board, on real disk: the root's own missing `schema`, a card written by a
/// newer Lanework, and a lane whose frontmatter will not parse.
private enum ManyDefects {
    static let board = "Malformed/many-defects.kanban"

    static let intactLane = "10000000-0000-4000-8000-000000000001"
    static let brokenLane = "30000000-0000-4000-8000-000000000002"
    static let tailLane = "50000000-0000-4000-8000-000000000003"

    static let intactCard = "20000000-0000-4000-8000-000000000001"
    static let newerCard = "20000000-0000-4000-8000-000000000002"
    /// Broken, and behind the broken lane — never enumerated, so never in the aggregate.
    static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009"
    static let tailCard = "60000000-0000-4000-8000-000000000004"
}

struct FixtureManyDefectsTests {

    /// The whole list, in walk order, asserted as a list — the ordering *is* the contract, because it
    /// is what the surface groups top-down and what `primary` reads off.
    @Test func everyFailFastDefectIsCollectedInWalkOrder() {
        do {
            _ = try loadFixture(ManyDefects.board)
            Issue.record("a board with three fail-fast defects loaded")
        } catch let failure as BoardLoadFailure {
            // The paths, as a list, because the *order* is the contract: the root first — its own
            // `schema` is the this-really-is-a-board gate, and the walk continued past it because
            // nothing below reads the root's document — then lanes in folder-name order with each
            // lane's cards inside it.
            #expect(failure.defects.map(\.path) == [
                "index.md",
                "\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md",
                "\(ManyDefects.brokenLane)/index.md",
            ])
            guard failure.defects.count == 3 else { return }
            #expect(failure.defects[0].reason == .missingSchema)
            #expect(failure.defects[1].reason == .schemaNewerThanApp(found: 2))
            // The lane's reason is matched by shape rather than by the parser's exact sentence,
            // which is the YAML engine's wording and not this suite's to pin.
            if case .unparseableYAML = failure.defects[2].reason {} else {
                Issue.record("expected unparseable YAML on the lane, got \(failure.defects[2].reason)")
            }
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }

    /// **A broken lane takes its subtree with it, and that is the designed loop.** The card under the
    /// unparseable lane is broken too and is deliberately absent from the aggregate: it was never
    /// enumerated. Repair the lane, Re-check — "re-runs the whole walk" — and the next aggregate
    /// carries it. Deeper defects surface one repair at a time, by design, not by omission.
    @Test func aBrokenLanesSubtreeIsNeverEnumerated() {
        do {
            _ = try loadFixture(ManyDefects.board)
            Issue.record("a board with three fail-fast defects loaded")
        } catch let failure as BoardLoadFailure {
            #expect(!failure.defects.contains { $0.path.contains(ManyDefects.cardBehindTheBrokenLane) })
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }

    /// `primary` is the walk's first defect — the outermost problem, which is the one a one-line
    /// surface should name.
    @Test func primaryIsTheRootsOwnDefect() {
        do {
            _ = try loadFixture(ManyDefects.board)
            Issue.record("a board with three fail-fast defects loaded")
        } catch let failure as BoardLoadFailure {
            #expect(failure.primary == BoardLoadError(path: "index.md", reason: .missingSchema))
            #expect(failure.description.hasSuffix("(and 2 more)"))
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }

    /// **The root is unskippable** (01-storage-format.md § Malformed input): the surface never offers
    /// Skip there — the root defects have minted repairs, and a newer root schema is Cancel-only — so
    /// a set naming the root's `index.md` is ignored rather than obeyed. Policed in the loader, which
    /// is what makes a board with no root document impossible to construct.
    @Test func aSkipSetNamingTheRootIsIgnored() {
        do {
            _ = try BoardLoader.load(
                boardRoot: fixtureBoard(ManyDefects.board),
                skipping: [
                    "index.md",
                    ".",
                    "\(ManyDefects.intactLane)/\(ManyDefects.newerCard)/index.md",
                    "\(ManyDefects.brokenLane)/index.md",
                ]
            )
            Issue.record("the root's own defect was skipped — a board with no schema loaded")
        } catch let failure as BoardLoadFailure {
            // Exactly the root's, and nothing else: the two below-root entries were honoured.
            #expect(failure.defects == [BoardLoadError(path: "index.md", reason: .missingSchema)])
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }
}

// MARK: - Malformed/skippable-defects.kanban — the skip channel

/// **"Skip is user-consented tolerance, loudly marked"** (01-storage-format.md § Malformed input,
/// ruled 2026-07-31): a skipped item loads the board without it, the file stays on disk untouched,
/// and the opened board carries a warning naming what left. Per-open, never persisted — which this
/// suite gets for free, because the set is an argument.
private enum SkippableDefects {
    static let board = "Malformed/skippable-defects.kanban"

    static let intactLane = "10000000-0000-4000-8000-000000000001"
    static let brokenLane = "30000000-0000-4000-8000-000000000002"
    static let tailLane = "50000000-0000-4000-8000-000000000003"

    static let intactCard = "20000000-0000-4000-8000-000000000001"
    static let newerCard = "20000000-0000-4000-8000-000000000002"
    static let cardBehindTheBrokenLane = "40000000-0000-4000-8000-000000000009"
    static let tailCard = "60000000-0000-4000-8000-000000000004"

    static let newerCardPath = "\(intactLane)/\(newerCard)/index.md"
    static let brokenLanePath = "\(brokenLane)/index.md"
}

struct FixtureSkippableDefectsTests {

    /// Unskipped, the board is an ordinary two-defect refusal — the baseline the skips are measured
    /// against, and the proof the fixture is broken in exactly two places.
    @Test func withoutSkipsTheBoardRefusesWithBothDefects() {
        do {
            _ = try loadFixture(SkippableDefects.board)
            Issue.record("a board with two fail-fast defects loaded")
        } catch let failure as BoardLoadFailure {
            #expect(failure.defects.map(\.path) == [
                SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath,
            ])
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }

    /// Skip both and the board opens — **and each skipped item leaves with its whole subtree**. The
    /// broken lane's own card is perfectly valid and is gone too: that is the honest cost of the
    /// tolerance, and the reason the notice names what left rather than pretending nothing did.
    @Test func skippingEveryDefectLoadsTheBoardWithoutThoseItems() throws {
        let result = try BoardLoader.load(
            boardRoot: fixtureBoard(SkippableDefects.board),
            skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
        )

        #expect(result.model.title.value == "Skippable Defects")
        #expect(result.model.lanes.map(\.id.rawValue) == [
            SkippableDefects.intactLane, SkippableDefects.tailLane,
        ])

        let intact = try #require(result.model.lanes.first { $0.id.rawValue == SkippableDefects.intactLane })
        #expect(intact.cards.map(\.id.rawValue) == [SkippableDefects.intactCard])

        // The subtree went with the lane, valid card and all.
        let everyCard = result.model.lanes.flatMap { $0.cards.map(\.id.rawValue) }
        #expect(!everyCard.contains(SkippableDefects.cardBehindTheBrokenLane))
        #expect(everyCard == [SkippableDefects.intactCard, SkippableDefects.tailCard])
    }

    /// The loud mark: one warning per skip, naming the defect's own path — which is what the surface
    /// row named and what the opened board's notice resolves its Reveal in Finder against.
    @Test func everySkipIsWarnedAbout() throws {
        let result = try BoardLoader.load(
            boardRoot: fixtureBoard(SkippableDefects.board),
            skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
        )

        #expect(result.warnings == [
            .userSkipped(path: SkippableDefects.newerCardPath),
            .userSkipped(path: SkippableDefects.brokenLanePath),
        ])
    }

    /// A partial skip is an ordinary refusal over what is left — the surface's per-item override,
    /// and the reason Skip is a set rather than a switch.
    @Test func skippingOneDefectStillRefusesForTheOther() {
        do {
            _ = try BoardLoader.load(
                boardRoot: fixtureBoard(SkippableDefects.board),
                skipping: [SkippableDefects.newerCardPath]
            )
            Issue.record("the unskipped lane defect did not refuse the load")
        } catch let failure as BoardLoadFailure {
            #expect(failure.defects.map(\.path) == [SkippableDefects.brokenLanePath])
        } catch {
            Issue.record("expected a BoardLoadFailure, got \(error)")
        }
    }

    /// **Skip touches nothing on disk** — "the file stays on disk untouched, tolerated-invisible like
    /// strays". Asserted on the one tree where a stray write would show up in `git status`.
    @Test func aSkippedItemIsLeftExactlyWhereItIs() throws {
        let root = fixtureBoard(SkippableDefects.board)
        let before = try allIndexMdFiles(under: root).map(\.path).sorted()

        _ = try BoardLoader.load(
            boardRoot: root,
            skipping: [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
        )

        #expect(try allIndexMdFiles(under: root).map(\.path).sorted() == before)
        let skipped = root.appendingPathComponent(SkippableDefects.brokenLanePath)
        let text = try String(contentsOf: skipped, encoding: .utf8)
        #expect(text.contains("labels: [red, green"), "the skipped file was rewritten")
    }
}

// MARK: - Memo-vs-cold equivalence, over every golden board

/// **Result-purity with cost unspecified** (02-architecture.md § Live-reload resilience, blessed
/// 2026-07-31: "The loader's contract is result-purity with cost unspecified: same tree in, same
/// snapshot out, and the memo can only change how fast").
///
/// The memo is the one thing in the loader that could make two walks of the same tree disagree, so
/// the claim is stated where the trees are real and hand-authored: every golden board is walked cold,
/// then walked again with the first walk's memo, and the two results are compared whole. Nothing here
/// edits a tree — the mechanism's per-file behaviour is `BoardLoaderParseMemoTests`' subject; this is
/// the equivalence, across every shape the fixture corpus holds.
///
/// The malformed boards are included on purpose. A refusal is a result too, and a memo that changed
/// which defects a walk collected — or their order — would be the worst possible way for this to be
/// wrong, since the decision surface is written directly against that list.
struct FixtureMemoEquivalenceTests {

    /// Every board under `Fixtures/Valid`, by relative path.
    static let validBoards = [
        "Valid/rich-board.kanban",
        "Valid/interrupted-create.kanban",
        "Valid/non-uuid-strays.kanban",
        "Valid/stray-files.kanban",
        "Valid/tombstones.kanban",
        "Valid/duplicate-order-tie-break.kanban",
        "Valid/unknown-key-order.kanban",
        "Valid/coercion.kanban",
        "Valid/duplicate-top-level-keys.kanban",
        "Valid/board-level-deleted.kanban",
        "Valid/optional-keys.kanban",
    ]

    /// Every board under `Fixtures/Malformed`, by relative path.
    static let malformedBoards = [
        "Malformed/board-root-missing-index.kanban",
        "Malformed/missing-schema.kanban",
        "Malformed/unparseable-yaml.kanban",
        "Malformed/schema-newer-than-app.kanban",
        "Malformed/many-defects.kanban",
        "Malformed/skippable-defects.kanban",
    ]

    @Test("A memoized walk of a valid board is the cold walk, whole", arguments: validBoards)
    func aMemoizedWalkMatchesTheColdWalk(board: String) throws {
        let cold = try loadFixture(board)
        let warm = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo)

        #expect(warm.model == cold.model)
        #expect(warm.warnings == cold.warnings)
        #expect(warm.defects == cold.defects)
        #expect(warm.trashKinds == cold.trashKinds)
        // The memo the second walk produced can stand in for the first's, which is what makes the
        // store's chain of reloads self-sustaining rather than degrading walk by walk.
        #expect(warm.memo.count == cold.memo.count)
    }

    @Test("A memoized walk of a valid board opens no index.md at all", arguments: validBoards)
    func aMemoizedWalkReadsNothing(board: String) throws {
        let cold = try loadFixture(board)

        let counter = BoardLoader.ParseCounter()
        _ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: cold.memo, counter: counter)

        #expect(counter.counts.parsed == 0)
        #expect(counter.counts.reused == cold.memo.count)
        #expect(counter.counts.reused > 0, "a fixture with nothing to memoize proves nothing")
    }

    @Test("A memoized walk of a malformed board refuses identically", arguments: malformedBoards)
    func aMemoizedWalkRefusesIdentically(board: String) {
        // A refusing walk hands back no memo — that is the design, not an omission: a defective file
        // is never recorded, and a walk that threw produced no `LoadResult` to carry one on. So the
        // memo under test is the empty one a first walk would offer, and what is pinned is that the
        // memo parameter never moves the aggregate a refusal reports.
        let defects = refusal(board, memo: nil)
        #expect(defects == refusal(board, memo: BoardLoader.ParseMemo()))
        #expect(!defects.isEmpty, "\(board) is in the malformed corpus but loaded")
    }

    /// One malformed board's aggregate, or `[]` where it unexpectedly loaded.
    private func refusal(_ board: String, memo: BoardLoader.ParseMemo?) -> [BoardLoadError] {
        do throws(BoardLoadFailure) {
            _ = try BoardLoader.load(boardRoot: fixtureBoard(board), memo: memo)
            return []
        } catch {
            return error.defects
        }
    }

    /// The malformed corpus' real memo case: a board that refuses is repaired-by-skip into one that
    /// loads, and the memo that produced carries into the next walk without moving the answer.
    @Test("A skipped-open board reloads through its own memo unchanged")
    func aSkippedOpenReloadsUnchanged() throws {
        let skips: Set<String> = [SkippableDefects.newerCardPath, SkippableDefects.brokenLanePath]
        let root = fixtureBoard(SkippableDefects.board)

        let cold = try BoardLoader.load(boardRoot: root, skipping: skips)
        let warm = try BoardLoader.load(boardRoot: root, skipping: skips, memo: cold.memo)

        #expect(warm.model == cold.model)
        #expect(warm.warnings == cold.warnings)
        #expect(warm.defects == cold.defects)
    }
}
