import XCTest final class SettingsManagerSyncTests: XCTestCase { func testBookmarkMutationPathReusesEncodedPayload() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let addBookmarkSection = try extractFunction( named: "func addBookmark(provider: BooruProvider, tags: [String], folder: UUID? = nil)", from: source ) let updateBookmarksSection = try extractFunction(named: "func updateBookmarks(", from: source) let bookmarksSetterSection = try extractFunction(named: "var bookmarks: [SettingsBookmark]", from: source) let normalizedAddBookmark = strippingCommentsAndStrings(from: addBookmarkSection) let normalizedUpdateBookmarks = strippingCommentsAndStrings(from: updateBookmarksSection) let normalizedBookmarksSetter = strippingCommentsAndStrings(from: bookmarksSetterSection) let addBookmarkInlineEncodeCount = tokenCount( matching: #"Self\s*\.\s*encode\s*\(\s*updatedBookmarks\s*\)"#, in: normalizedAddBookmark ) let updateBookmarksPreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedUpdateBookmarks ) let bookmarksSetterPreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedBookmarksSetter ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( addBookmarkInlineEncodeCount, 0, "addBookmark should not pre-encode and then trigger another encode downstream." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( updateBookmarksPreEncodedForwardCount, 0, "updateBookmarks should forward a pre-encoded payload to syncableData to avoid duplicate encoding." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( bookmarksSetterPreEncodedForwardCount, 0, "bookmarks setter should forward a pre-encoded payload to syncableData." ) } func testFavoriteMutationPathReusesEncodedPayload() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let addFavoriteSection = try extractFunction( named: "func addFavorite(post: BooruPost, provider: BooruProvider, folder: UUID? = nil)", from: source ) let updateFavoritesSection = try extractFunction(named: "func updateFavorites(", from: source) let favoritesSetterSection = try extractFunction(named: "var favorites: [SettingsFavoritePost]", from: source) let normalizedAddFavorite = strippingCommentsAndStrings(from: addFavoriteSection) let normalizedUpdateFavorites = strippingCommentsAndStrings(from: updateFavoritesSection) let normalizedFavoritesSetter = strippingCommentsAndStrings(from: favoritesSetterSection) let addFavoriteInlineEncodeCount = tokenCount( matching: #"Self\s*\.\s*encode\s*\(\s*updatedFavorites\s*\)"#, in: normalizedAddFavorite ) let updateFavoritesPreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedUpdateFavorites ) let favoritesSetterPreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedFavoritesSetter ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( addFavoriteInlineEncodeCount, 0, "addFavorite should not pre-encode and then trigger another encode downstream." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( updateFavoritesPreEncodedForwardCount, 0, "updateFavorites should forward a pre-encoded payload to syncableData to avoid duplicate encoding." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( favoritesSetterPreEncodedForwardCount, 0, "favorites setter should forward a pre-encoded payload to syncableData." ) } func testSearchHistoryMutationPathReusesSortedPayloadAndAvoidsInlineResort() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let searchHistorySetterSection = try extractFunction( named: "var searchHistory: [BooruSearchQuery]", from: source ) let updateSearchHistorySection = try extractFunction( named: "func updateSearchHistory(", from: source ) let normalizedSetterSection = strippingCommentsAndStrings(from: searchHistorySetterSection) let normalizedUpdateSection = strippingCommentsAndStrings(from: updateSearchHistorySection) let setterSortedHistoryDeclarationCount = tokenCount( matching: #"\blet\s+sortedSearchHistory\s*="#, in: normalizedSetterSection ) let setterInlineSortClosureCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\)\s*\{\s*\$0\s*\.\s*sorted"#, in: normalizedSetterSection ) let setterPreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedSetterSection ) let updateSortedHistoryDeclarationCount = tokenCount( matching: #"\blet\s+sortedSearchHistory\s*="#, in: normalizedUpdateSection ) let updateInlineSortClosureCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\)\s*\{\s*\$0\s*\.\s*sorted"#, in: normalizedUpdateSection ) let updatePreEncodedForwardCount = tokenCount( matching: #"syncableData\s*\([\s\S]*?\bencodedData\s*:"#, in: normalizedUpdateSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( setterSortedHistoryDeclarationCount, 0, "searchHistory setter should precompute sorted values once." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( setterInlineSortClosureCount, 0, "searchHistory setter should avoid inline resorting inside syncableData." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( setterPreEncodedForwardCount, 0, "searchHistory setter should forward a pre-encoded payload to syncableData." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( updateSortedHistoryDeclarationCount, 0, "updateSearchHistory should precompute sorted values once." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( updateInlineSortClosureCount, 0, "updateSearchHistory should avoid inline resorting inside syncableData." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( updatePreEncodedForwardCount, 0, "updateSearchHistory should forward a pre-encoded payload to syncableData." ) } func testBatchedSyncPathGuardsUnchangedPayloadWrites() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let triggerSyncSection = try extractFunction( named: "private func triggerSyncIfNeeded(for key: SettingsSyncKey)", from: source ) let normalizedSection = strippingCommentsAndStrings(from: triggerSyncSection) let cloudWriteCount = tokenCount( matching: #"\bNSUbiquitousKeyValueStore\s*\.\s*default\s*\.\s*set\s*\(\s*encoded\s*,\s*forKey\s*:"#, in: normalizedSection ) let unchangedGuardCount = tokenCount( matching: #"\!\=\s*encoded\b"#, in: normalizedSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan(cloudWriteCount, 0, "Expected batched sync path to contain cloud writes.") // swiftlint:disable:next prefer_nimble XCTAssertGreaterThanOrEqual( unchangedGuardCount, cloudWriteCount, "Every batched iCloud write should be guarded to avoid unchanged payload rewrites." ) } func testBatchedSyncUsesCoordinatorQueue() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let triggerBatchSection = try extractFunction( named: "private func triggerBatchedSync()", from: source ) let normalizedSection = strippingCommentsAndStrings(from: triggerBatchSection) let coordinatorEnqueueCount = tokenCount( matching: #"\bsyncCoordinator\s*\.\s*enqueue\s*\("#, in: normalizedSection ) let detachedCount = tokenCount( matching: #"\bTask\s*\.\s*detached\b"#, in: normalizedSection ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( coordinatorEnqueueCount, 0, "Batched sync should enqueue through a single sync coordinator." ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( detachedCount, 0, "Batched sync should not launch detached tasks directly." ) } func testPerKeySyncPathAvoidsDetachedFanOut() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let triggerSyncSection = try extractFunction( named: "private func triggerSyncIfNeeded(for key: SettingsSyncKey)", from: source ) let normalizedSection = strippingCommentsAndStrings(from: triggerSyncSection) let detachedCount = tokenCount( matching: #"\bTask\s*\.\s*detached\b"#, in: normalizedSection ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( detachedCount, 0, "Per-key sync should run on the coordinator path without detached fan-out." ) } func testManualFullSyncAggregatesChangeNotification() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let fullSyncSection = try extractFunction( named: "func triggerSyncIfNeededForAll()", from: source ) let normalizedSection = strippingCommentsAndStrings(from: fullSyncSection) let bookmarksKeyCount = tokenCount(matching: #"\.bookmarks\b"#, in: normalizedSection) let favoritesKeyCount = tokenCount(matching: #"\.favorites\b"#, in: normalizedSection) let searchHistoryKeyCount = tokenCount(matching: #"\.searchHistory\b"#, in: normalizedSection) let customProvidersKeyCount = tokenCount(matching: #"\.customProviders\b"#, in: normalizedSection) let objectWillChangeCount = tokenCount( matching: #"\bobjectWillChange\s*\.\s*send\s*\("#, in: normalizedSection ) // swiftlint:disable:next prefer_nimble XCTAssertEqual( bookmarksKeyCount + favoritesKeyCount + searchHistoryKeyCount + customProvidersKeyCount, 4, "Full sync should evaluate all supported sync keys." ) // swiftlint:disable:next prefer_nimble XCTAssertGreaterThan( objectWillChangeCount, 0, "Full sync should emit a consolidated objectWillChange notification when merged state changes." ) } }