import Foundation public protocol SessionBridge: Sendable { func authState() async throws -> AuthState func networkState() async throws -> NetworkState func pollEvents() async throws -> [SessionEvent] func sendPhoneNumber(_ phone: String) async throws func sendCode(_ code: String) async throws func sendPassword(_ password: String) async throws func loadFolders() async throws -> [Folder] func loadChats(folderId: Int32?) async throws -> [ChatSummary] func loadHistory(chatId: Int64) async throws -> [Message] func searchMessages(chatId: Int64, query: String) async throws -> [Message] func openProfile(chatId: Int64) async throws -> Profile func leaveChat(chatId: Int64) async throws func sendMessage(chatId: Int64, text: String, replyToMessageId: Int64?) async throws -> Message func editMessage(chatId: Int64, messageId: Int64, text: String) async throws -> Message func deleteMessages(chatId: Int64, messageIds: [Int64]) async throws func forwardMessages(toChatId: Int64, fromChatId: Int64, messageIds: [Int64]) async throws func react(chatId: Int64, messageId: Int64, reaction: String) async throws -> [Reaction] func pinnedMessages(chatId: Int64) async throws -> [Message] func copyPayload(chatId: Int64, messageId: Int64) async throws -> String func setDraft(chatId: Int64, text: String) async throws func downloadPhoto(fileId: Int32) async throws -> DownloadedFile func downloadVoice(fileId: Int32) async throws -> DownloadedFile } public actor FakeSessionBridge: SessionBridge { private var auth: AuthState private var chats: [ChatSummary] private var messages: [Int64: [Message]] private var events: [SessionEvent] private var nextMessageId: Int64 private static let baseMessageDate: Int32 = 1_700_000_000 public init(auth: AuthState = .waitPhoneNumber) { self.auth = auth let saved = ChatSummary( id: 1, title: "Saved Messages", username: "saved", lastMessage: "Hello from fake TDLib", unreadCount: 1, isPinned: true ) let team = ChatSummary( id: 2, title: "iOS Team", lastMessage: "Bridge smoke is green", unreadMentionCount: 1, folderIds: [0, 2], isMuted: true, draft: Draft(chatId: 2, text: "Follow up") ) self.chats = [saved, team] self.messages = [ 1: [ Message( id: 1, chatId: 1, senderName: "Alice", text: "Hello from fake TDLib", date: Self.baseMessageDate, media: .photo(PhotoMedia(fileId: 100, width: 1280, height: 720)), isOutgoing: false, isRead: false ) ], 2: [ Message( id: 2, chatId: 2, senderName: "Mikhail", text: "Bridge smoke is green", date: Self.baseMessageDate + 60, media: .voice(VoiceMedia(fileId: 200, duration: 12, mimeType: "audio/ogg")), isOutgoing: true ) ], ] self.events = [.chatListChanged([saved, team])] self.nextMessageId = 3 } public func authState() async throws -> AuthState { auth } public func networkState() async throws -> NetworkState { .ready } public func pollEvents() async throws -> [SessionEvent] { let drained = events events.removeAll() return drained } public func sendPhoneNumber(_ phone: String) async throws { auth = .waitCode events.append(.authChanged(auth)) } public func sendCode(_ code: String) async throws { auth = .waitPassword events.append(.authChanged(auth)) } public func sendPassword(_ password: String) async throws { auth = .ready events.append(.authChanged(auth)) } public func loadFolders() async throws -> [Folder] { [Folder(id: 0, name: "All"), Folder(id: 2, name: "Work")] } public func loadChats(folderId: Int32?) async throws -> [ChatSummary] { let result = folderId.map { folderId in chats.filter { $0.folderIds.contains(folderId) } } ?? chats events.append(.chatListChanged(result)) return result } public func loadHistory(chatId: Int64) async throws -> [Message] { messages[chatId] ?? [] } public func searchMessages(chatId: Int64, query: String) async throws -> [Message] { guard !query.isEmpty else { return messages[chatId] ?? [] } return (messages[chatId] ?? []).filter { $0.text.localizedCaseInsensitiveContains(query) || $0.senderName.localizedCaseInsensitiveContains(query) } } public func openProfile(chatId: Int64) async throws -> Profile { let chat = chats.first { $0.id == chatId } let profile = Profile( chatId: chatId, title: chat?.title ?? "Unknown", username: chat?.username, bio: chatId == 1 ? "Fake profile for the iOS app shell" : "Team chat", isGroup: chatId != 1, memberCount: chatId == 1 ? nil : 4 ) events.append(.profileLoaded(profile)) return profile } public func leaveChat(chatId: Int64) async throws { chats.removeAll { $0.id == chatId } messages.removeValue(forKey: chatId) events.append(.chatListChanged(chats)) } public func sendMessage(chatId: Int64, text: String, replyToMessageId: Int64?) async throws -> Message { let message = Message( id: nextMessageId, chatId: chatId, senderName: "Me", text: text, date: Int32(Date().timeIntervalSince1970), isOutgoing: true, replyText: replyToMessageId.map { "Reply to #\($0)" } ) nextMessageId += 1 messages[chatId, default: []].append(message) if let index = chats.firstIndex(where: { $0.id == chatId }) { chats[index].lastMessage = text chats[index].draft = nil } events.append(.messageAdded(chatId, message)) return message } public func editMessage(chatId: Int64, messageId: Int64, text: String) async throws -> Message { guard var chatMessages = messages[chatId], let index = chatMessages.firstIndex(where: { $0.id == messageId }) else { throw FakeBridgeError.messageNotFound } chatMessages[index].text = text chatMessages[index].editDate = Int32(Date().timeIntervalSince1970) messages[chatId] = chatMessages return chatMessages[index] } public func deleteMessages(chatId: Int64, messageIds: [Int64]) async throws { messages[chatId]?.removeAll { messageIds.contains($0.id) } } public func forwardMessages(toChatId: Int64, fromChatId: Int64, messageIds: [Int64]) async throws { let sourceMessages = (messages[fromChatId] ?? []).filter { messageIds.contains($0.id) } for source in sourceMessages { let forwarded = Message( id: nextMessageId, chatId: toChatId, senderName: "Me", text: source.text, date: Int32(Date().timeIntervalSince1970), isOutgoing: true, forwardSenderName: source.senderName ) nextMessageId += 1 messages[toChatId, default: []].append(forwarded) events.append(.messageAdded(toChatId, forwarded)) } } public func react(chatId: Int64, messageId: Int64, reaction: String) async throws -> [Reaction] { guard var chatMessages = messages[chatId], let index = chatMessages.firstIndex(where: { $0.id == messageId }) else { throw FakeBridgeError.messageNotFound } if let reactionIndex = chatMessages[index].reactions.firstIndex(where: { $0.emoji == reaction }) { chatMessages[index].reactions.remove(at: reactionIndex) } else { chatMessages[index].reactions.append(Reaction(emoji: reaction, count: 1, isChosen: true)) } messages[chatId] = chatMessages return chatMessages[index].reactions } public func pinnedMessages(chatId: Int64) async throws -> [Message] { Array((messages[chatId] ?? []).prefix(1)) } public func copyPayload(chatId: Int64, messageId: Int64) async throws -> String { guard let message = messages[chatId]?.first(where: { $0.id == messageId }) else { throw FakeBridgeError.messageNotFound } return message.text } public func setDraft(chatId: Int64, text: String) async throws { let draft = Draft(chatId: chatId, text: text) if let index = chats.firstIndex(where: { $0.id == chatId }) { chats[index].draft = text.isEmpty ? nil : draft } events.append(.draftChanged(draft)) } public func downloadPhoto(fileId: Int32) async throws -> DownloadedFile { DownloadedFile(fileId: fileId, path: "/tmp/fake-photo-\(fileId).jpg") } public func downloadVoice(fileId: Int32) async throws -> DownloadedFile { DownloadedFile(fileId: fileId, path: "/tmp/fake-voice-\(fileId).ogg") } } public enum FakeBridgeError: LocalizedError { case messageNotFound public var errorDescription: String? { switch self { case .messageNotFound: "Message not found" } } }