import Foundation
import Testing
import UniformTypeIdentifiers
@testable import Kanban

/// **Pasting a file URL as an attachment** — the image-data branch's reserved clause 2, filled
/// (04-interactions.md ▸ Clipboard, ruled 2026-08-09; follow-up to the paste-image card, 92a9ea6a).
/// A Finder copy of one or more files pastes them into the anchor card's `attachments/` through the
/// same `BoardStore.importAttachments` path a Finder drop takes: one collision ladder, one folder
/// refusal (via `FinderDrop.partition`), one set of banners.
///
/// Like every other write suite here the landing tests drive a **real store over a real temp board**
/// and read back through the loader or the raw bytes, never through a snapshot the store handed out.
/// `WriterFixture`, `Ident` and `Item` come from `WriterTestSupport.swift`; `FakePasteboard`,
/// `ClipboardHarness`, `makeClipboardHarness` and the board fixture come from `ClipboardTests.swift`;
/// `encodedImage` comes from `PasteImageTests.swift`.

// MARK: - Sources on disk

/// Real files (and folders) **outside the board** for a fake pasteboard's file URLs to point at — a
/// file URL is a reference to something the filesystem actually has, unlike the image branch's raw
/// bytes, and `FinderDrop.partition`'s directory check reads the real filesystem rather than a
/// declared type. `DropSources`' own shape, one file over (`FileDropWriteTests.swift`).
private struct PasteFileSources {
    let root: URL

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

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

    @discardableResult
    func file(_ relativePath: String, _ bytes: Data = Data([0x01])) throws -> URL {
        let url = root.appendingPathComponent(relativePath)
        try FileManager.default.createDirectory(
            at: url.deletingLastPathComponent(), withIntermediateDirectories: true
        )
        try bytes.write(to: url)
        return url
    }

    @discardableResult
    func folder(_ relativePath: String) throws -> URL {
        let url = root.appendingPathComponent(relativePath, isDirectory: true)
        try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
        return url
    }
}

/// Attachment names as the loader sees them — never the store's snapshot, which a write deliberately
/// does not touch (the one-way flow). `PasteImageWriteTests`' own helper, one file over.
@MainActor
private func fileTestAttachments(_ card: String, in fixture: WriterFixture) throws -> [String] {
    let model = try BoardLoader.load(boardRoot: fixture.root).model
    for lane in model.lanes {
        if let match = lane.cards.first(where: { $0.id.rawValue == card }) { return match.attachments }
    }
    return []
}

// MARK: - Classification

@Suite("PastedImage ▸ carriesFileURL, a web URL is not a file")
struct FileURLPresenceTests {

    /// `public.url` is the conforms-*from* direction — every file URL is one, not the reverse — so a
    /// browser's Copy Link never trips the branch a Finder copy does.
    @Test("A plain web URL does not conform to public.file-url")
    func webURLsDoNotCount() {
        #expect(!PastedImage.carriesFileURL([UTType.url.identifier]))
    }
}

// MARK: - Where a file lands on the board

@MainActor
@Suite("Paste ▸ files into a card")
struct PasteFileWriteTests {

    @Test("A Finder-copied file lands under its own name in the anchor card")
    func aFileLands() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png", Data([0x89, 0x50]))
        harness.pasteboard.seedFileURLs([shot])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteFiles(into: harness.store))

        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture) == ["shot.png"])
        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/shot.png") == Data([0x89, 0x50]))
        #expect(try Data(contentsOf: shot) == Data([0x89, 0x50]), "a copy's origin, never its casualty")
        #expect(harness.store.banners.oneShots.isEmpty)
        #expect(harness.store.banners.losses.isEmpty)
    }

    @Test("A multi-file Finder copy lands every file, in pasteboard order")
    func multipleFilesLand() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png", Data([0x89, 0x50]))
        let notes = try sources.file("notes.txt", Data([0x41, 0x42]))
        harness.pasteboard.seedFileURLs([shot, notes])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteFiles(into: harness.store))

        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture) == ["notes.txt", "shot.png"])
        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/shot.png") == Data([0x89, 0x50]))
        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/notes.txt") == Data([0x41, 0x42]))
        #expect(harness.store.banners.oneShots.isEmpty)
    }

    @Test("A name already taken climbs the Finder ladder rather than overwriting")
    func collisionsRename() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let mine = Data("hand placed".utf8)
        try harness.fixture.file("\(Ident.lane1)/\(Ident.card2)/attachments/shot.png", mine)
        let shot = try sources.file("shot.png", Data([0x02]))
        harness.pasteboard.seedFileURLs([shot])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteFiles(into: harness.store))

        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/shot.png") == mine)
        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture) == ["shot 2.png", "shot.png"])
    }

    @Test("A mixed paste imports its files and names the folder it skipped")
    func mixedPasteSkipsFolders() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png", Data([0x89, 0x50]))
        let directory = try sources.folder("Project")
        harness.pasteboard.seedFileURLs([shot, directory])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteFiles(into: harness.store))

        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture) == ["shot.png"])
        #expect(!harness.fixture.exists("\(Ident.lane1)/\(Ident.card2)/attachments/Project"))
        #expect(harness.store.banners.oneShots.isEmpty, "nothing failed: the folder was never attempted")
        #expect(harness.store.banners.losses.map(\.message) == ["Folders can't be attached — 1 skipped"])
    }

    @Test("A folders-only paste writes nothing at all and only names the loss")
    func foldersOnlyPasteWritesNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let directory = try sources.folder("Project")
        harness.pasteboard.seedFileURLs([directory])
        harness.store.select([clipboardCard2], in: .board)

        #expect(!harness.clipboard.pasteFiles(into: harness.store))

        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture).isEmpty)
        #expect(harness.store.banners.oneShots.isEmpty)
        #expect(harness.store.banners.losses.map(\.message) == ["Folders can't be attached — 1 skipped"])
    }

    /// The pasteboard declared `public.file-url` (so `canPasteFiles` reads true), but produced no
    /// actual URL objects when asked — the honest outcome is the one where the card is untouched,
    /// same as the image branch's lying-pasteboard case.
    @Test("A pasteboard that declares a file URL but resolves none writes nothing at all")
    func aLyingPasteboardWritesNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.fileURL.identifier, Data())])
        harness.clipboard.refresh()
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.canPasteFiles(into: harness.store))
        #expect(!harness.clipboard.pasteFiles(into: harness.store))
        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture).isEmpty)
        #expect(harness.store.banners.oneShots.isEmpty)
        #expect(harness.store.banners.losses.isEmpty)
    }

    /// The card window's branch: the target is the window's own card, whatever the board's selection
    /// happens to be — `PasteImageWriteTests.theCardWindowsOwnCard`'s own shape.
    @Test("The card window pastes onto its own card, not onto the board's selection")
    func theCardWindowsOwnCard() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png")
        harness.pasteboard.seedFileURLs([shot])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteFiles(intoCard: clipboardCard4, in: harness.store))

        #expect(try fileTestAttachments(Ident.card4, in: harness.fixture) == ["shot.png"])
        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture).isEmpty)
    }

    /// **The recorded precedence call**: a Finder-copied image file carries both a file URL and an
    /// image flavor on the same pasteboard. The file branch wins — the actual file lands, byte for
    /// byte, rather than a re-encoded "Pasted Image.png" beside it — which is `imagePayload == nil`
    /// whenever `fileURLPayload` is true, `refresh()`'s own reading of `PastedImage.flavor`'s clause
    /// order.
    @Test("A file URL outranks image data riding beside it — the actual file lands, not a picture copy")
    func fileURLOutranksImageData() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let png = encodedImage(.png)
        let photo = try sources.file("photo.png", png)
        harness.pasteboard.seedFileURLs([photo], also: [(UTType.png.identifier, png)])
        harness.clipboard.refresh()
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.fileURLPayload)
        #expect(harness.clipboard.imagePayload == nil, "the image branch defers to the file URL")
        #expect(harness.clipboard.canPasteFiles(into: harness.store))
        #expect(!harness.clipboard.canPasteImage(into: harness.store))

        #expect(harness.clipboard.pasteFiles(into: harness.store))

        #expect(try fileTestAttachments(Ident.card2, in: harness.fixture) == ["photo.png"])
        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/photo.png") == png)
    }
}

// MARK: - Validation

@MainActor
@Suite("Paste ▸ file menu validation")
struct PasteFileValidationTests {

    @Test("The board branch needs a file URL and a card to put it on")
    func theBoardBranchsClauses() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png")

        harness.store.select([clipboardCard1], in: .board)
        #expect(!harness.clipboard.canPasteFiles(into: harness.store), "nothing on the pasteboard")

        harness.pasteboard.seedFileURLs([shot])
        harness.clipboard.refresh()
        #expect(harness.clipboard.canPasteFiles(into: harness.store))

        // A lane anchors no card, so the row greys out — and so does the paste.
        harness.store.select([clipboardLane1], in: .board)
        #expect(!harness.clipboard.canPasteFiles(into: harness.store))
        #expect(!harness.clipboard.pasteFiles(into: harness.store))

        harness.store.clearSelection()
        #expect(!harness.clipboard.canPasteFiles(into: harness.store))
    }

    @Test("A file URL offers the file branch even though it silences the picture branch")
    func fileURLOffersItsOwnBranch() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png")
        harness.pasteboard.seedFileURLs([shot], also: [(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()
        harness.store.select([clipboardCard1], in: .board)

        #expect(harness.clipboard.fileURLPayload)
        #expect(harness.clipboard.imagePayload == nil)
        #expect(harness.clipboard.canPasteFiles(into: harness.store))
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
        #expect(!harness.clipboard.canPasteBoardBackground(into: harness.store), "a file URL suppresses this too")
    }

    @Test("The card-window branch refuses a card that is not on the board side")
    func theCardWindowBranchsClauses() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let sources = try PasteFileSources()
        defer { sources.tearDown() }
        let shot = try sources.file("shot.png")
        harness.pasteboard.seedFileURLs([shot])
        harness.clipboard.refresh()

        #expect(harness.clipboard.canPasteFiles(intoCard: clipboardCard1, in: harness.store))
        #expect(!harness.clipboard.canPasteFiles(intoCard: clipboardCard3, in: harness.store), "trashed")
        #expect(!harness.clipboard.canPasteFiles(intoCard: clipboardLane1, in: harness.store), "a lane")
    }

    /// Clause 1 of the precedence, proven rather than assumed — `PastedImageClassificationTests
    /// .boardItemsWin`'s own claim, one surface over. Synthetic (`layerForeign`'s doc), because a real
    /// copy's `clearContents()` would never leave a file URL beside the app's own type in the first
    /// place — the guard exists so that if it ever did, the board payload would still win.
    @Test("A board payload on the pasteboard outranks a file URL riding beside it")
    func boardPayloadOutranksFileURL() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.store.select([clipboardCard1], in: .board)
        harness.clipboard.copy(from: harness.store)
        harness.pasteboard.layerForeign([(UTType.fileURL.identifier, Data("file:///tmp/shot.png".utf8))])
        harness.clipboard.refresh()

        #expect(!harness.clipboard.fileURLPayload)
        #expect(harness.clipboard.canPaste(into: harness.store))
        #expect(!harness.clipboard.canPasteFiles(into: harness.store))
    }

    @Test("A non-file URL (a browser's Copy Link) offers nothing to either attachment branch")
    func nonFileURLsOfferNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.url.identifier, Data("https://example.com".utf8))])
        harness.clipboard.refresh()
        harness.store.select([clipboardCard1], in: .board)

        #expect(!harness.clipboard.fileURLPayload)
        #expect(!harness.clipboard.canPasteFiles(into: harness.store))
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
    }
}
