import Foundation
import Testing
@testable import Kanban

// MARK: - Fixtures

// `WriterFixture` lives in `WriterTestSupport.swift` — shared with `WriteFidelityTests.swift`.

private enum Fixture {
    /// Unknown keys in a deliberate order, an own-line comment above and below, an inline
    /// comment, a `modified-by` stamp, and a body — everything one `title` rewrite must leave
    /// exactly as it found it.
    static let rich = """
        ---
        # hand-written header
        schema: 1
        title: Original
        order: 1024
        project: lanework       # agent overlay
        sphere: work
        labels: [a, b, c]
        created: 2026-07-26T16:41:38Z
        modified: 2026-07-26T19:06:55Z
        modified-by: claude
        # trailing note
        ---
        Body text.

        More body — with *markdown*.

        """

    static let minimal = """
        ---
        schema: 1
        order: 1024
        title: Thing
        ---
        Body

        """

    /// The whole frontmatter as one flow mapping: reads fine, has no line per key to rewrite.
    static let flowMapping = "---\n{schema: 1, order: 1024}\n---\nbody\n"

    /// YAML's explicit-key syntax — the key is a sequence, not a name the editor can match.
    static let nonScalarKey = "---\n? [a, b]\n: value\nschema: 1\n---\nbody\n"
}

/// Literal UUID-shaped folder names, so the display-order tie-break is pinned rather than
/// accidental (`Ranks.isOrderedForDisplay`).
private enum Child {
    static let a = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
    static let b = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
    static let c = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
    static let deleted = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
    static let indexless = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"
}

// `Ident` and `Item` live in `WriterTestSupport.swift` — shared with `WriteFidelityTests.swift`.

/// The keys a move or a copy is allowed to have touched; every other line must be byte-identical.
/// `kind` joins them as of 2026-07-29: a file without one gains it on the first app write that
/// rewrites it (the integrity service's on-touch heal), which is a key this write is allowed to have
/// touched exactly like the stamps.
private let rewrittenKeys = [
    FrontmatterKeys.order, FrontmatterKeys.modified, FrontmatterKeys.modifiedBy, FrontmatterKeys.kind,
]

/// A folder's UUID-shaped children keyed by the `title` inside them. A copy remints every folder
/// it materializes, so the file's own content is the only way back to "which card is which".
private func childrenByTitle(of relativePath: String, in fixture: WriterFixture) throws -> [String: String] {
    var byTitle: [String: String] = [:]
    for child in try BoardLoader.directoryCandidates(in: fixture.url(relativePath))
    where BoardLoader.isUUIDShaped(child.lastPathComponent) {
        let name = child.lastPathComponent
        guard fixture.exists("\(relativePath)/\(name)/index.md") else { continue }
        if let title = try FrontmatterDocument.parse(fixture.indexText("\(relativePath)/\(name)")).title.value {
            byTitle[title] = name
        }
    }
    return byTitle
}

// `writeFailure` lives in `WriterTestSupport.swift` — shared with `WriteFidelityTests.swift`.

/// The file's lines minus every line that opens one of `keys` — what a rewrite of exactly those
/// keys has to leave byte-identical.
private func lines(of text: String, excludingKeys keys: [String]) -> [String] {
    text.components(separatedBy: "\n")
        .filter { line in !keys.contains { line.hasPrefix("\($0):") } }
}

// MARK: - Preservation

struct BoardWriterPreservationTests {
    @Test func aTitleEditTouchesOnlyTheTitleAndTheStamps() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.rich)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        let after = try fixture.indexText("card")
        let stamped = [FrontmatterKeys.title, FrontmatterKeys.modified, FrontmatterKeys.modifiedBy]
        #expect(lines(of: after, excludingKeys: stamped) == lines(of: Fixture.rich, excludingKeys: stamped))
        #expect(after.contains("title: Renamed\n"))

        let document = try FrontmatterDocument.parse(after)
        // `labels` left this list on 2026-08-09 — it is schema-owned now (`FrontmatterKeys.labels`),
        // which is exactly why the `keys` and `rawValue` assertions below still name it: the write path
        // preserves it verbatim like any other key it did not touch, and only its *classification*
        // moved.
        #expect(document.unknownFields.map(\.key) == ["project", "sphere"])
        #expect(document.keys == [
            "schema", "title", "order", "project", "sphere", "labels", "created", "modified",
        ])
        #expect(document.rawValue(for: "labels") == "[a, b, c]")
        #expect(after.hasPrefix("---\n# hand-written header\nschema: 1\n"))
        #expect(after.contains("project: lanework       # agent overlay\n"))
        #expect(after.contains("\n# trailing note\n---\n"))
    }

    @Test func aFrontmatterEditLeavesTheBodyByteIdentical() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.rich)
        let body = Data("Body text.\n\nMore body — with *markdown*.\n".utf8)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.order, to: .double(2048))
        }

        let after = try fixture.indexData("card")
        #expect(after.suffix(body.count) == body)
        #expect(try FrontmatterDocument.parse(fixture.indexText("card")).order == .valid(2048))
    }

    @Test func aSuccessfulWriteLeavesNoTempFileBehind() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.minimal)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        #expect(try fixture.entryNames("card") == ["index.md"])
    }

    /// The engine's per-line ending guarantee (01-storage-format.md § Fractal layout ▸ Rules),
    /// verified end to end through the writer: a CRLF file stays CRLF.
    @Test func aCRLFFileKeepsCRLFOnEveryRewrittenLine() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let text = "---\r\nschema: 1\r\norder: 1024\r\ntitle: Thing\r\n"
            + "modified: 2026-01-01T00:00:00Z\r\nmodified-by: claude\r\n---\r\nbody\r\n"
        let folder = try fixture.item("card", text)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        let after = try fixture.indexText("card")
        #expect(after.contains("title: Renamed\r\n"))
        #expect(after.contains("modified: ") && !after.contains("2026-01-01T00:00:00Z"))
        #expect(!after.contains("modified-by"))
        #expect(after.hasSuffix("---\r\nbody\r\n"))
        // No bare LF survives anywhere once the CRLF pairs are taken out.
        #expect(!after.replacingOccurrences(of: "\r\n", with: "").contains("\n"))
    }
}

// MARK: - Stamps

struct BoardWriterStampTests {
    @Test func modifiedIsStampedAndModifiedByIsCleared() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.rich)
        #expect(try FrontmatterDocument.parse(fixture.indexText("card")).modifiedBy == .valid("claude"))

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        let after = try fixture.indexText("card")
        #expect(!after.contains("modified-by"))

        let document = try FrontmatterDocument.parse(after)
        #expect(document.modifiedBy == .missing)
        let modified = try #require(document.modified.value)
        #expect(abs(modified.timeIntervalSinceNow) < 60)
    }

    /// The stamps run *after* the caller's closure, so they always win — a call site cannot
    /// leave a stale `modified` or a re-added `modified-by` behind.
    @Test func theStampsOutrankWhatTheEditsClosureDid() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.minimal)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.modified, to: .date(Date(timeIntervalSince1970: 0)))
            document.set(FrontmatterKeys.modifiedBy, to: .string("claude"))
        }

        let document = try FrontmatterDocument.parse(fixture.indexText("card"))
        #expect(document.modifiedBy == .missing)
        let modified = try #require(document.modified.value)
        #expect(abs(modified.timeIntervalSinceNow) < 60)
    }

    /// A file with no `modified` at all gains one — appended before the closing delimiter.
    @Test func aFileWithoutAModifiedKeyGainsOne() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.minimal)

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        let document = try FrontmatterDocument.parse(fixture.indexText("card"))
        #expect(document.keys == ["schema", "order", "title", "modified"])
        #expect(document.modified.value != nil)
        #expect(document.body == "Body\n")
    }
}

// MARK: - Readable but uneditable

/// 01-storage-format.md § Frontmatter, settled: a frontmatter shape the surgical editor cannot
/// key by spans loads and renders normally, and every app write to it refuses loudly.
struct BoardWriterUneditableTests {
    @Test func ordinaryDocumentsAreEditable() throws {
        #expect(try FrontmatterDocument.parse(Fixture.rich).uneditableShape == nil)
        #expect(try FrontmatterDocument.parse(Fixture.minimal).uneditableShape == nil)
        #expect(try FrontmatterDocument.parse("---\nflow: {a: 1,\nb: 2}\nlast: x\n---\nbody\n").uneditableShape == nil)
        #expect(FrontmatterDocument(body: "new").uneditableShape == nil)
    }

    @Test func aWholeFrontmatterFlowMappingReadsButRefusesWrites() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.flowMapping)

        let document = try FrontmatterDocument.parse(Fixture.flowMapping)
        #expect(document.uneditableShape == .keyWithoutOwnLine)
        #expect(document.schema == .valid(1))
        #expect(document.order == .valid(1024))

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.set(FrontmatterKeys.title, to: .string("Renamed"))
            }
        }
        #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine))
        // No title in `Fixture.flowMapping`, so the pre-flight read leaves it nil.
        #expect(error?.operation == .style(title: nil))
        #expect(error?.path.hasSuffix("card/index.md") == true)
        #expect(error?.description.contains("style") == true)
        #expect(try fixture.indexText("card") == Fixture.flowMapping)
        #expect(try fixture.entryNames("card") == ["index.md"])
    }

    @Test func aNonScalarKeyReadsButRefusesWrites() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.nonScalarKey)

        #expect(try FrontmatterDocument.parse(Fixture.nonScalarKey).uneditableShape == .nonScalarKey)
        #expect(try FrontmatterDocument.parse(Fixture.nonScalarKey).serialized() == Fixture.nonScalarKey)

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.set(FrontmatterKeys.title, to: .string("Renamed"))
            }
        }
        #expect(error?.reason == .uneditableFrontmatter(.nonScalarKey))
        #expect(try fixture.indexText("card") == Fixture.nonScalarKey)
    }

    /// A key whose spacing the span matcher cannot key on lands in the same refusal — the
    /// detection is conservative on purpose.
    @Test func aKeyWithSpaceBeforeItsColonIsUneditable() throws {
        #expect(try FrontmatterDocument.parse("---\nschema : 1\n---\nbody\n").uneditableShape == .keyWithoutOwnLine)
    }
}

// MARK: - Failure paths

struct BoardWriterFailureTests {
    @Test func aFileThatIsNotUTF8IsALoudErrorAndIsLeftAlone() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let latin1 = try #require("---\nschema: 1\ntitle: café\n---\nbody\n".data(using: .isoLatin1))
        let folder = try fixture.item("card", bytes: latin1)

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.set(FrontmatterKeys.title, to: .string("Renamed"))
            }
        }
        guard case let .unreadable(message) = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(message.contains("UTF-8"))
        #expect(try fixture.indexData("card") == latin1)
        #expect(try fixture.entryNames("card") == ["index.md"])
    }

    @Test func aMissingIndexIsALoudError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = fixture.url("card")
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { _ in }
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(try fixture.entryNames("card") == [])
    }

    @Test func unparseableFrontmatterIsALoudError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let text = "---\nschema: 1\n  bad: indent\n---\nbody\n"
        let folder = try fixture.item("card", text)

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { _ in }
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(try fixture.indexText("card") == text)
    }

    /// The atomicity guarantee under an I/O failure: the temp file cannot be created, so the
    /// destination still holds its previous bytes and nothing is left lying around.
    @Test func aFailedWriteLeavesTheFileAndTheFolderExactlyAsTheyWere() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item("card", Fixture.rich)
        let before = try fixture.indexData("card")

        try FileManager.default.setAttributes([.posixPermissions: 0o555], ofItemAtPath: folder.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: folder.path) }

        let error = writeFailure {
            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.set(FrontmatterKeys.title, to: .string("Renamed"))
            }
        }
        guard case .io = error?.reason else {
            Issue.record("expected .io, got \(String(describing: error?.reason))")
            return
        }
        // `updateIndex` read `Fixture.rich` (title: Original) successfully before the write
        // itself failed — title enrichment engages even though the failure is `.io`, not a
        // read/uneditable refusal.
        #expect(error?.operation == .style(title: "Original"))
        #expect(error?.path.hasSuffix("card/index.md") == true)
        #expect(try fixture.indexData("card") == before)
        #expect(try fixture.entryNames("card") == ["index.md"])
    }
}

// MARK: - Renumber

/// 01-storage-format.md § Ordering: the renumber fallback — the sole exception to
/// "a reorder rewrites only the moved item's index.md".
struct BoardWriterRenumberTests {
    /// Crowded orders whose display sequence disagrees with folder-name order, so the assignment
    /// is provably by `order` first and name only as tie-break.
    private func crowdedLane(_ fixture: WriterFixture) throws -> URL {
        try fixture.item("lane/\(Child.a)", child(order: "1.0000003", title: "A"))
        try fixture.item("lane/\(Child.b)", child(order: "1.0000001", title: "B"))
        try fixture.item("lane/\(Child.c)", child(order: "1.0000002", title: "C"))
        try fixture.item(
            "lane/\(Child.deleted)",
            "---\nschema: 1\norder: 0.5\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\nmodified-by: claude\n---\nbody\n"
        )
        // Skipped for the same reasons BoardLoader skips them.
        try fixture.item("lane/notes", "---\nschema: 1\norder: 1\ntitle: Stray\n---\nbody\n")
        try FileManager.default.createDirectory(
            at: fixture.url("lane/\(Child.indexless)"),
            withIntermediateDirectories: true
        )
        return fixture.url("lane")
    }

    private func child(order: String, title: String) -> String {
        """
        ---
        schema: 1
        order: \(order)
        title: \(title)
        project: lanework       # agent overlay
        modified-by: claude
        ---
        \(title) body

        """
    }

    @Test func visibleChildrenAreRenumberedInDisplayOrder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let lane = try crowdedLane(fixture)

        try BoardWriter.renumberVisibleChildren(of: lane)

        let orders = try [Child.b, Child.c, Child.a].map {
            try FrontmatterDocument.parse(fixture.indexText("lane/\($0)")).order
        }
        #expect(orders == [.valid(1024), .valid(2048), .valid(3072)])
        #expect(try fixture.indexText("lane/\(Child.b)").contains("order: 1024\n"))
    }

    /// **A rescale stamps nothing** (01-storage-format.md § Ordering, verbatim: "order-only rewrites,
    /// so no `modified` stamp and no `modified-by` clear"; § Frontmatter ▸ `modified`'s scope, refined
    /// 2026-07-30). Every sibling's file is rewritten and not one of them is stamped — a foreign
    /// `modified-by` survives, which is the pairing read at its sharpest: attribution cannot change when
    /// content didn't.
    ///
    /// The prior version of this test asserted the opposite (`modifiedBy == .missing`, `modified != nil`)
    /// under the pre-2026-07-29 rule that every app write stamps.
    @Test func eachRewrittenChildKeepsItsStampsAndItsUnknownKeys() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let lane = try crowdedLane(fixture)
        let priorModified = try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.a)"))
            .rawValue(for: FrontmatterKeys.modified)

        try BoardWriter.renumberVisibleChildren(of: lane)

        for name in [Child.a, Child.b, Child.c] {
            let text = try fixture.indexText("lane/\(name)")
            let document = try FrontmatterDocument.parse(text)
            #expect(document.modifiedBy == .valid("claude"), "a rescale touches no content, so attribution stands")
            #expect(document.rawValue(for: FrontmatterKeys.modified) == priorModified, "and nothing is stamped")
            #expect(document.unknownFields.map(\.key) == ["project"])
            #expect(text.contains("project: lanework       # agent overlay\n"))
            #expect(document.body.hasSuffix(" body\n"))
            #expect(try fixture.entryNames("lane/\(name)") == ["index.md"])
        }
    }

    @Test func tombstonedAndStrayChildrenAreUntouched() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let lane = try crowdedLane(fixture)
        let tombstone = try fixture.indexData("lane/\(Child.deleted)")
        let stray = try fixture.indexData("lane/notes")

        try BoardWriter.renumberVisibleChildren(of: lane)

        #expect(try fixture.indexData("lane/\(Child.deleted)") == tombstone)
        #expect(try fixture.indexData("lane/notes") == stray)
    }

    /// **An order-less sibling takes part in the rescale rather than stopping it**
    /// (01-storage-format.md § Ordering, re-ruled 2026-07-31): it joins the batch on its
    /// append-at-end reading, so it comes out of the renumber holding a real rank and sitting exactly
    /// where the board was already drawing it — last.
    @Test func aChildWithAnUnusableOrderJoinsTheRenumberAtTheEnd() throws {
        for unusable in ["order: banana", "order: .nan", "order:"] {
            let fixture = try WriterFixture()
            defer { fixture.tearDown() }
            // `b` sorts after `a` by folder name, but it is the *rank* that puts it last here: `a`
            // carries one and `b` does not.
            try fixture.item("lane/\(Child.a)", child(order: "1.0000001", title: "A"))
            try fixture.item("lane/\(Child.b)", "---\nschema: 1\n\(unusable)\ntitle: B\n---\nbody\n")

            try BoardWriter.renumberVisibleChildren(of: fixture.url("lane"))

            #expect(try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.a)")).order == .valid(1024), "\(unusable)")
            #expect(try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.b)")).order == .valid(2048), "\(unusable)")
        }
    }

    /// The order-less sibling sorts *after* every ranked one even when its folder name would put it
    /// first — the whole of the append-at-end reading, seen through the rescale.
    @Test func anOrderlessChildRenumbersLastRegardlessOfFolderName() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("lane/\(Child.a)", "---\nschema: 1\ntitle: A\n---\nbody\n")
        try fixture.item("lane/\(Child.b)", child(order: "512", title: "B"))
        try fixture.item("lane/\(Child.c)", child(order: "1536", title: "C"))

        try BoardWriter.renumberVisibleChildren(of: fixture.url("lane"))

        #expect(try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.b)")).order == .valid(1024))
        #expect(try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.c)")).order == .valid(2048))
        #expect(try FrontmatterDocument.parse(fixture.indexText("lane/\(Child.a)")).order == .valid(3072))
    }

    /// A renumber still runs over *readable* children: a sibling that refuses writes fails the whole
    /// operation before anything has been rewritten.
    @Test func aChildWithAnUnreadableIndexFailsTheWholeRenumber() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("lane/\(Child.a)", child(order: "1.0000001", title: "A"))
        try fixture.item("lane/\(Child.b)", bytes: Data([0xFF, 0xFE, 0x00]))
        let untouched = try fixture.indexData("lane/\(Child.a)")

        let error = writeFailure { try BoardWriter.renumberVisibleChildren(of: fixture.url("lane")) }
        #expect(error?.path.contains(Child.b) == true)
        #expect(error?.operation == .renumberChildren)
        #expect(try fixture.indexData("lane/\(Child.a)") == untouched)
    }

    @Test func aChildWithUneditableFrontmatterFailsTheWholeRenumber() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("lane/\(Child.a)", child(order: "1.0000001", title: "A"))
        try fixture.item("lane/\(Child.b)", Fixture.flowMapping)
        let untouched = try fixture.indexData("lane/\(Child.a)")

        let error = writeFailure { try BoardWriter.renumberVisibleChildren(of: fixture.url("lane")) }
        #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine))
        #expect(try fixture.indexData("lane/\(Child.a)") == untouched)
    }

    @Test func renumberingAnEmptyParentIsANoOp() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try FileManager.default.createDirectory(at: fixture.url("lane"), withIntermediateDirectories: true)
        try BoardWriter.renumberVisibleChildren(of: fixture.url("lane"))
        #expect(try fixture.entryNames("lane") == [])
    }
}

// MARK: - The rank stamps

/// **The write side of the optional-`order` ruling** (01-storage-format.md § Ordering, re-ruled
/// 2026-07-31): "The rank materializes on touch — the first Writer rewrite of the file stamps a real
/// rank, and placement math that must rank an item *relative to* an order-less sibling stamps that
/// sibling inline, inside the gesture's bracket and commit."
///
/// Both stamps write `Ranks.resolvedOrders`' own answer — the rank the loader was already rendering
/// the file at — so every assertion here is also an assertion that nothing moved.
struct BoardWriterRankStampTests {

    private func order(_ fixture: WriterFixture, _ path: String) throws -> FieldValue<Double> {
        try FrontmatterDocument.parse(fixture.indexText(path)).order
    }

    /// The on-touch half: any rewrite at all — here an ordinary title edit — stamps the rank.
    @Test func aRewriteOfAnOrderlessFileStampsItsRank() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\norder: 1024\ntitle: A\n---\nbody\n")
        let folder = try fixture.item("\(Ident.lane1)/\(Child.b)", "---\ntitle: Minimum\n---\nbody\n")

        try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        // 1024 (the ranked sibling) + one gap — where the board was already drawing it.
        #expect(try order(fixture, "\(Ident.lane1)/\(Child.b)") == .valid(2048))
        // The `kind` backfill rides the same write, and the ranked sibling is untouched.
        #expect(try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Child.b)")).kind == .valid("card"))
        #expect(try order(fixture, "\(Ident.lane1)/\(Child.a)") == .valid(1024))
    }

    /// An unusable value heals the same way — the reading is stated over usability, so the stamp is.
    @Test func aRewriteOfAnUnusableOrderStampsOverIt() throws {
        for unusable in ["order: banana", "order: .nan"] {
            let fixture = try WriterFixture()
            defer { fixture.tearDown() }
            let folder = try fixture.item(
                "\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\n\(unusable)\n---\nbody\n")

            try BoardWriter.updateIndex(inItemFolder: folder, operation: .style(title: nil)) { document in
                document.set(FrontmatterKeys.title, to: .string("Renamed"))
            }

            #expect(try order(fixture, "\(Ident.lane1)/\(Child.a)") == .valid(1024), "\(unusable)")
        }
    }

    /// **The board root never gains a rank** — it has no siblings to sit among, and `order` is
    /// meaningless there.
    @Test func theBoardRootIsNeverStamped() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = try fixture.item("board", "---\nschema: 1\nkind: board\ntitle: Board\n---\nbody\n")

        try BoardWriter.updateIndex(inItemFolder: root, kind: .board, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Renamed"))
        }

        #expect(try order(fixture, "board") == .missing)
    }

    /// **The inline half**: a rank write is placement math landing, so the container's order-less
    /// siblings are stamped with the reading the placement was computed against — inside the same
    /// call, which is inside the caller's bracket and commit.
    @Test func aRankWriteStampsItsOrderlessSiblingsInline() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\norder: 1024\ntitle: A\n---\nbody\n")
        try fixture.item("\(Ident.lane1)/\(Child.b)", "---\nschema: 1\nkind: card\ntitle: B\n---\nbody\n")
        let c = try fixture.item("\(Ident.lane1)/\(Child.c)", "---\nschema: 1\nkind: card\ntitle: C\n---\nbody\n")

        // The reading is A=1024, B=2048, C=3072. A drop between B and C is their midpoint — an
        // answer that is only true on disk if B and C hold those ranks.
        try BoardWriter.updateIndex(inItemFolder: c, operation: .reorder(title: nil)) { document in
            document.set(FrontmatterKeys.order, to: .double(2560))
        }

        #expect(try order(fixture, "\(Ident.lane1)/\(Child.a)") == .valid(1024))
        #expect(try order(fixture, "\(Ident.lane1)/\(Child.b)") == .valid(2048))
        #expect(try order(fixture, "\(Ident.lane1)/\(Child.c)") == .valid(2560))
    }

    /// The pass is idempotent and silent on a board this app wrote: nothing order-less, nothing
    /// written, and no sibling's bytes touched.
    @Test func aRankWriteTouchesNoRankedSibling() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\norder: 1024\ntitle: A\n---\nbody\n")
        let b = try fixture.item("\(Ident.lane1)/\(Child.b)", "---\nschema: 1\nkind: card\norder: 2048\ntitle: B\n---\nbody\n")
        let untouched = try fixture.indexData("\(Ident.lane1)/\(Child.a)")

        try BoardWriter.updateIndex(inItemFolder: b, operation: .reorder(title: nil)) { document in
            document.set(FrontmatterKeys.order, to: .double(512))
        }

        #expect(try fixture.indexData("\(Ident.lane1)/\(Child.a)") == untouched)
    }

    /// A create appends *after* the order-less sibling rather than above it — which needs that
    /// sibling stamped first, since `createChild` mints a file instead of rewriting one.
    @Test func aCreateStampsTheOrderlessSiblingItAppendsPast() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\nkind: board\n---\n")
        let lane = try fixture.item(Ident.lane1, "---\nschema: 1\nkind: lane\norder: 1024\n---\n")
        try fixture.item("\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\norder: 1024\ntitle: A\n---\nbody\n")
        try fixture.item("\(Ident.lane1)/\(Child.b)", "---\nschema: 1\nkind: card\ntitle: B\n---\nbody\n")

        let created = try BoardWriter.createCard(inLane: lane, title: "New")

        #expect(try order(fixture, "\(Ident.lane1)/\(Child.b)") == .valid(2048))
        #expect(try order(fixture, "\(Ident.lane1)/\(created.rawValue)") == .valid(3072))

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes[0].cards.map(\.title.value) == ["A", "B", "New"])
    }

    /// A move into a lane stamps the destination's order-less children **before the folder lands**,
    /// so the reading it stamps is the one the caller's placement was computed against — not one
    /// re-based against the arriving item's own foreign rank.
    @Test func aMoveStampsTheDestinationBeforeTheFolderArrives() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\nkind: board\n---\n")
        try fixture.item("\(Ident.lane1)", "---\nschema: 1\nkind: lane\norder: 1024\n---\n")
        try fixture.item("\(Ident.lane2)", "---\nschema: 1\nkind: lane\norder: 2048\n---\n")
        try fixture.item("\(Ident.lane1)/\(Child.a)", "---\nschema: 1\nkind: card\norder: 1024\ntitle: A\n---\nbody\n")
        try fixture.item("\(Ident.lane1)/\(Child.b)", "---\nschema: 1\nkind: card\ntitle: B\n---\nbody\n")
        // The arriving card carries a rank far above anything in the destination — the value that
        // would poison the reading if the stamp ran after the move.
        try fixture.item("\(Ident.lane2)/\(Child.c)", "---\nschema: 1\nkind: card\norder: 99999\ntitle: C\n---\nbody\n")

        // The reading in lane1 is A=1024, B=2048; the drop between them is their midpoint.
        _ = try BoardWriter.moveItem(
            at: fixture.url("\(Ident.lane2)/\(Child.c)"),
            toParent: fixture.url("\(Ident.lane1)"),
            sourceBoardRoot: fixture.root,
            destinationBoardRoot: fixture.root,
            order: 1536
        )

        #expect(try order(fixture, "\(Ident.lane1)/\(Child.b)") == .valid(2048))
        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes[0].cards.map(\.title.value) == ["A", "C", "B"])
    }
}

// MARK: - Loader integration

struct BoardWriterLoaderIntegrationTests {
    /// The temp files are dot-prefixed precisely so `BoardLoader`'s `.skipsHiddenFiles` never
    /// sees a crashed write's residue — not as a stray warning, and not as a candidate.
    @Test func leftoverTempFilesAreInvisibleToTheLoader() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Child.a, "---\nschema: 1\norder: 1024\ntitle: Lane\n---\n")

        for folder in ["", Child.a] {
            let residue = fixture.url(folder).appendingPathComponent(".index.md.lanework-\(UUID().uuidString)")
            try Data("half-written\n".utf8).write(to: residue)
        }

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.warnings.isEmpty)
        #expect(result.model.lanes.map(\.id.rawValue) == [Child.a])
    }

    /// A board written by the writer reloads to the same values it was given.
    @Test func aWrittenBoardReloadsCleanly() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        let lane = try fixture.item(Child.a, "---\nschema: 1\norder: 1024\ntitle: Lane\n---\n")
        try fixture.item("\(Child.a)/\(Child.b)", "---\nschema: 1\norder: 1024\ntitle: Card\n---\nbody\n")

        try BoardWriter.updateIndex(inItemFolder: lane, operation: .style(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string("Doing"))
        }

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.warnings.isEmpty)
        #expect(result.model.lanes.first?.title == .valid("Doing"))
        #expect(result.model.lanes.first?.modifiedBy == .missing)
        #expect(result.model.lanes.first?.modified.value != nil)
        #expect(result.model.lanes.first?.cards.first?.title == .valid("Card"))
    }
}

// MARK: - Create board

/// `BoardWriter.createBoard`: the folder (if missing) plus a freshly minted `index.md` — never
/// a rewrite, so these tests check what a brand-new file looks like rather than what a rewrite
/// preserved.
struct BoardWriterCreateBoardTests {
    @Test func createsAFolderAndAValidIndexTheLoaderOpens() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("MyBoard.kanban")

        try BoardWriter.createBoard(at: root, title: "My Board")

        let result = try BoardLoader.load(boardRoot: root)
        #expect(result.warnings.isEmpty)
        #expect(result.model.schema == 1)
        #expect(result.model.title == .valid("My Board"))
        #expect(result.model.modifiedBy == .missing)

        let created = try #require(result.model.created.value)
        let modified = try #require(result.model.modified.value)
        #expect(abs(created.timeIntervalSinceNow) < 60)
        #expect(abs(modified.timeIntervalSinceNow) < 60)
    }

    /// **`kind` is written at creation of every object** (01-storage-format.md § Frontmatter,
    /// re-ruled 2026-07-29) and goes last, where the common table puts it — and where the on-touch
    /// backfill appends one on an older file, so a created object and a healed one read the same.
    @Test func keyOrderIsSchemaTitleCreatedModifiedKind() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("MyBoard.kanban")

        try BoardWriter.createBoard(at: root, title: "My Board")

        let document = try FrontmatterDocument.parse(fixture.indexText("MyBoard.kanban"))
        #expect(document.keys == ["schema", "title", "created", "modified", "kind"])
        #expect(document.kind == .valid("board"))
    }

    @Test func aNilTitleWritesNoTitleKeyAndTheLoaderReadsItMissing() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("Untitled.kanban")

        try BoardWriter.createBoard(at: root, title: nil)

        let document = try FrontmatterDocument.parse(fixture.indexText("Untitled.kanban"))
        #expect(document.keys == ["schema", "created", "modified", "kind"])
        #expect(!document.contains(FrontmatterKeys.title))

        let result = try BoardLoader.load(boardRoot: root)
        #expect(result.model.title == .missing)
    }

    @Test func refusesToClobberAnExistingBoard() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let existing = "---\nschema: 1\ntitle: Existing\n---\nbody\n"
        let root = try fixture.item("Existing.kanban", existing)

        let error = writeFailure {
            try BoardWriter.createBoard(at: root, title: "New")
        }
        guard case .io = error?.reason else {
            Issue.record("expected .io, got \(String(describing: error?.reason))")
            return
        }
        #expect(error?.path.hasSuffix("Existing.kanban/index.md") == true)
        #expect(try fixture.indexText("Existing.kanban") == existing)
    }

    @Test func anExtensionLessBoardFolderIsCreatedAndOpensSuccessfully() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("PlainBoard")

        try BoardWriter.createBoard(at: root, title: "Plain")

        let result = try BoardLoader.load(boardRoot: root)
        #expect(result.warnings.isEmpty)
        #expect(result.model.title == .valid("Plain"))
    }

    @Test func createdEqualsModifiedAtCreationTime() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("Board.kanban")

        try BoardWriter.createBoard(at: root, title: "Board")

        let document = try FrontmatterDocument.parse(fixture.indexText("Board.kanban"))
        #expect(document.rawValue(for: FrontmatterKeys.created) == document.rawValue(for: FrontmatterKeys.modified))
    }

    /// **Creation-time parity** (ruled 2026-08-09, extending Option C to every `createBoard` caller):
    /// a board is born with a current-version `CLAUDE.md`, with no dependency on a later Mac open to
    /// write the first copy — the gap `AgentGuideStoreTests` doesn't cover, since every fixture there
    /// hand-writes its tree and calls `store.refreshAgentGuide()` directly rather than going through
    /// `createBoard`. `KanbanMobile.BoardIndexStore.createBoard` calls this exact method, so this one
    /// assertion is also the phone's coverage — there is no second code path to test twice.
    @Test("createBoard installs a current-version agent guide, with no dependency on a later open")
    func createBoardInstallsTheCurrentAgentGuide() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("MyBoard.kanban")

        try BoardWriter.createBoard(at: root, title: "My Board")

        let guideText = try String(decoding: fixture.data("MyBoard.kanban/\(AgentGuide.filename)"), as: UTF8.self)
        #expect(AgentGuide.installedVersion(of: guideText) == AgentGuide.version)
        #expect(guideText == AgentGuide.content, "the exact shipped guide, not a hand-rolled write")
        #expect(!fixture.exists("MyBoard.kanban/\(AgentGuide.userFilename)"), "nothing existed to rescue at a brand-new root")
    }

    /// **The twin** (ruled 2026-08-09, card dc1314bb): `createBoard` writes `AGENTS.md` alongside
    /// `CLAUDE.md`, byte-identical — a fresh board never lives even a moment with only one of the two
    /// claimed guide names.
    @Test("createBoard installs the AGENTS.md twin too, byte-identical to CLAUDE.md")
    func createBoardInstallsTheAgentsMdTwin() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("MyBoard.kanban")

        try BoardWriter.createBoard(at: root, title: "My Board")

        let claudeText = try fixture.data("MyBoard.kanban/\(AgentGuide.filename)")
        let agentsText = try fixture.data("MyBoard.kanban/\(AgentGuide.agentsFilename)")
        #expect(agentsText == claudeText, "byte-identical, not merely equal-content")
        #expect(agentsText == Data(AgentGuide.content.utf8))
    }
}

// MARK: - Create lane / card

/// `BoardWriter.createLane`/`createCard`: minting a fresh identity and placing it after the
/// current visible siblings (01-storage-format.md § Ordering). `fixture.root` doubles as the
/// board root for lane creation — `createChild` never reads the parent's own `index.md`, only
/// its children, so a bare directory is as good a board root as a fully-formed one for these
/// tests.
struct BoardWriterCreateChildTests {
    @Test func mintsLowercaseUUIDShapedNamesDistinctAcrossCalls() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        let first = try BoardWriter.createLane(inBoard: fixture.root, title: "Todo")
        let second = try BoardWriter.createLane(inBoard: fixture.root, title: "Doing")

        for id in [first, second] {
            #expect(BoardLoader.isUUIDShaped(id.rawValue))
            #expect(id.rawValue == id.rawValue.lowercased())
        }
        #expect(first != second)
    }

    @Test func laneKeyOrderIsSchemaTitleOrderCreatedModifiedKind() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        let laneID = try BoardWriter.createLane(inBoard: fixture.root, title: "Lane")

        let document = try FrontmatterDocument.parse(fixture.indexText(laneID.rawValue))
        #expect(document.keys == ["schema", "title", "order", "created", "modified", "kind"])
        #expect(document.kind == .valid("lane"))
        #expect(document.schema == .valid(1))
        #expect(document.modifiedBy == .missing)
    }

    @Test func anEmptyParentsFirstChildLandsAt1024() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        let laneID = try BoardWriter.createLane(inBoard: fixture.root, title: "Only")

        let document = try FrontmatterDocument.parse(fixture.indexText(laneID.rawValue))
        #expect(document.order == .valid(1024))
    }

    @Test func appendsAfterVisibleSiblingsAtMaxPlus1024() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item(Child.a, "---\nschema: 1\norder: 1024\ntitle: A\n---\nbody\n")
        try fixture.item(Child.b, "---\nschema: 1\norder: 2048\ntitle: B\n---\nbody\n")

        let newID = try BoardWriter.createLane(inBoard: fixture.root, title: "New")

        let document = try FrontmatterDocument.parse(fixture.indexText(newID.rawValue))
        #expect(document.order == .valid(3072))
    }

    /// The highest-order sibling is tombstoned — tombstones are inert to ordering, so it must
    /// not factor into the append target at all (not `9999 + 1024`, not anything derived from it).
    @Test func aTombstonedHighestOrderSiblingIsExcludedFromTheAppendTarget() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item(Child.a, "---\nschema: 1\norder: 1024\ntitle: A\n---\nbody\n")
        try fixture.item(
            Child.deleted,
            "---\nschema: 1\norder: 9999\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n"
        )

        let newID = try BoardWriter.createLane(inBoard: fixture.root, title: "New")

        let document = try FrontmatterDocument.parse(fixture.indexText(newID.rawValue))
        #expect(document.order == .valid(2048))
    }

    /// A UUID-shaped sibling folder with no `index.md` — an interrupted create — is skipped
    /// during the order scan, not fatal; the new item still lands correctly, and the board as a
    /// whole still loads, surfacing only the loader's ordinary `.missingIndex` warning.
    @Test func aSiblingFolderWithoutIndexIsSkippedNotFatal() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Child.a, "---\nschema: 1\norder: 1024\ntitle: A\n---\nbody\n")
        try FileManager.default.createDirectory(at: fixture.url(Child.indexless), withIntermediateDirectories: true)

        let newID = try BoardWriter.createLane(inBoard: fixture.root, title: "New")

        let document = try FrontmatterDocument.parse(fixture.indexText(newID.rawValue))
        #expect(document.order == .valid(2048))

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.warnings == [.missingIndex(path: Child.indexless)])
        #expect(Set(result.model.lanes.map(\.id.rawValue)) == Set([Child.a, newID.rawValue]))
    }

    /// **A sibling with an unusable `order` no longer fails the create** (01-storage-format.md
    /// § Ordering, re-ruled 2026-07-31): it reads as append-at-end, gets stamped with that reading
    /// inline, and the new lane appends past it — which is what "append after the current visible
    /// siblings" has to mean for the result to survive a reload.
    @Test func aSiblingWithAnUnusableOrderIsStampedAndAppendedPast() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\nkind: board\ntitle: Board\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\norder: 1024\ntitle: Ranked\n---\nbody\n")
        try fixture.item(Ident.lane2, "---\nschema: 1\norder: banana\ntitle: Unusable\n---\nbody\n")

        let newID = try BoardWriter.createLane(inBoard: fixture.root, title: "New")

        #expect(try FrontmatterDocument.parse(fixture.indexText(Ident.lane2)).order == .valid(2048))
        #expect(try FrontmatterDocument.parse(fixture.indexText(newID.rawValue)).order == .valid(3072))

        let model = try BoardLoader.load(boardRoot: fixture.root).model
        #expect(model.lanes.map(\.title.value) == ["Ranked", "Unusable", "New"])
    }

    @Test func aMissingParentFolderIsALoudUnreadableError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let missingParent = fixture.url("does-not-exist")

        let error = writeFailure {
            _ = try BoardWriter.createLane(inBoard: missingParent, title: "New")
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
    }

    @Test func aParentThatIsAFileIsALoudUnreadableError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let filePath = fixture.root.appendingPathComponent("afile")
        try Data("x".utf8).write(to: filePath)

        let error = writeFailure {
            _ = try BoardWriter.createLane(inBoard: filePath, title: "New")
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
    }

    @Test func createdEqualsModifiedAtCreationTime() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        let laneID = try BoardWriter.createLane(inBoard: fixture.root, title: "Lane")

        let document = try FrontmatterDocument.parse(fixture.indexText(laneID.rawValue))
        #expect(document.rawValue(for: FrontmatterKeys.created) == document.rawValue(for: FrontmatterKeys.modified))
    }

    /// `createCard` is `createLane` one level down — a lane folder (need not have its own
    /// `index.md` for this to work; `createChild` never reads the parent's own file) is just as
    /// good a parent as a board root.
    @Test func createCardMintsUnderALaneFolderWithTheSameRules() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let laneID = try BoardWriter.createLane(inBoard: fixture.root, title: "Lane")
        let laneFolder = fixture.url(laneID.rawValue)

        let first = try BoardWriter.createCard(inLane: laneFolder, title: "First")
        let second = try BoardWriter.createCard(inLane: laneFolder, title: "Second")

        #expect(BoardLoader.isUUIDShaped(first.rawValue))
        #expect(first != second)
        let firstDocument = try FrontmatterDocument.parse(fixture.indexText("\(laneID.rawValue)/\(first.rawValue)"))
        let secondDocument = try FrontmatterDocument.parse(fixture.indexText("\(laneID.rawValue)/\(second.rawValue)"))
        #expect(firstDocument.order == .valid(1024))
        #expect(secondDocument.order == .valid(2048))
    }
}

// MARK: - Create round-trip

struct BoardWriterCreateIntegrationTests {
    /// `createBoard` → `createLane` → `createCard` → `BoardLoader.load`: the whole structure
    /// shows up, in append order, exactly as created.
    @Test func createBoardCreateLaneCreateCardRoundTripsThroughTheLoader() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = fixture.url("Trip.kanban")

        try BoardWriter.createBoard(at: root, title: "Trip")
        let lane1 = try BoardWriter.createLane(inBoard: root, title: "Todo")
        let lane2 = try BoardWriter.createLane(inBoard: root, title: "Doing")
        let lane1Folder = root.appendingPathComponent(lane1.rawValue, isDirectory: true)
        let card1 = try BoardWriter.createCard(inLane: lane1Folder, title: "First")
        let card2 = try BoardWriter.createCard(inLane: lane1Folder, title: "Second")

        let result = try BoardLoader.load(boardRoot: root)
        #expect(result.warnings.isEmpty)
        #expect(result.model.title == .valid("Trip"))
        #expect(result.model.lanes.map(\.id.rawValue) == [lane1.rawValue, lane2.rawValue])
        #expect(result.model.lanes[0].title == .valid("Todo"))
        #expect(result.model.lanes[1].title == .valid("Doing"))
        #expect(result.model.lanes[0].cards.map(\.id.rawValue) == [card1.rawValue, card2.rawValue])
        #expect(result.model.lanes[0].cards[0].title == .valid("First"))
        #expect(result.model.lanes[0].cards[1].title == .valid("Second"))
        #expect(result.model.lanes[1].cards.isEmpty)
    }
}

// MARK: - Editability scope: reads vs rewrites

/// The readable-but-uneditable pre-flight belongs only to operations that will *rewrite*
/// siblings. A flow-mapping sibling loads and renders normally (01-storage-format.md §
/// Frontmatter), so it must not block creating a new item beside it — but renumber, which
/// would have to rewrite that very file, refuses up front, before anything is written.
struct BoardWriterEditabilityScopeTests {
    private static let flowSibling = "---\n{schema: 1, order: 5000}\n---\n"

    @Test func createSucceedsBesideAnUneditableSibling() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\n---\n")
        try fixture.item(Child.a, Self.flowSibling)

        let id = try BoardWriter.createLane(inBoard: fixture.root, title: "New")
        let text = try fixture.indexText(id.rawValue)
        #expect(text.contains("order: 6024"))
    }

    @Test func renumberRefusesOnAnUneditableSibling() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\n---\n")
        try fixture.item(Child.a, Self.flowSibling)
        try fixture.item(Child.b, "---\nschema: 1\norder: 1024\n---\n")
        let before = try fixture.indexData(Child.b)

        let error = writeFailure { try BoardWriter.renumberVisibleChildren(of: fixture.root) }
        #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine))
        #expect(error?.path.hasSuffix("\(Child.a)/index.md") == true)
        #expect(try fixture.indexData(Child.b) == before)
        #expect(try fixture.indexText(Child.a) == Self.flowSibling)
    }
}

// MARK: - Move

/// 01-storage-format.md § Fractal layout ▸ Rules, "Identity lifecycle: moves keep the UUID,
/// copies mint fresh ones" — the move half, including the import boundary where a colliding
/// UUID is degraded to a copy.
struct BoardWriterMoveTests {
    /// Board `A.kanban`: two lanes, each holding one card, everything rich enough that a
    /// byte-level assertion means something.
    private func boardA(_ fixture: WriterFixture) throws {
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Todo"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Card One"))
        try fixture.item("A.kanban/\(Ident.lane2)", Item.rich(order: "2048", title: "Doing"))
        try fixture.item("A.kanban/\(Ident.lane2)/\(Ident.card2)", Item.rich(order: "1024", title: "Card Two"))
    }

    /// Board `B.kanban`: a separate identity namespace with two lanes of its own, one of them
    /// holding a card.
    private func boardB(_ fixture: WriterFixture) throws {
        try fixture.item("B.kanban", Item.board)
        try fixture.item("B.kanban/\(Ident.lane3)", Item.rich(order: "1024", title: "Inbox"))
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card3)", Item.rich(order: "1024", title: "Card Three"))
        try fixture.item("B.kanban/\(Ident.lane4)", Item.rich(order: "2048", title: "Done"))
    }

    @discardableResult
    private func move(
        _ fixture: WriterFixture,
        _ source: String,
        to destination: String,
        from sourceBoard: String = "A.kanban",
        into destinationBoard: String = "A.kanban",
        order: Double? = nil
    ) throws -> MoveResult {
        try BoardWriter.moveItem(
            at: fixture.url(source),
            toParent: fixture.url(destination),
            sourceBoardRoot: fixture.url(sourceBoard),
            destinationBoardRoot: fixture.url(destinationBoard),
            order: order
        )
    }

    /// The everyday move: a card to another lane in the same board. The folder travels whole,
    /// its identity travels with it, and the only lines that may differ are the ones the
    /// destination `order` and the app-write stamps own.
    @Test func aCrossLaneMoveKeepsTheIdentityAndRewritesOnlyTheOrderAndStamps() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        let before = try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)")
        let siblingBefore = try fixture.indexData("A.kanban/\(Ident.lane2)/\(Ident.card2)")

        let result = try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)", to: "A.kanban/\(Ident.lane2)")

        #expect(result.id.rawValue == Ident.card1)
        #expect(result.reminted.isEmpty)
        #expect(try fixture.entryNames("A.kanban/\(Ident.lane1)") == ["index.md"])

        let after = try fixture.indexText("A.kanban/\(Ident.lane2)/\(Ident.card1)")
        #expect(lines(of: after, excludingKeys: rewrittenKeys) == lines(of: before, excludingKeys: rewrittenKeys))

        let document = try FrontmatterDocument.parse(after)
        let source = try FrontmatterDocument.parse(before)
        #expect(document.order == .valid(2048))
        #expect(document.created == source.created)
        #expect(document.modifiedBy == .missing)
        #expect(abs(try #require(document.modified.value).timeIntervalSinceNow) < 60)
        #expect(document.unknownFields.map(\.key) == ["project"], "`labels` is schema-owned since 2026-08-09")
        #expect(document.rawValue(for: FrontmatterKeys.labels) == "[a, b, c]", "and still preserved verbatim")
        #expect(after.contains("project: lanework       # agent overlay\n"))

        // The move rewrites exactly one file: a destination sibling is not even opened.
        #expect(try fixture.indexData("A.kanban/\(Ident.lane2)/\(Ident.card2)") == siblingBefore)
        #expect(try BoardLoader.load(boardRoot: fixture.url("A.kanban")).warnings.isEmpty)
    }

    @Test func anExplicitOrderIsWrittenVerbatim() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)

        try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)", to: "A.kanban/\(Ident.lane2)", order: 1536)

        let document = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(Ident.lane2)/\(Ident.card1)"))
        #expect(document.order == .valid(1536))
    }

    /// Boards are independent identity namespaces: an import that collides with nothing is an
    /// ordinary move, UUID and `created` intact.
    @Test func aCrossBoardMoveWithoutACollisionKeepsTheIdentity() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        let before = try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)")

        let result = try move(
            fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)",
            to: "B.kanban/\(Ident.lane3)", into: "B.kanban"
        )

        #expect(result.id.rawValue == Ident.card1)
        #expect(result.reminted.isEmpty)
        #expect(!fixture.exists("A.kanban/\(Ident.lane1)/\(Ident.card1)"))

        let after = try fixture.indexText("B.kanban/\(Ident.lane3)/\(Ident.card1)")
        #expect(lines(of: after, excludingKeys: rewrittenKeys) == lines(of: before, excludingKeys: rewrittenKeys))
        #expect(try FrontmatterDocument.parse(after).order == .valid(2048))

        let result2 = try BoardLoader.load(boardRoot: fixture.url("B.kanban"))
        #expect(result2.warnings.isEmpty)
        #expect(result2.model.lanes.first?.cards.map(\.id.rawValue) == [Ident.card3, Ident.card1])
    }

    /// The import boundary: the arriving UUID already exists elsewhere in the destination board,
    /// so it is degraded to a copy — fresh identity, content untouched, source gone as for any
    /// move.
    @Test func anArrivingIdentityTheDestinationBoardAlreadyHoldsIsReminted() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card1)", Item.rich(order: "2048", title: "Stale Twin"))
        let twinBefore = try fixture.indexData("B.kanban/\(Ident.lane3)/\(Ident.card1)")
        let before = try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)")

        let result = try move(
            fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)",
            to: "B.kanban/\(Ident.lane4)", into: "B.kanban"
        )

        let minted = result.id.rawValue
        #expect(BoardLoader.isUUIDShaped(minted))
        #expect(minted != Ident.card1)
        #expect(result.reminted == [MoveResult.Remint(from: ItemID(rawValue: Ident.card1), to: ItemID(rawValue: minted))])

        // Content arrived intact — a remint renames a folder, it does not edit a file.
        let after = try fixture.indexText("B.kanban/\(Ident.lane4)/\(minted)")
        #expect(lines(of: after, excludingKeys: rewrittenKeys) == lines(of: before, excludingKeys: rewrittenKeys))
        #expect(try FrontmatterDocument.parse(after).title == .valid("Card One"))

        #expect(!fixture.exists("A.kanban/\(Ident.lane1)/\(Ident.card1)"))
        #expect(try fixture.indexData("B.kanban/\(Ident.lane3)/\(Ident.card1)") == twinBefore)
    }

    /// **Identity comparison is UUID-value equality, never string equality** (01-storage-format.md
    /// § Fractal layout ▸ Rules, settled): the destination board already holds the arriving UUID
    /// spelled in *uppercase* — an agent's `uuidgen` card — so the two are one identity and the
    /// import boundary must remint. A verbatim string set would sail straight past this and leave
    /// a duplicate UUID in the board.
    @Test func aCollisionSpelledInADifferentCaseIsStillOneIdentityAndRemints() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)

        let arriving = "abcdef01-2345-6789-abcd-ef0123456789"
        let twin = arriving.uppercased() // The same UUID, as `uuidgen` would have printed it.
        try fixture.item("A.kanban/\(Ident.lane1)/\(arriving)", Item.rich(order: "3072", title: "Card One"))
        try fixture.item("B.kanban/\(Ident.lane3)/\(twin)", Item.rich(order: "2048", title: "Stale Twin"))
        let twinBefore = try fixture.indexData("B.kanban/\(Ident.lane3)/\(twin)")

        let result = try move(
            fixture, "A.kanban/\(Ident.lane1)/\(arriving)",
            to: "B.kanban/\(Ident.lane4)", into: "B.kanban"
        )

        let minted = result.id.rawValue
        #expect(BoardLoader.isUUIDShaped(minted))
        #expect(ItemID(rawValue: minted) != ItemID(rawValue: arriving))
        #expect(ItemID(rawValue: minted) != ItemID(rawValue: twin))
        #expect(result.reminted == [MoveResult.Remint(from: ItemID(rawValue: arriving), to: ItemID(rawValue: minted))])
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane4)/\(minted)")).title
            == .valid("Card One"))

        // The resident twin is untouched, and the source left as for any move.
        #expect(try fixture.indexData("B.kanban/\(Ident.lane3)/\(twin)") == twinBefore)
        #expect(!fixture.exists("A.kanban/\(Ident.lane1)/\(arriving)"))
    }

    /// The same rule one level down: a lane arrives carrying a card whose UUID the destination
    /// board already holds under a different case-spelling — that card, and only that card, is
    /// reminted.
    @Test func aLaneMoveRemintsAChildCollidingOnlyByCaseSpelling() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)

        let arriving = "abcdef01-2345-6789-abcd-ef0123456789"
        let twin = arriving.uppercased()
        try fixture.item("A.kanban/\(Ident.lane1)/\(arriving)", Item.rich(order: "3072", title: "Colliding"))
        try fixture.item("B.kanban/\(Ident.lane3)/\(twin)", Item.rich(order: "2048", title: "B's Own"))

        let result = try move(fixture, "A.kanban/\(Ident.lane1)", to: "B.kanban", into: "B.kanban")

        #expect(result.id.rawValue == Ident.lane1)
        #expect(result.reminted.map(\.from.rawValue) == [arriving])
        let minted = try #require(result.reminted.first?.to.rawValue)
        #expect(fixture.exists("B.kanban/\(Ident.lane1)/\(minted)"))
        #expect(!fixture.exists("B.kanban/\(Ident.lane1)/\(arriving)"))
        // The card that collided with nothing kept its identity.
        #expect(fixture.exists("B.kanban/\(Ident.lane1)/\(Ident.card1)"))
    }

    /// The collision sitting in the very lane being dropped into — the case a move-then-rename
    /// could not repair, because the plain move would fail on the existing name.
    @Test func aCollisionInTheDestinationParentItselfIsStillReminted() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card1)", Item.rich(order: "2048", title: "Stale Twin"))

        let result = try move(
            fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)",
            to: "B.kanban/\(Ident.lane3)", into: "B.kanban"
        )

        #expect(result.id.rawValue != Ident.card1)
        #expect(result.reminted.count == 1)
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane3)/\(result.id.rawValue)")).title
            == .valid("Card One"))
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane3)/\(Ident.card1)")).title
            == .valid("Stale Twin"))
    }

    /// Degradation is per folder at the finest grain: a lane arriving with one colliding card is
    /// still a lane *move*, and only that card is reminted — its bytes untouched, its
    /// non-colliding siblings' identities intact.
    @Test func aLaneMoveRemintsOnlyTheCollidingCard() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Card Two"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card4)", Item.rich(order: "3072", title: "Card Four"))
        // Only this one already exists over in B.
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card2)", Item.rich(order: "2048", title: "B's Own"))
        let laneBefore = try fixture.indexText("A.kanban/\(Ident.lane1)")
        let collidingBefore = try fixture.indexData("A.kanban/\(Ident.lane1)/\(Ident.card2)")
        let keptBefore = try fixture.indexData("A.kanban/\(Ident.lane1)/\(Ident.card1)")

        let result = try move(fixture, "A.kanban/\(Ident.lane1)", to: "B.kanban", into: "B.kanban")

        #expect(result.id.rawValue == Ident.lane1)
        #expect(result.reminted.map(\.from.rawValue) == [Ident.card2])
        let minted = try #require(result.reminted.first?.to.rawValue)
        #expect(BoardLoader.isUUIDShaped(minted))

        // The repair is folder-name-only: the reminted card's file is byte-identical.
        #expect(try fixture.indexData("B.kanban/\(Ident.lane1)/\(minted)") == collidingBefore)
        #expect(try fixture.indexData("B.kanban/\(Ident.lane1)/\(Ident.card1)") == keptBefore)
        #expect(fixture.exists("B.kanban/\(Ident.lane1)/\(Ident.card4)"))
        #expect(!fixture.exists("B.kanban/\(Ident.lane1)/\(Ident.card2)"))

        // The lane's own index.md is the one file the move rewrote.
        let laneAfter = try fixture.indexText("B.kanban/\(Ident.lane1)")
        #expect(lines(of: laneAfter, excludingKeys: rewrittenKeys) == lines(of: laneBefore, excludingKeys: rewrittenKeys))
        #expect(try FrontmatterDocument.parse(laneAfter).order == .valid(3072))
        #expect(try FrontmatterDocument.parse(laneAfter).modifiedBy == .missing)

        #expect(!fixture.exists("A.kanban/\(Ident.lane1)"))
        #expect(try BoardLoader.load(boardRoot: fixture.url("B.kanban")).warnings.isEmpty)
    }

    /// Tombstones are on disk, so they are identities: arriving on top of one would be exactly
    /// the duplicate the import boundary exists to prevent.
    @Test func aCollisionWithATombstonedDestinationItemStillRemints() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        try fixture.item(
            "B.kanban/\(Ident.lane4)/\(Ident.card1)",
            "---\nschema: 1\norder: 1024\ntitle: Gone\ndeleted: 2026-01-01T00:00:00Z\n---\nbody\n"
        )

        let result = try move(
            fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)",
            to: "B.kanban/\(Ident.lane3)", into: "B.kanban"
        )

        #expect(result.id.rawValue != Ident.card1)
        #expect(result.reminted.map(\.from.rawValue) == [Ident.card1])
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane4)/\(Ident.card1)")).deleted.value != nil)
    }

    /// Boards are independent identity namespaces: the same UUID living in another board is not
    /// this move's business — only an import boundary ever looks.
    @Test func aSameBoardMoveNeverRemintsEvenWhenAnotherBoardSharesTheUUID() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try boardB(fixture)
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card1)", Item.rich(order: "2048", title: "Fork"))

        let result = try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)", to: "A.kanban/\(Ident.lane2)")

        #expect(result.id.rawValue == Ident.card1)
        #expect(result.reminted.isEmpty)
        #expect(fixture.exists("A.kanban/\(Ident.lane2)/\(Ident.card1)"))
        #expect(fixture.exists("B.kanban/\(Ident.lane3)/\(Ident.card1)"))
    }

    /// Discover before you write: the move has to rewrite the moved item's `order`, so a file it
    /// cannot round-trip refuses the gesture while the folder is still where it was.
    @Test func anUneditableItemRefusesTheMoveAndTheFolderDoesNotTravel() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card3)", Item.uneditable)

        let error = writeFailure {
            try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card3)", to: "A.kanban/\(Ident.lane2)")
        }
        #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine))
        // The pre-flight read the source's document (`Item.uneditable`, title: Odd) before the
        // shape refusal, so the title survives into the thrown error.
        #expect(error?.operation == .move(title: "Odd"))
        #expect(try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card3)") == Item.uneditable)
        #expect(try fixture.entryNames("A.kanban/\(Ident.lane2)") == [Ident.card2, "index.md"])
    }

    @Test func aMissingDestinationParentRefusesTheMove() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        let before = try fixture.indexData("A.kanban/\(Ident.lane1)/\(Ident.card1)")

        let error = writeFailure {
            try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)", to: "A.kanban/nowhere")
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(try fixture.indexData("A.kanban/\(Ident.lane1)/\(Ident.card1)") == before)
    }

    @Test func aDestinationParentThatIsAFileRefusesTheMove() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try fixture.file("A.kanban/notes.txt", Data("x".utf8))

        let error = writeFailure {
            try move(fixture, "A.kanban/\(Ident.lane1)/\(Ident.card1)", to: "A.kanban/notes.txt")
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(fixture.exists("A.kanban/\(Ident.lane1)/\(Ident.card1)"))
    }

    /// Level detection is by name shape, so a stray is not an item: moving one would invent an
    /// identity the loader would go on ignoring.
    @Test func aStrayFolderIsNotAnItemAndCannotBeMoved() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try boardA(fixture)
        try fixture.item("A.kanban/\(Ident.lane1)/notes", Item.rich(order: "1024", title: "Stray"))

        let error = writeFailure {
            try move(fixture, "A.kanban/\(Ident.lane1)/notes", to: "A.kanban/\(Ident.lane2)")
        }
        guard case let .unreadable(message) = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(message.contains("UUID-shaped"))
        #expect(fixture.exists("A.kanban/\(Ident.lane1)/notes"))
    }
}

// MARK: - Copy

/// 01-storage-format.md § Fractal layout ▸ Rules, "Identity lifecycle" — the copy half: fresh
/// UUIDs for every folder materialized, `created` kept (a fork) or restamped (born from a
/// template), `modified-by` cleared because a copy is an app write.
struct BoardWriterCopyTests {
    /// One board, one lane, two cards — the source of every copy below.
    private func board(_ fixture: WriterFixture) throws {
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Todo"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Card One"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Card Two"))
        try fixture.item("A.kanban/\(Ident.lane2)", Item.rich(order: "2048", title: "Doing"))
    }

    /// Content that must travel verbatim because the copy never reads it: two attachments (one
    /// in a subfolder the app never creates but preserves) and a stray file.
    private func attach(_ fixture: WriterFixture, to cardPath: String) throws {
        try fixture.file("\(cardPath)/attachments/sketch.png", Data([0x89, 0x50, 0x4E, 0x47, 0x00, 0xFF]))
        try fixture.file("\(cardPath)/attachments/sub/deep.bin", Data([0x00, 0x01, 0x02, 0xFE]))
        try fixture.file("\(cardPath)/notes.txt", Data("hand-written\n".utf8))
    }

    /// The ⌥-drag duplicate: a card copied beside itself. Fresh identity, `created` kept,
    /// `modified` stamped, `modified-by` cleared, everything else byte-identical.
    @Test func aCardCopyMintsAFreshIdentityAndForksTheStamps() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        let source = "A.kanban/\(Ident.lane1)/\(Ident.card1)"
        let before = try fixture.indexData(source)

        let id = try BoardWriter.copyItem(
            at: fixture.url(source),
            toParent: fixture.url("A.kanban/\(Ident.lane1)"),
            order: nil,
            stamps: .fork
        )

        #expect(BoardLoader.isUUIDShaped(id.rawValue))
        #expect(id.rawValue != Ident.card1)

        let copy = try fixture.indexText("A.kanban/\(Ident.lane1)/\(id.rawValue)")
        let original = String(decoding: before, as: UTF8.self)
        #expect(lines(of: copy, excludingKeys: rewrittenKeys) == lines(of: original, excludingKeys: rewrittenKeys))

        let document = try FrontmatterDocument.parse(copy)
        let sourceDocument = try FrontmatterDocument.parse(original)
        #expect(document.order == .valid(3072))
        #expect(document.created == sourceDocument.created)
        #expect(document.modifiedBy == .missing)
        #expect(abs(try #require(document.modified.value).timeIntervalSinceNow) < 60)
        #expect(document.unknownFields.map(\.key) == ["project"], "`labels` is schema-owned since 2026-08-09")
        #expect(document.rawValue(for: FrontmatterKeys.labels) == "[a, b, c]", "and still preserved verbatim")
        #expect(document.body == "Card One body — with *markdown*.\n")

        // The source is never touched, on any path.
        #expect(try fixture.indexData(source) == before)
    }

    @Test func anExplicitOrderIsWrittenVerbatim() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card1)"),
            toParent: fixture.url("A.kanban/\(Ident.lane2)"),
            order: 512,
            stamps: .fork
        )

        let document = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(Ident.lane2)/\(id.rawValue)"))
        #expect(document.order == .valid(512))
    }

    /// A copied lane's cards are new cards, not the same cards seen twice: every folder the copy
    /// materialized has a fresh identity, at every depth.
    @Test func aLaneCopyMintsFreshIdentitiesAtEveryLevel() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .fork
        )

        #expect(id.rawValue != Ident.lane1)
        let copied = try childrenByTitle(of: "A.kanban/\(id.rawValue)", in: fixture)
        #expect(Set(copied.keys) == ["Card One", "Card Two"])
        #expect(Set(copied.values).isDisjoint(with: [Ident.card1, Ident.card2]))
        #expect(copied.values.allSatisfy(BoardLoader.isUUIDShaped))

        // Ranks travel with the cards; only the copied *root* gets a new one.
        let one = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(id.rawValue)/\(try #require(copied["Card One"]))"))
        let two = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(id.rawValue)/\(try #require(copied["Card Two"]))"))
        #expect(one.order == .valid(1024))
        #expect(two.order == .valid(2048))
        #expect(one.created == .valid(try #require(FrontmatterDocument.parse(Item.rich(order: "1024", title: "x")).created.value)))
        #expect(one.modifiedBy == .missing)
        #expect(two.modifiedBy == .missing)
        #expect(try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(id.rawValue)")).order == .valid(3072))

        let result = try BoardLoader.load(boardRoot: fixture.url("A.kanban"))
        #expect(result.warnings.isEmpty)
        #expect(result.model.lanes.map(\.id.rawValue) == [Ident.lane1, Ident.lane2, id.rawValue])
    }

    /// Attachments, their subfolders, and strays travel byte-for-byte — the copy never opens
    /// them, so there is nothing to get wrong.
    @Test func attachmentsSubfoldersAndStraysTravelByteIdentically() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        try attach(fixture, to: "A.kanban/\(Ident.lane1)/\(Ident.card1)")

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .fork
        )

        let copied = try childrenByTitle(of: "A.kanban/\(id.rawValue)", in: fixture)
        let card = try #require(copied["Card One"])
        for path in ["attachments/sketch.png", "attachments/sub/deep.bin", "notes.txt"] {
            #expect(
                try fixture.data("A.kanban/\(id.rawValue)/\(card)/\(path)")
                    == fixture.data("A.kanban/\(Ident.lane1)/\(Ident.card1)/\(path)")
            )
        }
        // `attachments/` is not UUID-shaped, so the remint walk never descends into it.
        #expect(try fixture.entryNames("A.kanban/\(id.rawValue)/\(card)").contains("attachments"))
    }

    /// A symlink surviving a copy as a symlink — never resolved, never followed to its target's
    /// bytes — is `FileManager.copyItem`'s own default behavior (01-storage-format.md § Fractal
    /// layout ▸ Rules, "Symlinks are never traversed": "Copy flows copy the link itself, never its
    /// target"). Nothing in `copyItem` arranges this on purpose — it falls out of leaning on
    /// `FileManager.copyItem` for the whole subtree rather than reading files one by one — so this
    /// pins the behavior against a future change (a different copy mechanism, a Foundation
    /// update) silently starting to dereference links instead.
    @Test func copyItemPreservesASymlinkVerbatimInsideACopiedSubtree() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        let cardPath = "A.kanban/\(Ident.lane1)/\(Ident.card1)"
        try fixture.file("\(cardPath)/attachments/real.png", Data([0x89, 0x50, 0x4E, 0x47]))
        try FileManager.default.createSymbolicLink(
            atPath: fixture.url("\(cardPath)/attachments/linked.png").path,
            withDestinationPath: "real.png"
        )

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .fork
        )

        let copied = try childrenByTitle(of: "A.kanban/\(id.rawValue)", in: fixture)
        let card = try #require(copied["Card One"])
        let copiedLink = fixture.url("A.kanban/\(id.rawValue)/\(card)/attachments/linked.png")

        let values = try copiedLink.resourceValues(forKeys: [.isSymbolicLinkKey])
        #expect(values.isSymbolicLink == true)
        #expect(try FileManager.default.destinationOfSymbolicLink(atPath: copiedLink.path) == "real.png")
        // The source link is untouched too — a copy never touches what it reads from.
        #expect(
            try FileManager.default.destinationOfSymbolicLink(
                atPath: fixture.url("\(cardPath)/attachments/linked.png").path
            ) == "real.png"
        )
    }

    /// Template instantiation: born today, not forked — `created` and `modified` both fresh, and
    /// the same `Date` for the whole tree.
    @Test func bornStampsCreatedAndModifiedFreshAtEveryLevel() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .born
        )

        let copied = try childrenByTitle(of: "A.kanban/\(id.rawValue)", in: fixture)
        let paths = ["A.kanban/\(id.rawValue)"] + copied.values.map { "A.kanban/\(id.rawValue)/\($0)" }
        for path in paths {
            let document = try FrontmatterDocument.parse(fixture.indexText(path))
            let created = try #require(document.created.value)
            let modified = try #require(document.modified.value)
            #expect(abs(created.timeIntervalSinceNow) < 60)
            #expect(abs(created.timeIntervalSince(modified)) < 2)
            #expect(document.modifiedBy == .missing)
        }
    }

    /// **A copy is a transaction** (01-storage-format.md § Frontmatter, ruled 2026-07-29): a nested
    /// card the surgical editor cannot key refuses the **whole** copy, naming that card, and nothing is
    /// materialized at the destination.
    ///
    /// This replaced the former nested leniency, which copied such a card verbatim — stale `modified-by`
    /// and all — and stamped its siblings normally. The kindness was the one verdict 01's doctrine
    /// forbids: "proceed partially, lose a little" is never a verdict, and a silently unstamped
    /// descendant now also carries a live tracker claim it has no right to (the `remote` sever).
    @Test func aNestedUneditableFileRefusesTheWholeCopyNamingIt() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        // `Item.uneditable`'s own title, so the refusal has a name to carry.
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card3)", Item.uneditable)
        let before = try fixture.entryNames("A.kanban")

        let error = writeFailure {
            _ = try BoardWriter.copyItem(
                at: fixture.url("A.kanban/\(Ident.lane1)"),
                toParent: fixture.url("A.kanban"),
                order: nil,
                stamps: .fork
            )
        }

        let failure = try #require(error)
        #expect(failure.operation == .copy(title: "Odd"), "the refusal names the offending item")
        if case .uneditableFrontmatter = failure.reason {} else {
            Issue.record("expected the uneditable-shape refusal, got \(failure.reason)")
        }
        #expect(failure.path.contains(Ident.card3), "and the offending file's own path")
        #expect(try fixture.entryNames("A.kanban") == before, "nothing was materialized")
    }

    /// The preflight runs over the **source**, so a refusal is free: the copy is refused before a single
    /// byte is written, rather than materialized and then cleaned up.
    @Test func aRefusedCopyNeverTouchesTheDestinationAtAll() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card3)", Item.uneditable)
        // A second board, so "nothing at the destination" is a claim about an empty container rather
        // than about a folder that happens to hold the source too.
        try fixture.item("B.kanban", Item.board)
        try fixture.item("B.kanban/\(Ident.lane3)", Item.rich(order: "1024", title: "Elsewhere"))

        _ = writeFailure {
            _ = try BoardWriter.copyItem(
                at: fixture.url("A.kanban/\(Ident.lane1)"),
                toParent: fixture.url("B.kanban"),
                order: nil,
                stamps: .fork
            )
        }

        #expect(try fixture.entryNames("B.kanban").sorted() == ["index.md", Ident.lane3].sorted())
    }

    /// **Item-level copies sever tracker identity** (01-storage-format.md § Fractal layout ▸ Rules,
    /// ruled 2026-07-29): "every folder an item-level copy materializes drops the reserved
    /// `remote`/`remote-state` keys, at every level … because two local objects must never both claim to
    /// be the same remote object". The **source** keeps both, because a sever is something a copy does to
    /// itself.
    ///
    /// A lane copy, so both keys are exercised where the schema puts them — `remote-state` on the lane,
    /// `remote` on its cards — and both levels are asserted, which is what "at every level" means.
    @Test func anItemLevelCopySeversTheReservedTrackerKeys() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.tracked(order: "1024", title: "Todo", key: "remote-state"))
        try fixture.item(
            "A.kanban/\(Ident.lane1)/\(Ident.card1)",
            Item.tracked(order: "1024", title: "Card One", key: "remote")
        )

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .fork
        )

        let lane = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(id.rawValue)"))
        #expect(lane.value(for: "remote-state") == nil, "the copied lane's tracker mapping is severed")
        #expect(lane.value(for: "project") != nil, "and every other unknown key is untouched")

        let copiedCard = try #require(try fixture.entryNames("A.kanban/\(id.rawValue)").first(where: BoardLoader.isUUIDShaped))
        let card = try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(id.rawValue)/\(copiedCard)"))
        #expect(card.value(for: "remote") == nil, "the copied card's too — at every level")
        #expect(card.value(for: "project") != nil)

        // The originals still claim their remote objects: only the copy is severed.
        #expect(try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(Ident.lane1)"))
            .value(for: "remote-state") != nil)
        #expect(try FrontmatterDocument.parse(fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)"))
            .value(for: "remote") != nil)
    }

    /// Every occurrence goes, not just the winning one — `FrontmatterDocument.remove`'s own rule, and it
    /// matters here more than anywhere: a hand-duplicated `remote:` left behind would resurrect the
    /// claim the moment the winner were removed.
    @Test func theSeverTakesEveryOccurrenceOfTheKey() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Todo"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", """
        ---
        schema: 1
        title: Twinned
        order: 1024
        remote: gitea#1
        remote: gitea#2
        ---
        Body.

        """)

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card1)"),
            toParent: fixture.url("A.kanban/\(Ident.lane1)"),
            order: nil,
            stamps: .fork
        )

        let text = try fixture.indexText("A.kanban/\(Ident.lane1)/\(id.rawValue)")
        #expect(!text.contains("remote:"), "both occurrences went")
    }

    /// A UUID-shaped folder with no `index.md` — interrupted-create residue — is reminted and
    /// carried like any other folder, and simply not rewritten: the same skip the loader applies.
    @Test func aNestedFolderWithoutAnIndexIsCopiedAndRemintedButNotRewritten() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        try FileManager.default.createDirectory(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.indexless)"),
            withIntermediateDirectories: true
        )

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)"),
            toParent: fixture.url("A.kanban"),
            order: nil,
            stamps: .fork
        )

        let names = try fixture.entryNames("A.kanban/\(id.rawValue)").filter(BoardLoader.isUUIDShaped)
        #expect(names.count == 3)
        #expect(Set(names).isDisjoint(with: [Ident.card1, Ident.card2, Ident.indexless]))
        let orphan = try #require(names.first { !fixture.exists("A.kanban/\(id.rawValue)/\($0)/index.md") })
        #expect(try fixture.entryNames("A.kanban/\(id.rawValue)/\(orphan)") == [])
    }

    /// The root gets no leniency: it must be rewritten to carry its new `order`, so an
    /// uneditable one refuses before anything is materialized.
    @Test func anUneditableRootRefusesTheCopyAndMaterializesNothing() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card3)", Item.uneditable)
        let before = try fixture.entryNames("A.kanban/\(Ident.lane2)")

        let error = writeFailure {
            _ = try BoardWriter.copyItem(
                at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card3)"),
                toParent: fixture.url("A.kanban/\(Ident.lane2)"),
                order: nil,
                stamps: .fork
            )
        }
        #expect(error?.reason == .uneditableFrontmatter(.keyWithoutOwnLine))
        // Same enrichment as the move pre-flight: the root's document (title: Odd) was read
        // before the uneditable-shape refusal fired.
        #expect(error?.operation == .copy(title: "Odd"))
        #expect(try fixture.entryNames("A.kanban/\(Ident.lane2)") == before)
    }

    /// All-or-nothing at the destination — and since the copy became a **transaction** (ruled
    /// 2026-07-29) this case never even materializes: an unreadable nested `index.md` is caught by the
    /// preflight, over the *source*, before a byte is copied. The claim is the same one, met earlier and
    /// more cheaply: nothing is at the destination, and `.unreadable` names the file rather than the
    /// half-finished copy of it.
    @Test func anUnreadableDescendantRefusesTheCopyBeforeItStarts() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        let unreadable = fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card2)").appendingPathComponent("index.md")
        try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: unreadable.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o644], ofItemAtPath: unreadable.path) }
        let before = try fixture.entryNames("A.kanban")

        let error = writeFailure {
            _ = try BoardWriter.copyItem(
                at: fixture.url("A.kanban/\(Ident.lane1)"),
                toParent: fixture.url("A.kanban"),
                order: nil,
                stamps: .fork
            )
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(error?.path.contains(Ident.card2) == true, "the offending file is named")
        #expect(try fixture.entryNames("A.kanban") == before)
    }

    /// A copy always mints, so the import boundary has nothing to do here: landing in a board
    /// that already holds the source's UUID is not even a special case.
    @Test func aCopyIntoABoardHoldingTheSameUUIDMintsAnywayWithoutError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try board(fixture)
        try fixture.item("B.kanban", Item.board)
        try fixture.item("B.kanban/\(Ident.lane3)", Item.rich(order: "1024", title: "Inbox"))
        try fixture.item("B.kanban/\(Ident.lane3)/\(Ident.card1)", Item.rich(order: "1024", title: "Twin"))

        let id = try BoardWriter.copyItem(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card1)"),
            toParent: fixture.url("B.kanban/\(Ident.lane3)"),
            order: nil,
            stamps: .fork
        )

        #expect(id.rawValue != Ident.card1)
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane3)/\(id.rawValue)")).title
            == .valid("Card One"))
        #expect(try FrontmatterDocument.parse(fixture.indexText("B.kanban/\(Ident.lane3)/\(Ident.card1)")).title
            == .valid("Twin"))
        #expect(try BoardLoader.load(boardRoot: fixture.url("B.kanban")).warnings.isEmpty)
    }
}

// MARK: - Purge

/// `BoardWriter.purgeItem`: physical removal — the create-undo's own primitive
/// (01-storage-format.md § Deletion; 13-native-undo.md: "create → remove the created folder") —
/// irreversible, and distinct from the ordinary delete, which is a *move* into `.trash/`. Does not
/// require the item to be in the trash first: an undone create's folder was never trashed to begin
/// with.
struct BoardWriterPurgeTests {
    @Test func purgeRemovesTheFolderTreeIncludingNestedContentFromDisk() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("A.kanban", Item.board)
        let laneFolder = try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Todo"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Card One"))

        try BoardWriter.purgeItem(at: laneFolder)

        #expect(!fixture.exists("A.kanban/\(Ident.lane1)"))
        #expect(!fixture.exists("A.kanban/\(Ident.lane1)/\(Ident.card1)"))
    }

    /// A folder that is already gone (a Finder deletion) is success, not an error — the same
    /// end state a purge would have produced.
    @Test func purgeOfAnAlreadyMissingFolderSucceedsSilently() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let gone = fixture.url("A.kanban/\(Ident.lane1)")
        #expect(!fixture.exists("A.kanban/\(Ident.lane1)"))

        try BoardWriter.purgeItem(at: gone)
    }

    @Test func purgeRefusesANonUUIDShapedFolder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = try fixture.item("A.kanban", Item.board)

        let error = writeFailure { try BoardWriter.purgeItem(at: root) }
        guard case let .unreadable(message) = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(message.contains("UUID-shaped"))
        #expect(fixture.exists("A.kanban"))
    }

    @Test func purgeDoesNotRequirePriorTombstone() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let folder = try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Live"))
        #expect(try FrontmatterDocument.parse(fixture.indexText(Ident.lane1)).deleted == .missing)

        try BoardWriter.purgeItem(at: folder)

        #expect(!fixture.exists(Ident.lane1))
    }
}

// MARK: - Same-parent move degrades to a reorder

/// A move whose destination is the item's current parent is the same gesture as any other drop
/// — the writer degrades it to the bare `order` rewrite instead of leaking a `FileManager`
/// name collision, and the appended rank is computed excluding the item itself, which unlike
/// every other move is already sitting among the siblings it would otherwise count.
struct BoardWriterSameParentMoveTests {
    @Test func aSameParentMoveWithNilOrderAppendsAfterTheOtherSiblings() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Lane"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card3)", Item.rich(order: "3072", title: "Third"))

        let result = try BoardWriter.moveItem(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card1)"),
            toParent: fixture.url("A.kanban/\(Ident.lane1)"),
            sourceBoardRoot: fixture.url("A.kanban"),
            destinationBoardRoot: fixture.url("A.kanban"),
            order: nil
        )

        #expect(result.id.rawValue == Ident.card1)
        #expect(result.reminted.isEmpty)
        // Appended after Second (2048) and Third (3072), not after its own stale 1024 —
        // and not after itself miscounted (which would give 2048... or 4096+1024).
        let text = try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)")
        #expect(text.contains("order: 4096"))
        // **The same-parent path is a reorder, so it stamps nothing** (01-storage-format.md
        // § Frontmatter ▸ `modified`'s scope, refined 2026-07-30): the container never changed, so the
        // foreign `modified-by` survives. This assertion was `!text.contains("modified-by:")` under the
        // pre-refinement rule that every app write clears it.
        #expect(text.contains("modified-by: claude"))
    }

    @Test func aSameParentMoveWithAnExplicitOrderJustRewritesIt() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("A.kanban", Item.board)
        try fixture.item("A.kanban/\(Ident.lane1)", Item.rich(order: "1024", title: "Lane"))
        try fixture.item("A.kanban/\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "3072", title: "Only"))
        let laneBefore = try fixture.indexData("A.kanban/\(Ident.lane1)")

        let result = try BoardWriter.moveItem(
            at: fixture.url("A.kanban/\(Ident.lane1)/\(Ident.card1)"),
            toParent: fixture.url("A.kanban/\(Ident.lane1)"),
            sourceBoardRoot: fixture.url("A.kanban"),
            destinationBoardRoot: fixture.url("A.kanban"),
            order: 512
        )

        #expect(result.id.rawValue == Ident.card1)
        #expect(try fixture.indexText("A.kanban/\(Ident.lane1)/\(Ident.card1)").contains("order: 512"))
        #expect(try fixture.indexData("A.kanban/\(Ident.lane1)") == laneBefore)
        #expect(try fixture.entryNames("A.kanban/\(Ident.lane1)").contains(Ident.card1))
    }
}

// MARK: - Import attachments

/// `BoardWriter.importAttachments`: the write side of 01-storage-format.md § Attachments —
/// never refuses a drop, auto-renames Finder-style on collision, and stops a multi-file batch
/// at the first failure without undoing what already landed.
struct BoardWriterImportAttachmentsTests {
    /// A bare UUID-shaped card folder — `importAttachments` never reads its `index.md`, so a
    /// minimal one (like every other card these suites mint) is as good a target as a fully
    /// loaded one.
    private func card(_ fixture: WriterFixture) throws -> URL {
        try fixture.item(Ident.card1, Item.rich(order: "1024", title: "Card"))
    }

    @Test func firstImportCreatesAttachmentsAndLandsTheOriginalName() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let bytes = Data([0x89, 0x50, 0x4E, 0x47, 0x00, 0xFF])
        let source = try fixture.file("sources/shot.png", bytes)

        let landed = try BoardWriter.importAttachments([source], intoCard: card)

        #expect(landed == [ImportedAttachment(sourceURL: source, fileName: "shot.png")])
        #expect(try fixture.data("\(Ident.card1)/attachments/shot.png") == bytes)
        // The source is a copy's origin, never its casualty.
        #expect(try fixture.data("sources/shot.png") == bytes)
    }

    /// Three separate drops of a file named `shot.png`: never overwritten, never refused — each
    /// lands under the next free Finder-style name, and all three coexist afterwards.
    @Test func collidingImportsAutoRenameFinderStyle() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let first = try fixture.file("sources/1/shot.png", Data([0x01]))
        let second = try fixture.file("sources/2/shot.png", Data([0x02]))
        let third = try fixture.file("sources/3/shot.png", Data([0x03]))

        let landed1 = try BoardWriter.importAttachments([first], intoCard: card)
        let landed2 = try BoardWriter.importAttachments([second], intoCard: card)
        let landed3 = try BoardWriter.importAttachments([third], intoCard: card)

        #expect(landed1.map(\.fileName) == ["shot.png"])
        #expect(landed2.map(\.fileName) == ["shot 2.png"])
        #expect(landed3.map(\.fileName) == ["shot 3.png"])
        #expect(try fixture.entryNames("\(Ident.card1)/attachments") == ["shot 2.png", "shot 3.png", "shot.png"])
        #expect(try fixture.data("\(Ident.card1)/attachments/shot.png") == Data([0x01]))
        #expect(try fixture.data("\(Ident.card1)/attachments/shot 2.png") == Data([0x02]))
        #expect(try fixture.data("\(Ident.card1)/attachments/shot 3.png") == Data([0x03]))
    }

    /// A name with no extension at all suffixes directly — `"notes"` → `"notes 2"`, not
    /// `"notes 2."` or some other extension-shaped artifact.
    @Test func anExtensionLessCollisionRenamesToNotes2() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let first = try fixture.file("sources/1/notes", Data("first".utf8))
        let second = try fixture.file("sources/2/notes", Data("second".utf8))

        _ = try BoardWriter.importAttachments([first], intoCard: card)
        let landed = try BoardWriter.importAttachments([second], intoCard: card)

        #expect(landed.map(\.fileName) == ["notes 2"])
        #expect(try fixture.entryNames("\(Ident.card1)/attachments") == ["notes", "notes 2"])
    }

    /// `fileExists` is true for a directory as much as a file: a subfolder that happens to share
    /// the incoming name blocks it exactly like a file would, and is never touched by the import.
    @Test func aFileNamedLikeAnExistingSubfolderAlsoRenames() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        try fixture.file("\(Ident.card1)/attachments/shot.png/inner.txt", Data("hand-made".utf8))
        let source = try fixture.file("sources/shot.png", Data([0x09]))

        let landed = try BoardWriter.importAttachments([source], intoCard: card)

        #expect(landed.map(\.fileName) == ["shot 2.png"])
        #expect(try fixture.data("\(Ident.card1)/attachments/shot 2.png") == Data([0x09]))
        #expect(try fixture.entryNames("\(Ident.card1)/attachments/shot.png") == ["inner.txt"])
    }

    /// One call, three sources, two of which collide with what is already there and with each
    /// other in turn — the returned names track input order, not landing order.
    @Test func multiFileImportInOneCallReturnsLandedNamesInInputOrder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        try fixture.file("\(Ident.card1)/attachments/shot.png", Data([0x00]))
        let shot = try fixture.file("sources/1/shot.png", Data([0x01]))
        let notes = try fixture.file("sources/2/notes.txt", Data([0x02]))
        let shotAgain = try fixture.file("sources/3/shot.png", Data([0x03]))

        let landed = try BoardWriter.importAttachments([shot, notes, shotAgain], intoCard: card)

        #expect(landed.map(\.fileName) == ["shot 2.png", "notes.txt", "shot 3.png"])
        #expect(landed.map(\.sourceURL) == [shot, notes, shotAgain])
        #expect(try fixture.entryNames("\(Ident.card1)/attachments")
            == ["notes.txt", "shot 2.png", "shot 3.png", "shot.png"])
    }

    /// Refused before any copy for that file: the failing source is named, nothing of its lands,
    /// and — because each import is its own completed write — the file that landed *before* it
    /// in the same batch stays landed rather than being rolled back.
    @Test func anUnreadableSourceRefusesNamingItAndStopsTheBatch() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let first = try fixture.file("sources/shot.png", Data([0x01]))
        let missing = fixture.url("sources").appendingPathComponent("missing.png")
        let third = try fixture.file("sources/third.png", Data([0x03]))

        let error = writeFailure {
            _ = try BoardWriter.importAttachments([first, missing, third], intoCard: card)
        }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(error?.path == missing.path)
        #expect(error?.operation == .importAttachment(filename: "missing.png"))
        #expect(try fixture.entryNames("\(Ident.card1)/attachments") == ["shot.png"])
    }

    /// Attachments belong to cards: the shape guard `moveItem`/`copyItem`/`purgeItem` share
    /// refuses a board root (or any non-UUID-shaped folder) before `attachments/` is even
    /// considered.
    @Test func importIntoANonUUIDShapedFolderIsRefused() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let root = try fixture.item("A.kanban", Item.board)
        let source = try fixture.file("sources/shot.png", Data([0x01]))

        let error = writeFailure {
            _ = try BoardWriter.importAttachments([source], intoCard: root)
        }
        guard case let .unreadable(message) = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
        #expect(message.contains("UUID-shaped"))
        #expect(!fixture.exists("A.kanban/attachments"))
    }

    /// "Never refuses the drop" is policy, not an I/O guarantee: a copy that genuinely cannot
    /// land — `attachments/` made unwritable here, standing in for a full disk or a permissions
    /// error — surfaces as `.io` naming the source file, and no partial file is left behind.
    @Test func aFailedCopyLeavesNoPartialAttachment() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let existing = try fixture.file("sources/existing.png", Data([0x00]))
        _ = try BoardWriter.importAttachments([existing], intoCard: card)
        let attachmentsFolder = fixture.url(Ident.card1).appendingPathComponent("attachments")
        try FileManager.default.setAttributes([.posixPermissions: 0o555], ofItemAtPath: attachmentsFolder.path)

        let source = try fixture.file("sources/shot.png", Data([0x01]))
        let error = writeFailure {
            _ = try BoardWriter.importAttachments([source], intoCard: card)
        }
        guard case .io = error?.reason else {
            Issue.record("expected .io, got \(String(describing: error?.reason))")
            return
        }
        #expect(error?.path == source.path)
        #expect(try fixture.entryNames("\(Ident.card1)/attachments") == ["existing.png"])
    }
}

// MARK: - List attachments

/// `BoardWriter.listAttachments`: the flat, top-level-files-only read (01-storage-format.md §
/// Attachments) that never writes anything, not even `attachments/` itself.
struct BoardWriterListAttachmentsTests {
    private func card(_ fixture: WriterFixture) throws -> URL {
        try fixture.item(Ident.card1, Item.rich(order: "1024", title: "Card"))
    }

    /// Subfolders and hidden files never surface, and survive both the import and the listing
    /// untouched; the sort is Finder's numeric order, not plain string order (`"shot 2.png"`
    /// before `"shot 10.png"`, which a byte-wise sort would put the other way).
    @Test func listingIsFlatExcludesHiddenFilesAndSortsFinderStyle() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)
        let shot2 = try fixture.file("sources/1/shot 2.png", Data([0x01]))
        let shot10 = try fixture.file("sources/2/shot 10.png", Data([0x02]))
        _ = try BoardWriter.importAttachments([shot2, shot10], intoCard: card)
        try fixture.file("\(Ident.card1)/attachments/sub/inside.txt", Data("hand-made".utf8))
        try fixture.file("\(Ident.card1)/attachments/.DS_Store", Data("hidden".utf8))

        let listing = try BoardWriter.listAttachments(ofCard: card)

        #expect(listing == ["shot 2.png", "shot 10.png"])
        #expect(try fixture.entryNames("\(Ident.card1)/attachments/sub") == ["inside.txt"])
        #expect(fixture.exists("\(Ident.card1)/attachments/.DS_Store"))
    }

    /// Nothing imported yet is an ordinary state, not an error — `attachments/` need not exist
    /// for a card to list cleanly as empty.
    @Test func aMissingAttachmentsFolderListsAsEmpty() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let card = try card(fixture)

        #expect(try BoardWriter.listAttachments(ofCard: card) == [])
        #expect(!fixture.exists("\(Ident.card1)/attachments"))
    }

    /// Unlike a missing `attachments/`, a missing *card* is a loud error — the caller asked
    /// about something that is not there.
    @Test func aMissingCardFolderIsALoudUnreadableError() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let missing = fixture.url(Ident.card1)

        let error = writeFailure { _ = try BoardWriter.listAttachments(ofCard: missing) }
        guard case .unreadable = error?.reason else {
            Issue.record("expected .unreadable, got \(String(describing: error?.reason))")
            return
        }
    }
}
