import XCTest
@testable import Rosetta

@MainActor
final class SearchParityTests: XCTestCase {
    func testSearchViewModelAndChatListUseSameQueryNormalization() async {
        let searchDispatcher = MockSearchDispatcher()
        let chatDispatcher = MockSearchDispatcher()
        let searchVM = SearchViewModel(searchDispatcher: searchDispatcher)
        let chatVM = ChatListViewModel(searchDispatcher: chatDispatcher)

        searchVM.setSearchQuery("   @Alice  ")
        chatVM.setSearchQuery("   @Alice  ")

        try? await Task.sleep(for: .milliseconds(1200))

        XCTAssertEqual(searchDispatcher.sentQueries, ["alice"])
        XCTAssertEqual(chatDispatcher.sentQueries, ["alice"])
    }

    func testSavedAliasesAndExactPublicKeyFallback() throws {
        let ownPair = try Self.makeKeyPair()
        let peerPair = try Self.makeKeyPair()

        let dialog = Self.makeDialog(
            account: ownPair.publicKeyHex,
            opponentKey: peerPair.publicKeyHex,
            username: "peer_user",
            title: "Peer User"
        )

        let saved = SearchParityPolicy.localAugmentedUsers(
            query: "Saved Messages",
            currentPublicKey: ownPair.publicKeyHex,
            dialogs: [dialog]
        )
        XCTAssertEqual(saved.count, 1)
        XCTAssertEqual(saved.first?.publicKey, ownPair.publicKeyHex)
        XCTAssertEqual(saved.first?.title, "Saved Messages")

        let exactPeer = SearchParityPolicy.localAugmentedUsers(
            query: "0x" + peerPair.publicKeyHex,
            currentPublicKey: ownPair.publicKeyHex,
            dialogs: [dialog]
        )
        XCTAssertEqual(exactPeer.count, 1)
        XCTAssertEqual(exactPeer.first?.publicKey, peerPair.publicKeyHex)
        XCTAssertEqual(exactPeer.first?.username, "peer_user")
    }

    func testServerAndLocalMergeDedupesByPublicKeyWithServerPriority() {
        let key = "021111111111111111111111111111111111111111111111111111111111111111"
        let localOnlyKey = "022222222222222222222222222222222222222222222222222222222222222222"

        let server = [
            SearchUser(username: "server_u", title: "Server Name", publicKey: key, verified: 2, online: 0),
        ]
        let local = [
            SearchUser(username: "local_u", title: "Local Name", publicKey: key, verified: 0, online: 1),
            SearchUser(username: "local_only", title: "Local Only", publicKey: localOnlyKey, verified: 0, online: 1),
        ]

        let merged = SearchParityPolicy.mergeServerAndLocal(server: server, local: local)
        XCTAssertEqual(merged.count, 2)
        XCTAssertEqual(merged[0].publicKey, key)
        XCTAssertEqual(merged[0].title, "Server Name")
        XCTAssertEqual(merged[1].publicKey, localOnlyKey)
    }

    func testActiveUiSearchIgnoresBackgroundUserInfoPackets() async {
        let bus = ChannelSearchBus()
        let uiChannel = SearchPacketChannel.ui(UUID())
        let uiDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: uiChannel)
        let userInfoDispatcher = ChannelBusSearchDispatcher(bus: bus, channel: .userInfo)
        let vm = SearchViewModel(searchDispatcher: uiDispatcher)

        var backgroundRequest = PacketSearch()
        backgroundRequest.search = "02aa"
        userInfoDispatcher.sendSearchPacket(backgroundRequest)

        vm.setSearchQuery("alice")
        try? await Task.sleep(for: .milliseconds(1200))
        XCTAssertEqual(uiDispatcher.sentQueries, ["alice"])

        var backgroundPacket = PacketSearch()
        backgroundPacket.users = [
            SearchUser(username: "random_user", title: "Random User", publicKey: "02aa", verified: 0, online: 0),
        ]
        userInfoDispatcher.emit(backgroundPacket)
        try? await Task.sleep(for: .milliseconds(80))

        XCTAssertTrue(vm.searchResults.isEmpty)
        XCTAssertTrue(vm.isSearching)

        var uiPacket = PacketSearch()
        uiPacket.users = [
            SearchUser(username: "alice_user", title: "Alice", publicKey: "02bb", verified: 1, online: 0),
        ]
        uiDispatcher.emit(uiPacket)
        try? await Task.sleep(for: .milliseconds(80))

        XCTAssertEqual(vm.searchResults.count, 1)
        XCTAssertEqual(vm.searchResults.first?.username, "alice_user")
        XCTAssertFalse(vm.isSearching)
    }
}

private extension SearchParityTests {
    static func makeKeyPair() throws -> (privateKeyHex: String, publicKeyHex: String) {
        let mnemonic = try CryptoManager.shared.generateMnemonic()
        let pair = try CryptoManager.shared.deriveKeyPair(from: mnemonic)
        return (pair.privateKey.hexString, pair.publicKey.hexString)
    }

    static func makeDialog(
        account: String,
        opponentKey: String,
        username: String,
        title: String
    ) -> Dialog {
        Dialog(
            id: UUID().uuidString,
            account: account,
            opponentKey: opponentKey,
            opponentTitle: title,
            opponentUsername: username,
            lastMessage: "",
            lastMessageTimestamp: 0,
            unreadCount: 0,
            isOnline: true,
            lastSeen: 0,
            verified: 0,
            iHaveSent: true,
            isPinned: false,
            isMuted: false,
            lastMessageFromMe: false,
            lastMessageDelivered: .delivered,
            lastMessageRead: true
        )
    }
}

private final class MockSearchDispatcher: SearchResultDispatching {
    var connectionState: ConnectionState = .authenticated
    var privateHash: String? = "mock-private-hash"
    private(set) var sentQueries: [String] = []
    private var handlers: [UUID: (PacketSearch) -> Void] = [:]

    func sendSearchPacket(_ packet: PacketSearch) {
        sentQueries.append(packet.search)
    }

    @discardableResult
    func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
        let id = UUID()
        handlers[id] = handler
        return id
    }

    func removeSearchResultHandler(_ id: UUID) {
        handlers.removeValue(forKey: id)
    }
}

@MainActor
private final class ChannelBusSearchDispatcher: SearchResultDispatching {
    let connectionState: ConnectionState = .authenticated
    let privateHash: String? = "mock-private-hash"
    private(set) var sentQueries: [String] = []

    private let bus: ChannelSearchBus
    private let channel: SearchPacketChannel

    init(bus: ChannelSearchBus, channel: SearchPacketChannel) {
        self.bus = bus
        self.channel = channel
    }

    func sendSearchPacket(_ packet: PacketSearch) {
        sentQueries.append(packet.search)
        bus.enqueue(channel: channel)
    }

    @discardableResult
    func addSearchResultHandler(_ handler: @escaping (PacketSearch) -> Void) -> UUID {
        bus.addHandler(channel: channel, handler)
    }

    func removeSearchResultHandler(_ id: UUID) {
        bus.removeHandler(id)
    }

    func emit(_ packet: PacketSearch) {
        bus.dispatch(packet)
    }
}

private final class ChannelSearchBus {
    private struct HandlerEntry {
        let channel: SearchPacketChannel
        let handler: (PacketSearch) -> Void
    }

    private var handlers: [UUID: HandlerEntry] = [:]
    private var pendingChannels: [SearchPacketChannel] = []

    @discardableResult
    func addHandler(
        channel: SearchPacketChannel,
        _ handler: @escaping (PacketSearch) -> Void
    ) -> UUID {
        let id = UUID()
        handlers[id] = HandlerEntry(channel: channel, handler: handler)
        return id
    }

    func removeHandler(_ id: UUID) {
        handlers.removeValue(forKey: id)
    }

    func enqueue(channel: SearchPacketChannel) {
        pendingChannels.append(channel)
    }

    func dispatch(_ packet: PacketSearch) {
        let routed = pendingChannels.isEmpty ? SearchPacketChannel.unscoped : pendingChannels.removeFirst()
        let callbacks = handlers.values
            .filter { $0.channel == .unscoped || $0.channel == routed }
            .map(\.handler)
        for callback in callbacks {
            callback(packet)
        }
    }
}
