//  SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
//  SPDX-License-Identifier: LGPL-3.0-or-later

@preconcurrency import FileProvider
@testable import NextcloudFileProviderKit
import NextcloudFileProviderKitMocks
import NextcloudKit
import RealmSwift
import TestInterface
import UniformTypeIdentifiers
import XCTest

final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
    static let account = Account(
        user: "testUser", id: "testUserId", serverUrl: "https://mock.nc.com", password: "abcd"
    )
    static let dbManager = FilesDatabaseManager(account: account, databaseDirectory: makeDatabaseDirectory(), fileProviderDomainIdentifier: NSFileProviderDomainIdentifier("test"), log: FileProviderLogMock())

    override func setUp() {
        super.setUp()
        Realm.Configuration.defaultConfiguration.inMemoryIdentifier = name
    }

    func testMetadataContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.etag = "test-etag"
        metadata.contentType = UTType.text.identifier
        metadata.size = 12

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.text)
    }

    func testMetadataExtensionContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.pdf", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        // Don't set the content type in metadata, test the extension uttype discovery

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.pdf)
    }

    func testMetadataFolderContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.directory = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.folder)
    }

    func testMetadataPackageContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.zip", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.directory = true
        metadata.contentType = UTType.package.identifier

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.package)
    }

    func testMetadataBundleContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.key", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.directory = true
        metadata.contentType = UTType.bundle.identifier

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.bundle)
    }

    func testMetadataUnixFolderContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.directory = true
        metadata.contentType = "httpd/unix-directory"

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(item.contentType, UTType.folder)
    }

    func testPredictedBundleContentType() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.app", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.directory = true
        metadata.contentType = "httpd/unix-directory"

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertTrue(item.contentType.conforms(to: .bundle))
    }

    func testItemUserInfoLockingPropsFileLocked() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.etag = "test-etag"
        metadata.size = 12
        metadata.lock = true
        metadata.lockOwner = Self.account.username
        metadata.lockOwnerEditor = "testEditor"
        metadata.lockTime = .init()
        metadata.lockTimeOut = .init().addingTimeInterval(6_000_000)

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertNotNil(item.userInfo?["locked"])

        let fileproviderItems = ["fileproviderItems": [item]]
        let lockPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked != nil ).@count > 0"
        )
        XCTAssertTrue(lockPredicate.evaluate(with: fileproviderItems))
    }

    func testItemUserInfoLockingPropsFileUnlocked() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.etag = "test-etag"
        metadata.date = .init()
        metadata.size = 12

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertNil(item.userInfo?["locked"])

        let fileproviderItems = ["fileproviderItems": [item]]
        let lockPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked == nil ).@count > 0"
        )
        XCTAssertTrue(lockPredicate.evaluate(with: fileproviderItems))
    }

    func testItemUserInfoDisplayEvictState() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.downloaded = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertNotNil(item.userInfo?["displayEvict"])

        let fileproviderItems = ["fileproviderItems": [item]]
        let canEvictPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayEvict == true ).@count > 0"
        )
        XCTAssertTrue(canEvictPredicate.evaluate(with: fileproviderItems))

        // Pinned items must NOT advertise the evict action. The framework
        // would still report `contentPolicy == .downloadEagerlyAndKeepDownloaded`
        // until its own refresh of the item lands, so a tight unpin-then-evict
        // path races with -2008 NonEvictable. Gating on `!keepDownloaded`
        // forces the user to clear the pin first; once the framework has
        // re-fetched the item, both `displayEvict` and `contentPolicy` flip
        // together (#9891).
        metadata.keepDownloaded = true
        let keepDownloadedItem = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertNotNil(keepDownloadedItem.userInfo?["displayEvict"])

        let fileproviderKeepDownloadedItems = ["fileproviderItems": [keepDownloadedItem]]
        XCTAssertFalse(canEvictPredicate.evaluate(with: fileproviderKeepDownloadedItems))
    }

    func testItemUserInfoNoDisplayEvictState() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.downloaded = false

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertNotNil(item.userInfo?["displayEvict"])

        let fileproviderItems = ["fileproviderItems": [item]]
        let undownloadedPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayEvict == false ).@count > 0"
        )
        XCTAssertTrue(undownloadedPredicate.evaluate(with: fileproviderItems))
    }

    // MARK: - "Remove download" visibility on folders (#10085)

    // Unlike the file cases above (which read `displayEvict` straight off the
    // metadata), directory eviction visibility depends on descendant *file* rows
    // in the database, so these tests seed the per-test Realm first.

    /// Full server path of the folder produced by `makeFolderItem()`.
    private var folderServerPath: String {
        Self.account.davFilesUrl + "/folder"
    }

    /// Seed one metadata row into the per-test Realm.
    private func seedMetadataRow(
        ocId: String,
        fileName: String,
        serverUrl: String,
        directory: Bool,
        downloaded: Bool = false,
        visitedDirectory: Bool = false,
        deleted: Bool = false,
        keepDownloaded: Bool = false
    ) throws {
        let row = RealmItemMetadata()
        row.ocId = ocId
        row.account = Self.account.ncKitAccount
        row.updateLocation(serverUrl: serverUrl, fileName: fileName)
        row.directory = directory
        row.downloaded = downloaded
        row.visitedDirectory = visitedDirectory
        row.deleted = deleted
        row.keepDownloaded = keepDownloaded

        let realm = Self.dbManager.ncDatabase()
        try realm.write { realm.add(row) }
    }

    /// A directory `Item` at `folderServerPath`; its descendants are whatever
    /// rows the test seeded under that path.
    private func makeFolderItem(keepDownloaded: Bool = false) -> Item {
        var metadata =
            SendableItemMetadata(ocId: "folder-id", fileName: "folder", account: Self.account)
        metadata.directory = true
        metadata.keepDownloaded = keepDownloaded
        return Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
    }

    /// A root-container `Item`; its descendants are any rows seeded under
    /// `Self.account.davFilesUrl`.
    private func makeRootItem() -> Item {
        Item.rootContainer(
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            remoteSupportsTrash: false,
            log: FileProviderLogMock()
        )
    }

    /// Regression for #10085: a folder holding a materialized descendant file
    /// must offer the folder eviction action ("Remove downloaded items"), gated on
    /// `displayEvictDescendants` — never the file action `displayEvict`.
    func testItemUserInfoDisplayEvictDescendantsForFolderWithDownloadedFile() throws {
        try seedMetadataRow(
            ocId: "child-file", fileName: "report.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: true
        )

        let item = makeFolderItem()
        XCTAssertEqual(item.userInfo?["displayEvictDescendants"] as? Bool, true)
        // The file-labelled action ("Remove download") must never show on a folder.
        XCTAssertEqual(item.userInfo?["displayEvict"] as? Bool, false)

        // Assert against the exact SUBQUERY predicate Finder evaluates.
        let canEvictPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayEvictDescendants == true ).@count > 0"
        )
        XCTAssertTrue(
            canEvictPredicate.evaluate(with: ["fileproviderItems": [item]]),
            "A folder with a materialized descendant file must qualify for the folder evict action."
        )
    }

    /// A materialized file offers "Remove download" (`displayEvict`) but never the
    /// folder-labelled action (`displayEvictDescendants`) — the two never overlap.
    func testItemUserInfoFileDoesNotOfferDisplayEvictDescendants() {
        var metadata =
            SendableItemMetadata(ocId: "file-id", fileName: "report.pdf", account: Self.account)
        metadata.downloaded = true
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayEvict"] as? Bool, true)
        XCTAssertEqual(item.userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// A folder whose descendant files are all dataless has nothing to remove.
    func testItemUserInfoNoDisplayEvictDescendantsForFolderWithoutDownloadedFile() throws {
        try seedMetadataRow(
            ocId: "child-file", fileName: "report.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: false
        )

        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// A materialized descendant *sub-folder* (visited, but holding no downloaded
    /// file) must NOT make the action appear — only files count (#10085).
    func testItemUserInfoNoDisplayEvictDescendantsForFolderWithOnlyVisitedSubfolder() throws {
        try seedMetadataRow(
            ocId: "subfolder", fileName: "nested", serverUrl: folderServerPath,
            directory: true, visitedDirectory: true
        )
        // A dataless file deeper in the tree must not count either.
        try seedMetadataRow(
            ocId: "deep-file", fileName: "note.txt", serverUrl: folderServerPath + "/nested",
            directory: false, downloaded: false
        )

        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// Pinned folders must NOT advertise the evict action even with materialized
    /// content — the -2008 NonEvictable guard applies to folders too (#9891).
    func testItemUserInfoDisplayEvictDescendantsGatedOnUnpinnedForFolder() throws {
        try seedMetadataRow(
            ocId: "child-file", fileName: "report.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: true
        )

        XCTAssertEqual(makeFolderItem(keepDownloaded: true).userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// A folder whose only downloaded descendant is individually pinned ("Always
    /// keep downloaded") has nothing removable — the action must stay hidden, so
    /// the handler never tries to evict strict-pinned content (-2008, #9891/#10085).
    func testItemUserInfoNoDisplayEvictDescendantsForFolderWithOnlyPinnedDownloadedFile() throws {
        try seedMetadataRow(
            ocId: "pinned-file", fileName: "keep.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: true, keepDownloaded: true
        )
        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// A downloaded file under a *sibling* folder sharing a name prefix
    /// (`.../folderX`) must not make `.../folder` advertise the action — guards
    /// the `+ "/"` boundary in the descendant predicate.
    func testItemUserInfoNoDisplayEvictDescendantsForPrefixSiblingFile() throws {
        try seedMetadataRow(
            ocId: "sibling-file", fileName: "report.pdf",
            serverUrl: Self.account.davFilesUrl + "/folderX",
            directory: false, downloaded: true
        )
        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// A downloaded file nested several levels deep must surface the folder action —
    /// exercises the `starts(with: path + "/")` recursion branch.
    func testItemUserInfoDisplayEvictDescendantsForDeepDownloadedFile() throws {
        try seedMetadataRow(
            ocId: "deep-file", fileName: "note.txt",
            serverUrl: folderServerPath + "/nested",
            directory: false, downloaded: true
        )
        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, true)
    }

    /// A soft-deleted (tombstoned) downloaded descendant must not keep the action visible.
    func testItemUserInfoNoDisplayEvictDescendantsForFolderWithDeletedDownloadedFile() throws {
        try seedMetadataRow(
            ocId: "child-file", fileName: "report.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: true, deleted: true
        )
        XCTAssertEqual(makeFolderItem().userInfo?["displayEvictDescendants"] as? Bool, false)
    }

    /// The root container offers "Remove downloaded items" for any downloaded file
    /// anywhere in the domain, and hides it when there is none — symmetric with
    /// pinning the whole file provider (#10085). Exercises the rootContainer branch
    /// of `fullServerPathUrl`.
    func testItemUserInfoDisplayEvictDescendantsForRootContainer() throws {
        XCTAssertEqual(makeRootItem().userInfo?["displayEvictDescendants"] as? Bool, false)

        try seedMetadataRow(
            ocId: "top-file", fileName: "top.pdf", serverUrl: Self.account.davFilesUrl,
            directory: false, downloaded: true
        )
        XCTAssertEqual(makeRootItem().userInfo?["displayEvictDescendants"] as? Bool, true)
        // The file-labelled action must never show on the root either.
        XCTAssertEqual(makeRootItem().userInfo?["displayEvict"] as? Bool, false)
    }

    /// A folder's `metadataVersion` must change when it gains a materialized
    /// descendant file, so the framework treats its cached snapshot as stale and
    /// re-reads the recomputed `displayEvict` instead of dropping the update
    /// (#10085). Without this, the ancestor-refresh nudge would be deduplicated.
    func testFolderMetadataVersionReflectsDescendantMaterialization() throws {
        let versionWithoutDownload = makeFolderItem().itemVersion.metadataVersion

        try seedMetadataRow(
            ocId: "child-file", fileName: "report.pdf", serverUrl: folderServerPath,
            directory: false, downloaded: true
        )

        let versionWithDownload = makeFolderItem().itemVersion.metadataVersion
        XCTAssertNotEqual(
            versionWithoutDownload,
            versionWithDownload,
            "A folder's metadataVersion must change when a descendant file materializes."
        )
    }

    func testItemUserInfoKeepDownloadedProperties() {
        var metadataA =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadataA.keepDownloaded = true

        let itemA = Item(
            metadata: metadataA,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(itemA.userInfo?["displayKeepDownloaded"] as? Bool, false)
        XCTAssertEqual(itemA.userInfo?["displayAllowAutoEvicting"] as? Bool, true)
        XCTAssertEqual(itemA.userInfo?["displayEvict"] as? Bool, false)

        let metadataB =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        let itemB = Item(
            metadata: metadataB,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertTrue(itemB.userInfo?["displayKeepDownloaded"] as? Bool == true)
        XCTAssertTrue(itemB.userInfo?["displayAllowAutoEvicting"] as? Bool == false)
        XCTAssertEqual(itemB.userInfo?["displayEvict"] as? Bool, false)

        var metadataC =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadataC.keepDownloaded = true
        metadataC.downloaded = true

        let itemC = Item(
            metadata: metadataC,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(itemC.userInfo?["displayKeepDownloaded"] as? Bool, false)
        XCTAssertEqual(itemC.userInfo?["displayAllowAutoEvicting"] as? Bool, true)
        // Pinned + downloaded: NOT offered for "Remove download". The user must
        // unpin first via "Allow automatic freeing up space"; once the
        // framework refreshes the item, the evict entry appears (#9891).
        XCTAssertEqual(itemC.userInfo?["displayEvict"] as? Bool, false)

        var metadataD =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadataD.downloaded = true

        let itemD = Item(
            metadata: metadataD,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(itemD.userInfo?["displayKeepDownloaded"] as? Bool, true)
        XCTAssertEqual(itemD.userInfo?["displayAllowAutoEvicting"] as? Bool, false)
        XCTAssertEqual(itemD.userInfo?["displayEvict"] as? Bool, true)
    }

    func testItemUserInfoDisplayOpenInBrowserBareMetadata() {
        // A bare item without a fileId and not yet uploaded has no server-side
        // counterpart, so "Open in browser" must be hidden. This covers freshly
        // created items pre-server-roundtrip as well as ignored items.
        let metadata = SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayOpenInBrowser"] as? Bool, false)
    }

    func testItemUserInfoDisplayOpenInBrowserUploadedItem() {
        // An uploaded item with a numeric fileId is the canonical case the
        // "Open in browser" action targets: PROPFIND for the private link can
        // resolve to a web URL the main app opens via QDesktopServices.
        var metadata = SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.fileId = "42"
        metadata.uploaded = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayOpenInBrowser"] as? Bool, true)

        // Cross-check the activation rule from `FileProviderExt/Info.plist` so
        // an accidental rename of the userInfo key would fail this test rather
        // than silently break the context menu entry.
        let fileproviderItems = ["fileproviderItems": [item]]
        let activationPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayOpenInBrowser == true ).@count > 0"
        )
        XCTAssertTrue(activationPredicate.evaluate(with: fileproviderItems))
    }

    func testItemUserInfoDisplayOpenInBrowserLockFileOfLocalOrigin() {
        // Lock files of local origin are placeholders not present on the
        // server, so "Open in browser" would resolve to nothing. Keep them
        // hidden even when a synthetic fileId is set.
        var metadata = SendableItemMetadata(ocId: "test-id", fileName: ".~lock.doc#", account: Self.account)
        metadata.fileId = "42"
        metadata.uploaded = true
        metadata.isLockFileOfLocalOrigin = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayOpenInBrowser"] as? Bool, false)
    }

    func testItemUserInfoDisplayCopyInternalLinkBareMetadata() {
        // A bare item without a fileId and not yet uploaded has no server-side
        // counterpart, so "Copy internal link" must be hidden. Mirrors the
        // bare-metadata case for `displayOpenInBrowser` because both actions
        // share the same server-side preconditions.
        let metadata = SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayCopyInternalLink"] as? Bool, false)
    }

    func testItemUserInfoDisplayCopyInternalLinkUploadedItem() {
        // An uploaded item with a numeric fileId is the canonical case the
        // "Copy internal link" action targets: PROPFIND for the private link
        // can resolve to the URL the main app writes to the clipboard.
        var metadata = SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.fileId = "42"
        metadata.uploaded = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayCopyInternalLink"] as? Bool, true)

        // Cross-check the activation rule from `FileProviderExt/Info.plist` so
        // an accidental rename of the userInfo key would fail this test rather
        // than silently break the context menu entry.
        let fileproviderItems = ["fileproviderItems": [item]]
        let activationPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayCopyInternalLink == true ).@count > 0"
        )
        XCTAssertTrue(activationPredicate.evaluate(with: fileproviderItems))
    }

    func testItemUserInfoDisplayCopyInternalLinkLockFileOfLocalOrigin() {
        // Lock files of local origin are placeholders not present on the
        // server, so the "Copy internal link" action would resolve to nothing.
        // Keep them hidden even when a synthetic fileId is set.
        var metadata = SendableItemMetadata(ocId: "test-id", fileName: ".~lock.doc#", account: Self.account)
        metadata.fileId = "42"
        metadata.uploaded = true
        metadata.isLockFileOfLocalOrigin = true

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertEqual(item.userInfo?["displayCopyInternalLink"] as? Bool, false)
    }

    func testItemUserInfoDisplayShare() {
        var metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadata.permissions = "GDNVW" // No "R" for shareable

        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        XCTAssertNil(item.userInfo?["displayShare"])

        let fileproviderItems = ["fileproviderItems": [item]]
        let lockPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayShare == nil ).@count > 0"
        )
        XCTAssertTrue(lockPredicate.evaluate(with: fileproviderItems))
    }

    func testItemLockFileUntrashable() {
        let metadata = SendableItemMetadata(
            ocId: "test-id", fileName: ".~lock.test.doc#", account: Self.account
        )
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertFalse(item.capabilities.contains(.allowsTrashing))
    }

    func testItemTrashabilityAffectedByCapabilities() async {
        let remoteInterface = MockRemoteInterface(account: Self.account)
        XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
        let remoteSupportsTrash = await remoteInterface.supportsTrash(account: Self.account)
        let metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test", account: Self.account)
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: remoteInterface,
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: remoteSupportsTrash,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(item.capabilities.contains(.allowsTrashing))
    }

    func testStoredItemTrashabilityFalseAffectedByCapabilities() async {
        let db = Self.dbManager.ncDatabase()
        debugPrint(db)

        let remoteInterface = MockRemoteInterface(account: Self.account)
        XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
        remoteInterface.capabilities =
            remoteInterface.capabilities.replacingOccurrences(of: ##""undelete": true,"##, with: "")
        let metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test", account: Self.account)
        Self.dbManager.addItemMetadata(metadata)
        let item = await Item.storedItem(
            identifier: .init(metadata.ocId),
            account: Self.account,
            remoteInterface: remoteInterface,
            dbManager: Self.dbManager,
            log: FileProviderLogMock()
        )
        XCTAssertEqual(item?.capabilities.contains(.allowsTrashing), false)
    }

    func testStoredItemTrashabilityTrueAffectedByCapabilities() async {
        let db = Self.dbManager.ncDatabase()
        debugPrint(db)

        let remoteInterface = MockRemoteInterface(account: Self.account)
        XCTAssert(remoteInterface.capabilities.contains(##""undelete": true,"##))
        let metadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test", account: Self.account)
        Self.dbManager.addItemMetadata(metadata)
        let item = await Item.storedItem(
            identifier: .init(metadata.ocId),
            account: Self.account,
            remoteInterface: remoteInterface,
            dbManager: Self.dbManager,
            log: FileProviderLogMock()
        )
        XCTAssertEqual(item?.capabilities.contains(.allowsTrashing), true)
    }

    func testCapabilitiesReadingFile() {
        // 1. Setup metadata for a readable file
        var metadata = SendableItemMetadata(
            ocId: "reading-file-id", fileName: "readable.txt", account: Self.account
        )
        metadata.permissions = "G"
        metadata.directory = false

        // 2. Create the item
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        // 3. Assertions
        XCTAssertTrue(
            item.capabilities.contains(.allowsReading),
            "Item with 'G' permission should be readable."
        )
    }

    func testCapabilitiesEnumeratingDirectory() {
        // 1. Setup metadata for a readable directory
        var metadata = SendableItemMetadata(
            ocId: "enum-dir-id", fileName: "MyFolder", account: Self.account
        )
        metadata.permissions = "G"
        metadata.directory = true

        // 2. Create the item
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        // 3. Assertions
        XCTAssertTrue(
            item.capabilities.contains(.allowsContentEnumerating),
            "Directory with 'G' permission should allow enumerating."
        )
    }

    func testCapabilitiesDeleting() {
        // Case 1: Deletable
        var deletableMetadata = SendableItemMetadata(
            ocId: "deletable-id", fileName: "deletable.txt", account: Self.account
        )
        deletableMetadata.permissions = "D"
        let canDeleteItem = Item(
            metadata: deletableMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(
            canDeleteItem.capabilities.contains(.allowsDeleting),
            "Item with 'D' permission should be deletable."
        )

        // Case 2: Locked
        var lockedMetadata = SendableItemMetadata(
            ocId: "locked-deletable-id", fileName: "locked.txt", account: Self.account
        )
        lockedMetadata.permissions = "D"
        lockedMetadata.lock = true
        let cannotDeleteLockedItem = Item(
            metadata: lockedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotDeleteLockedItem.capabilities.contains(.allowsDeleting),
            "Locked item should not be deletable."
        )

        // Case 3: No permission
        var noPermsMetadata = SendableItemMetadata(
            ocId: "no-del-perm-id", fileName: "readonly.txt", account: Self.account
        )
        noPermsMetadata.permissions = "G"
        let cannotDeleteNoPermsItem = Item(
            metadata: noPermsMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotDeleteNoPermsItem.capabilities.contains(.allowsDeleting),
            "Item without 'D' permission should not be deletable."
        )
    }

    func testCapabilitiesTrashing() {
        // Case 1: Can be trashed
        let trashableMetadata = SendableItemMetadata(
            ocId: "trashable-id", fileName: "trashable.txt", account: Self.account
        )
        let canTrashItem = Item(
            metadata: trashableMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(
            canTrashItem.capabilities.contains(.allowsTrashing),
            "Item should be trashable if server supports it."
        )

        // Case 2: Server does not support trash
        let cannotTrashItem = Item(
            metadata: trashableMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: false,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotTrashItem.capabilities.contains(.allowsTrashing),
            "Item should not be trashable if server does not support it."
        )

        // Case 3: Item is locked
        var lockedMetadata = SendableItemMetadata(
            ocId: "locked-trash-id", fileName: "locked.txt", account: Self.account
        )
        lockedMetadata.lock = true
        let cannotTrashLockedItem = Item(
            metadata: lockedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotTrashLockedItem.capabilities.contains(.allowsTrashing),
            "Locked item should not be trashable."
        )

        // Case 4: Item is a lock file
        let lockFileMetadata = SendableItemMetadata(
            ocId: "lockfile-id", fileName: ".~lock.file.docx#", account: Self.account
        )
        let cannotTrashLockFileItem = Item(
            metadata: lockFileMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotTrashLockFileItem.capabilities.contains(.allowsTrashing),
            "Office lock files should not be trashable."
        )
    }

    func testCapabilitiesWriting() {
        // Case 1: Writable
        var writableMetadata = SendableItemMetadata(
            ocId: "writable-id", fileName: "writable.txt", account: Self.account
        )
        writableMetadata.permissions = "W"
        let canWriteItem = Item(
            metadata: writableMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(
            canWriteItem.capabilities.contains(.allowsWriting),
            "File with 'W' permission should be writable."
        )

        // Case 2: Locked
        var lockedMetadata = writableMetadata
        lockedMetadata.ocId = "locked-writable-id"
        lockedMetadata.lock = true
        let cannotWriteLockedItem = Item(
            metadata: lockedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotWriteLockedItem.capabilities.contains(.allowsWriting),
            "Locked file should not be writable."
        )

        // Case 3: Is a directory
        var directoryMetadata = writableMetadata
        directoryMetadata.ocId = "dir-writable-id"
        directoryMetadata.directory = true
        let cannotWriteDirectoryItem = Item(
            metadata: directoryMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotWriteDirectoryItem.capabilities.contains(.allowsWriting),
            "Directory should not be writable."
        )
    }

    func testCapabilitiesRenamingAndReparenting() {
        let expected: NSFileProviderItemCapabilities = [.allowsRenaming, .allowsReparenting]

        // Case 1: Can be modified
        var modifiableMetadata = SendableItemMetadata(
            ocId: "modifiable-id", fileName: "moveme.txt", account: Self.account
        )
        modifiableMetadata.permissions = "NV"
        let canModifyItem = Item(
            metadata: modifiableMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(
            canModifyItem.capabilities.isSuperset(of: expected),
            "Item with 'NV' permission should be renamable and reparentable."
        )

        // Case 2: Is locked
        var lockedMetadata = modifiableMetadata
        lockedMetadata.ocId = "locked-modifiable-id"
        lockedMetadata.lock = true
        let cannotModifyLockedItem = Item(
            metadata: lockedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotModifyLockedItem.capabilities.isSuperset(of: expected),
            "Locked item should not be renamable or reparentable."
        )
    }

    func testCapabilitiesAddingSubItems() {
        // Case 1: Can add sub-items to a directory
        var dirMetadata =
            SendableItemMetadata(ocId: "dir-add-id", fileName: "MyFolder", account: Self.account)
        dirMetadata.permissions = "NV"
        dirMetadata.directory = true
        let canAddSubItemsItem = Item(
            metadata: dirMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertTrue(
            canAddSubItemsItem.capabilities.contains(.allowsAddingSubItems),
            "Directory with 'NV' should allow adding sub-items."
        )

        // Case 2: Cannot add sub-items to a file
        var fileMetadata = dirMetadata
        fileMetadata.ocId = "file-add-id"
        fileMetadata.directory = false
        let cannotAddToFileItem = Item(
            metadata: fileMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotAddToFileItem.capabilities.contains(.allowsAddingSubItems),
            "File should not allow adding sub-items."
        )

        // Case 3: Cannot add sub-items to a locked directory
        var lockedDirMetadata = dirMetadata
        lockedDirMetadata.ocId = "locked-dir-add-id"
        lockedDirMetadata.lock = true
        let cannotAddToLockedDirItem = Item(
            metadata: lockedDirMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )
        XCTAssertFalse(
            cannotAddToLockedDirItem.capabilities.contains(.allowsAddingSubItems),
            "Locked directory should not allow adding sub-items."
        )
    }

    func testCapabilitiesFullPermissionsFile() {
        var metadata = SendableItemMetadata(
            ocId: "full-perms-file", fileName: "do-it-all.txt", account: Self.account
        )
        metadata.permissions = "RGDNVW"
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        let expected: NSFileProviderItemCapabilities = [
            .allowsReading,
            .allowsDeleting,
            .allowsTrashing,
            .allowsWriting,
            .allowsRenaming,
            .allowsReparenting
        ]

        XCTAssertEqual(item.capabilities, expected)
    }

    func testCapabilitiesFullPermissionsFolder() {
        var metadata = SendableItemMetadata(
            ocId: "full-perms-folder", fileName: "SuperFolder", account: Self.account
        )
        metadata.permissions = "RGDNVW"
        metadata.directory = true
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        let expected: NSFileProviderItemCapabilities = [
            .allowsContentEnumerating,
            .allowsDeleting,
            .allowsTrashing,
            .allowsRenaming,
            .allowsReparenting,
            .allowsAddingSubItems
        ]

        XCTAssertEqual(item.capabilities, expected)
    }

    func testCapabilitiesNoPermissions() {
        var metadata =
            SendableItemMetadata(ocId: "no-perms", fileName: "nothing.txt", account: Self.account)
        metadata.permissions = "" // No permissions from server
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        // Trashing might still be allowed as it does not depend on the permission string
        let expected: NSFileProviderItemCapabilities = [.allowsTrashing]

        XCTAssertEqual(item.capabilities, expected)
    }

    func testDoesNotVendExcludingFromSync() {
        var metadata = SendableItemMetadata(
            ocId: "macos-exclusion", fileName: "file.txt", account: Self.account
        )
        metadata.permissions = ""
        let item = Item(
            metadata: metadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager,
            displayFileActions: false,
            remoteSupportsTrash: true,
            log: FileProviderLogMock()
        )

        // We deliberately do NOT vend `.allowsExcludingFromSync`. Finder's "Do not
        // synchronize" arrives as an ordinary `deleteItem` that cannot be distinguished
        // from a genuine delete, so honouring it would delete the item on the server and
        // lose data. This guard keeps the capability from being silently re-added.
        XCTAssertFalse(
            item.capabilities.contains(.allowsExcludingFromSync),
            "Item must not vend .allowsExcludingFromSync; see Item.capabilities for the rationale."
        )
    }

    func testItemShared() {
        var sharedMetadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        sharedMetadata.shareType = [ShareType.publicLink.rawValue]
        sharedMetadata.ownerId = Self.account.id
        sharedMetadata.ownerDisplayName = "Mr. Tester Testarino"
        let sharedItem = Item(
            metadata: sharedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertFalse(sharedItem.isShared)
        XCTAssertFalse(sharedItem.isSharedByCurrentUser)
        XCTAssertNil(sharedItem.ownerNameComponents) // Should be nil if it is shared by us

        var sharedByOtherMetadata = sharedMetadata
        sharedByOtherMetadata.ownerId = "claucambra"
        sharedByOtherMetadata.ownerDisplayName = "Claudio Cambra"
        let sharedByOtherTime = Item(
            metadata: sharedByOtherMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertFalse(sharedByOtherTime.isShared)
        XCTAssertFalse(sharedByOtherTime.isSharedByCurrentUser)
        XCTAssertNil(sharedByOtherTime.ownerNameComponents)

        var notSharedMetadata =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        notSharedMetadata.ownerId = Self.account.id
        notSharedMetadata.ownerDisplayName = "Mr. Tester Testarino"
        let notSharedItem = Item(
            metadata: notSharedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        debugPrint(notSharedMetadata.shareType)
        XCTAssertFalse(notSharedItem.isShared)
        XCTAssertFalse(notSharedItem.isSharedByCurrentUser)
        XCTAssertNil(notSharedItem.ownerNameComponents)
    }

    func testContentPolicy() {
        var metadataA =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        metadataA.keepDownloaded = true

        let itemA = Item(
            metadata: metadataA,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(itemA.contentPolicy, .downloadEagerlyAndKeepDownloaded)

        let metadataB =
            SendableItemMetadata(ocId: "test-id", fileName: "test.txt", account: Self.account)
        let itemB = Item(
            metadata: metadataB,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )
        XCTAssertEqual(itemB.contentPolicy, .inherited)
    }

    ///
    /// Regression coverage for #9891: the "Remove download" custom action must
    /// only be offered for items that are downloaded **and** not pinned via
    /// "Always keep downloaded".
    ///
    /// Pinning is enforced by `contentPolicy == .downloadEagerlyAndKeepDownloaded`,
    /// which the framework refuses to evict (-2008 `NonEvictable`).
    /// `requestModification` cannot signal a `contentPolicy` field change
    /// (`NSFileProviderItemFields` has no such case) and is queued
    /// asynchronously — there is no synchronous way to invalidate the
    /// framework's cached policy, so a tight unpin-then-evict path races. The
    /// fix is to keep the entry hidden until the framework has refreshed the
    /// item: both `displayEvict` and `contentPolicy` are read off the same
    /// `Item` returned by `item(for:)` and therefore flip together. The user
    /// flow is two-step: "Allow automatic freeing up space" → "Remove download".
    ///
    /// The action's activation rule in the file provider extension's
    /// `Info.plist` is a `SUBQUERY` over `userInfo.displayEvict`, so this
    /// asserts directly against the predicate Finder evaluates.
    ///
    func testItemUserInfoDisplayEvictGatedOnUnpinned() {
        var pinnedDownloadedMetadata =
            SendableItemMetadata(ocId: "pd", fileName: "pd.txt", account: Self.account)
        pinnedDownloadedMetadata.downloaded = true
        pinnedDownloadedMetadata.keepDownloaded = true
        let pinnedDownloadedItem = Item(
            metadata: pinnedDownloadedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        var unpinnedDownloadedMetadata =
            SendableItemMetadata(ocId: "ud", fileName: "ud.txt", account: Self.account)
        unpinnedDownloadedMetadata.downloaded = true
        let unpinnedDownloadedItem = Item(
            metadata: unpinnedDownloadedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        let onlyUnpinnedQualifiesPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayEvict == true ).@count == 1"
        )
        XCTAssertTrue(
            onlyUnpinnedQualifiesPredicate.evaluate(
                with: ["fileproviderItems": [pinnedDownloadedItem, unpinnedDownloadedItem]]
            ),
            "Of pinned+downloaded and unpinned+downloaded, only the unpinned item must qualify for the evict action."
        )

        // Sanity: pinned but undownloaded — nothing to evict — also excluded.
        var pinnedUndownloadedMetadata =
            SendableItemMetadata(ocId: "pu", fileName: "pu.txt", account: Self.account)
        pinnedUndownloadedMetadata.keepDownloaded = true
        let pinnedUndownloadedItem = Item(
            metadata: pinnedUndownloadedMetadata,
            parentItemIdentifier: .rootContainer,
            account: Self.account,
            remoteInterface: MockRemoteInterface(account: Self.account),
            dbManager: Self.dbManager
        )

        let noEvictPredicate = NSPredicate(
            format: "SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayEvict == true ).@count == 0"
        )
        XCTAssertTrue(
            noEvictPredicate.evaluate(
                with: ["fileproviderItems": [pinnedUndownloadedItem]]
            ),
            "Pinned but undownloaded items have nothing to evict and must not qualify."
        )
    }
}
