import Foundation
import Testing
@testable import Kanban

/// Board ▸ Copy Link's target predicate (`BoardStore.copyLinkTarget`) and its pasteboard write
/// (`BoardStore.copyCardLink`, `FolderLinkPasteboard`) — design ruling 2026-08-09, card 737a949f
/// "Add an option to card context menu to copy a link to the card folder".
///
/// The context menu's own click-scoped enablement (`CardFaceView.copyLinkEnabled`, which disables on
/// a multi-selection even when the clicked card was never the reader of `copyLinkTarget`) is not
/// pinned here — no sibling row (`styleTarget`/`targetIDs`) is either, per this suite's own
/// `TrashWriteTests.contextMenuDeleteIgnoresTheSelection` precedent for *why* the two answers must
/// differ. What is pinned here is the menu-bar row's answer and the actual bytes a write produces,
/// which is what the ruling's own test list asks for: "pasteboard contents … enablement (sole vs
/// multi selection)".

// MARK: - Fixture

@MainActor
private func makeCopyLinkBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.board()
    try fixture.lane(Ident.lane1, order: "1024", title: "Todo")
    try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "First")
    try fixture.card(Ident.card2, in: Ident.lane1, order: "2048", title: "Second")
    try fixture.lane(Ident.lane2, order: "2048", title: "Doing")
    try fixture.trashCard(Ident.indexless, order: "1024", title: "Trashed")
    return fixture
}

private let copyLinkLane1 = ItemID(rawValue: Ident.lane1)
private let copyLinkCard1 = ItemID(rawValue: Ident.card1)
private let copyLinkCard2 = ItemID(rawValue: Ident.card2)
private let copyLinkTrashed = ItemID(rawValue: Ident.indexless)

/// A pasteboard double that records exactly what was written — `FakePasteboard`'s shape
/// (`ClipboardTests.swift`), applied to Copy Link's smaller payload, so a test never races the
/// machine's one real pasteboard or every other test in the run.
@MainActor
private final class FakeFolderLinkPasteboard: FolderLinkPasteboard {
    private(set) var writeCount = 0
    private(set) var fileURL: URL?
    private(set) var path: String?

    func write(fileURL: URL, path: String) {
        writeCount += 1
        self.fileURL = fileURL
        self.path = path
    }
}

// MARK: - The target predicate

@MainActor
@Suite("Copy Link's target and enablement")
struct CopyLinkTargetTests {

    @Test("A sole selected live board card answers its own folder URL")
    func soleCardAnswersItsFolder() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([copyLinkCard1], in: .board)

        let expected = fixture.root
            .appendingPathComponent(Ident.lane1, isDirectory: true)
            .appendingPathComponent(Ident.card1, isDirectory: true)
        #expect(store.copyLinkTarget == expected)
    }

    @Test("A multi-selection disables it — a link is singular")
    func multiSelectionDisables() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([copyLinkCard1, copyLinkCard2], in: .board)
        #expect(store.copyLinkTarget == nil)
    }

    @Test("A lane selection, an empty selection, and a trash selection all disable it")
    func onlyALiveBoardCardEnablesIt() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([copyLinkLane1], in: .board)
        #expect(store.copyLinkTarget == nil, "a lane has no singular card folder to link")

        store.clearSelection()
        #expect(store.copyLinkTarget == nil, "nothing selected has no target either — no board fallback")

        store.select([copyLinkTrashed], in: .trash)
        #expect(store.copyLinkTarget == nil, "\"sole selected live card\" — a trashed card is not one")
    }

    /// The ruling's explicit gate ("disabled … wherever edit-shaped actions already disable"), even
    /// though the write itself mutates nothing on disk — `copyLinkTarget`'s own doc comment flags
    /// this as implemented-as-written rather than re-derived.
    @Test("An open inline editor disables it, exactly as the ruling asks")
    func inlineEditingDisablesIt() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.select([copyLinkCard1], in: .board)
        #expect(store.copyLinkTarget != nil, "sanity: the fixture must actually enable it first")

        store.transient.beginPlaceholder(inLane: copyLinkLane1)
        #expect(!store.acceptsBoardMutations, "sanity: the fixture must actually be refusing mutations")
        #expect(store.copyLinkTarget == nil)
    }
}

// MARK: - The write

@MainActor
@Suite("Copy Link's pasteboard write")
struct CopyLinkWriteTests {

    @Test("Both representations land, and the folder URL is exact")
    func writesBothRepresentations() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let pasteboard = FakeFolderLinkPasteboard()

        store.select([copyLinkCard1], in: .board)
        store.copyCardLink(to: pasteboard)

        let expectedFolder = fixture.root
            .appendingPathComponent(Ident.lane1, isDirectory: true)
            .appendingPathComponent(Ident.card1, isDirectory: true)
        #expect(pasteboard.writeCount == 1)
        #expect(pasteboard.fileURL == expectedFolder)
        #expect(pasteboard.fileURL?.isFileURL == true)
        #expect(pasteboard.path == expectedFolder.path)
    }

    @Test("A disabled target writes nothing")
    func disabledTargetWritesNothing() throws {
        let fixture = try makeCopyLinkBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let pasteboard = FakeFolderLinkPasteboard()

        store.select([copyLinkCard1, copyLinkCard2], in: .board)
        store.copyCardLink(to: pasteboard)

        #expect(pasteboard.writeCount == 0)
    }
}

// MARK: - The seam's shared write(link:) helper

@MainActor
@Suite("FolderLinkPasteboard's default write(link:)")
struct FolderLinkPasteboardTests {

    @Test("write(link:) derives the plain path from the very same URL it writes as the file URL")
    func writeLinkDerivesPathFromTheSameURL() {
        let pasteboard = FakeFolderLinkPasteboard()
        let folder = URL(fileURLWithPath: "/tmp/some board/lane/card", isDirectory: true)

        pasteboard.write(link: folder)

        #expect(pasteboard.fileURL == folder)
        #expect(pasteboard.path == folder.path)
    }
}
