//
//  EntityPreloaderTests.swift
//  damus
//
//  Created by Daniel D'Aquino on 2026-01-22.
//

import XCTest
import NostrSDK
import Negentropy
@testable import damus

/// Tests for the EntityPreloader functionality.
///
/// These tests verify that EntityPreloader correctly:
/// - Extracts author and referenced pubkeys from notes
/// - Batches preload requests efficiently
/// - Uses standard Nostr subscriptions to fetch metadata
/// - Integrates properly with SubscriptionManager
final class EntityPreloaderTests: XCTestCase {
    
    // MARK: - Helper Functions
    
    /// Creates and runs a local relay on the specified port.
    /// - Parameter port: The port number to run the relay on
    /// - Returns: The running LocalRelay instance
    private func setupRelay(port: UInt16) async throws -> LocalRelay {
        let builder = RelayBuilder().port(port: port).rateLimit(limit: .init(maxReqs: 100, notesPerMinute: 100))
        let relay = LocalRelay(builder: builder)
        try await relay.run()
        print("Relay url: \(await relay.url())")
        return relay
    }
    
    /// Connects to a relay and waits for the connection to be established.
    /// - Parameters:
    ///   - url: The relay URL to connect to
    ///   - label: Optional label for logging (e.g., "Relay1", "Relay2")
    /// - Returns: The connected RelayConnection instance
    private func connectToRelay(url: RelayURL, label: String = "") async -> RelayConnection {
        var connectionContinuation: CheckedContinuation<Void, Never>?
        var timeoutTask: Task<Void, Never>?
        var isResumed = false
        
        let relayConnection = RelayConnection(url: url, handleEvent: { _ in }, processUnverifiedWSEvent: { wsEvent in
            let prefix = label.isEmpty ? "" : "(\(label)) "
            switch wsEvent {
            case .connected:
                if !isResumed {
                    isResumed = true
                    timeoutTask?.cancel()
                    connectionContinuation?.resume()
                }
            case .message(let message):
                print("ENTITY_PRELOADER_TEST \(prefix): Received: \(message)")
            case .disconnected(let closeCode, let string):
                print("ENTITY_PRELOADER_TEST \(prefix): Disconnected: \(closeCode); \(String(describing: string))")
            case .error(let error):
                print("ENTITY_PRELOADER_TEST \(prefix): Received error: \(error)")
            }
        })
        relayConnection.connect()
        
        // Wait for connection to be established with timeout
        await withCheckedContinuation { continuation in
            connectionContinuation = continuation
            
            // Start timeout task
            timeoutTask = Task {
                try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 seconds
                if !isResumed {
                    isResumed = true
                    print("ENTITY_PRELOADER_TEST \(label.isEmpty ? "" : "(\(label)) ")ERROR: Connection timeout after 30 seconds")
                    continuation.resume()
                    XCTFail("Connection to relay \(url) timed out after 30 seconds")
                }
            }
        }
        
        return relayConnection
    }
    
    /// Sends events to a relay connection.
    /// - Parameters:
    ///   - events: Array of NostrEvent to send
    ///   - connection: The RelayConnection to send events through
    private func sendEvents(_ events: [NostrEvent], to connection: RelayConnection) async throws {
        for event in events {
            connection.send(.typical(.event(event)))
        }
    }
    
    /// Sets up a NostrNetworkManager with the specified relay URLs.
    /// - Parameters:
    ///   - urls: Array of RelayURL to add to the manager
    ///   - ndb: The Ndb instance to use
    /// - Returns: Configured and connected NostrNetworkManager
    private func setupNetworkManager(with urls: [RelayURL], ndb: Ndb) async throws -> NostrNetworkManager {
        let delegate = TestNetworkDelegate(ndb: ndb, keypair: test_keypair, bootstrapRelays: urls)
        let networkManager = NostrNetworkManager(delegate: delegate, addNdbToRelayPool: true)
        
        // Manually add relays to the pool since we're bypassing normal initialization
        for url in urls {
            do {
                try await networkManager.userRelayList.insert(relay: .init(url: url, rwConfiguration: .readWrite), force: true)
            }
            catch {
                switch error {
                case .relayAlreadyExists: continue
                default: throw error
                }
            }
        }
        
        await networkManager.connect()
        // Wait for relay pool to be ready.
        try await Task.sleep(for: .seconds(2))
        
        return networkManager
    }
    
    /// Stores events in NostrDB for testing purposes.
    /// - Parameters:
    ///   - events: Array of NostrEvent to store in NDB
    ///   - ndb: The Ndb instance to store events in
    private func storeEventsInNdb(_ events: [NostrEvent], ndb: Ndb) {
        for event in events {
            do {
                try ndb.add(event: event)
            } catch {
                XCTFail("Failed to store event in NDB: \(error)")
            }
        }
    }
    
    /// Creates a test note with the specified content and referenced pubkeys.
    /// - Parameters:
    ///   - content: The content of the note
    ///   - keypair: The keypair to sign the note with
    ///   - referencedPubkeys: Pubkeys to add as p-tags
    /// - Returns: A NostrEvent with the specified references
    private func createNoteWithReferences(content: String, keypair: Keypair, referencedPubkeys: [Pubkey] = []) -> NostrEvent? {
        var tags: [[String]] = []
        for pubkey in referencedPubkeys {
            tags.append(["p", pubkey.hex()])
        }
        
        return NostrEvent(content: content, keypair: keypair, tags: tags)
    }
    
    /// Creates a metadata event for a given pubkey.
    /// - Parameters:
    ///   - keypair: The keypair for the profile
    ///   - displayName: Display name for the profile
    /// - Returns: A metadata NostrEvent
    private func createMetadataEvent(keypair: Keypair, displayName: String) -> NostrEvent? {
        let metadata = """
        {
            "name": "\(displayName)",
            "display_name": "\(displayName)",
            "about": "Test user"
        }
        """
        return NostrEvent(content: metadata, keypair: keypair, kind: 0)
    }
    
    // MARK: - Test Cases
    
    /// Test that preloading with .noPreloading strategy does not trigger metadata fetching
    func testNoPreloadingStrategy() async throws {
        // Given: A relay with a note and metadata for the author
        let relay = try await setupRelay(port: 9100)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = generate_new_keypair().to_keypair()
        let note = NostrEvent(content: "Test note", keypair: authorKeypair)!
        let metadata = createMetadataEvent(keypair: authorKeypair, displayName: "Test Author")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note, metadata], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedNote = XCTestExpectation(description: "Received note")
        
        // When: Streaming with .noPreloading strategy
        Task {
            var noteReceived = false
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .noPreloading
            ) {
                if !noteReceived {
                    noteReceived = true
                    receivedNote.fulfill()
                }
            }
        }
        
        // Then: Should receive the note but metadata should not be in NDB
        await fulfillment(of: [receivedNote], timeout: 5.0)
        
        // Give a moment to see if any preloading happens (it shouldn't)
        try await Task.sleep(for: .seconds(2))
        
        // Verify metadata was NOT preloaded into NDB
        let hasMetadata = (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in
            pr != nil
        })) ?? false
        XCTAssertFalse(hasMetadata, "Metadata should not be preloaded with .noPreloading strategy")
    }
    
    /// Test that preloading with .preload strategy fetches author metadata
    func testPreloadAuthorMetadata() async throws {
        // Given: A relay with a note and metadata for the author (metadata NOT in NDB)
        let relay = try await setupRelay(port: 9101)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = test_keypair
        let note = NostrEvent(content: "Test note", keypair: authorKeypair)!
        let metadata = createMetadataEvent(keypair: authorKeypair, displayName: "Test Author")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note, metadata], to: relayConnection)
        
        let damus_state = await generate_test_damus_state(mock_profile_info: [:])
        
        let ndb = damus_state.ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedNote = XCTestExpectation(description: "Received note")
        let metadataPreloaded = XCTestExpectation(description: "Metadata preloaded")
        
        // When: Streaming with .preload strategy
        Task {
            var noteReceived = false
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                if !noteReceived {
                    noteReceived = true
                    receivedNote.fulfill()
                    
                    // Poll for metadata to be preloaded
                    for _ in 0..<20 {
                        try? await Task.sleep(for: .milliseconds(100))
                        if (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false {
                            metadataPreloaded.fulfill()
                            break
                        }
                    }
                }
            }
        }
        
        // Then: Should receive the note and metadata should be preloaded
        await fulfillment(of: [receivedNote, metadataPreloaded], timeout: 30.0)
    }
    
    /// Test that preloading fetches metadata for all referenced pubkeys (p-tags)
    func testPreloadReferencedPubkeys() async throws {
        // Given: A relay with a note that references other users, and metadata for all referenced users
        let relay = try await setupRelay(port: 9102)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = test_keypair
        let user1Keypair = generate_new_keypair().to_keypair()
        let user2Keypair = generate_new_keypair().to_keypair()
        
        let note = createNoteWithReferences(
            content: "Mentioning users",
            keypair: authorKeypair,
            referencedPubkeys: [user1Keypair.pubkey, user2Keypair.pubkey]
        )!
        
        let authorMetadata = createMetadataEvent(keypair: authorKeypair, displayName: "Author")!
        let user1Metadata = createMetadataEvent(keypair: user1Keypair, displayName: "User 1")!
        let user2Metadata = createMetadataEvent(keypair: user2Keypair, displayName: "User 2")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note, authorMetadata, user1Metadata, user2Metadata], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedNote = XCTestExpectation(description: "Received note")
        let authorMetadataPreloaded = XCTestExpectation(description: "Author metadata preloaded")
        let user1MetadataPreloaded = XCTestExpectation(description: "User 1 metadata preloaded")
        let user2MetadataPreloaded = XCTestExpectation(description: "User 2 metadata preloaded")
        
        // When: Streaming with .preload strategy
        Task {
            var noteReceived = false
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                if !noteReceived {
                    noteReceived = true
                    receivedNote.fulfill()
                    
                    // Poll for all metadata to be preloaded
                    for _ in 0..<30 {
                        try? await Task.sleep(for: .milliseconds(200))
                        
                        var allPreloaded = true
                        
                        if !((try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false) {
                            allPreloaded = false
                        } else if !authorMetadataPreloaded.isInverted {
                            authorMetadataPreloaded.fulfill()
                        }
                        
                        if !((try? ndb.lookup_profile(user1Keypair.pubkey, borrow: { pr in pr != nil })) ?? false) {
                            allPreloaded = false
                        } else if !user1MetadataPreloaded.isInverted {
                            user1MetadataPreloaded.fulfill()
                        }
                        
                        if !((try? ndb.lookup_profile(user2Keypair.pubkey, borrow: { pr in pr != nil })) ?? false) {
                            allPreloaded = false
                        } else if !user2MetadataPreloaded.isInverted {
                            user2MetadataPreloaded.fulfill()
                        }
                        
                        if allPreloaded {
                            break
                        }
                    }
                }
            }
        }
        
        // Then: Should preload metadata for author and all referenced users
        await fulfillment(of: [receivedNote, authorMetadataPreloaded, user1MetadataPreloaded, user2MetadataPreloaded], timeout: 30.0)
    }
    
    /// Test that preloading batches requests efficiently when multiple notes arrive
    func testBatchingMultipleNotes() async throws {
        // Given: A relay with multiple notes from different authors and their metadata
        let relay = try await setupRelay(port: 9103)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let author1Keypair = test_keypair
        let author2Keypair = generate_new_keypair().to_keypair()
        let author3Keypair = generate_new_keypair().to_keypair()
        
        let note1 = NostrEvent(content: "Note 1", keypair: author1Keypair)!
        let note2 = NostrEvent(content: "Note 2", keypair: author2Keypair)!
        let note3 = NostrEvent(content: "Note 3", keypair: author3Keypair)!
        
        let metadata1 = createMetadataEvent(keypair: author1Keypair, displayName: "Author 1")!
        let metadata2 = createMetadataEvent(keypair: author2Keypair, displayName: "Author 2")!
        let metadata3 = createMetadataEvent(keypair: author3Keypair, displayName: "Author 3")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note1, note2, note3, metadata1, metadata2, metadata3], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedAllNotes = XCTestExpectation(description: "Received all notes")
        receivedAllNotes.expectedFulfillmentCount = 3
        
        let allMetadataPreloaded = XCTestExpectation(description: "All metadata preloaded")
        
        // When: Streaming multiple notes with .preload strategy
        Task {
            var notesReceived = 0
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                notesReceived += 1
                receivedAllNotes.fulfill()
                
                if notesReceived >= 3 {
                    print("🔍 All 3 notes received, polling for metadata preload...")
                    print("📝 Author 1 pubkey: \(author1Keypair.pubkey.hex())")
                    print("📝 Author 2 pubkey: \(author2Keypair.pubkey.hex())")
                    print("📝 Author 3 pubkey: \(author3Keypair.pubkey.hex())")
                    
                    // Poll for all metadata to be preloaded
                    // EntityPreloader batches after 1 second, plus time for negentropy sync
                    for iteration in 0..<60 {
                        try? await Task.sleep(for: .milliseconds(500))
                        
                        let hasAuthor1 = (try? ndb.lookup_profile(author1Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                        let hasAuthor2 = (try? ndb.lookup_profile(author2Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                        let hasAuthor3 = (try? ndb.lookup_profile(author3Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                        
                        if iteration % 4 == 0 {  // Log every 2 seconds
                            print("⏱️  Iteration \(iteration): Author1=\(hasAuthor1), Author2=\(hasAuthor2), Author3=\(hasAuthor3)")
                        }
                        
                        let hasAll = hasAuthor1 && hasAuthor2 && hasAuthor3
                        
                        if hasAll {
                            print("✅ All metadata preloaded!")
                            allMetadataPreloaded.fulfill()
                            break
                        }
                    }
                    
                    // Final check
                    let finalAuthor1 = (try? ndb.lookup_profile(author1Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                    let finalAuthor2 = (try? ndb.lookup_profile(author2Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                    let finalAuthor3 = (try? ndb.lookup_profile(author3Keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                    print("❌ Final state after timeout: Author1=\(finalAuthor1), Author2=\(finalAuthor2), Author3=\(finalAuthor3)")
                    
                    break
                }
            }
        }
        
        // Then: Should batch and preload all metadata
        await fulfillment(of: [receivedAllNotes, allMetadataPreloaded], timeout: 60.0)
    }
    
    /// Test that preloading works correctly with negentropy streaming mode
    func testPreloadWithNegentropyStreaming() async throws {
        // Given: A relay with a note and metadata, NDB has the note but not the metadata
        let relay = try await setupRelay(port: 9104)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = test_keypair
        let note = NostrEvent(content: "Test note", keypair: authorKeypair)!
        let metadata = createMetadataEvent(keypair: authorKeypair, displayName: "Test Author")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note, metadata], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        storeEventsInNdb([note], ndb: ndb)
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedNoteFromNdb = XCTestExpectation(description: "Received note from NDB")
        let metadataPreloaded = XCTestExpectation(description: "Metadata preloaded")
        
        // When: Using negentropy streaming with preload
        Task {
            var noteReceived = false
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: .negentropy),
                preloadStrategy: .preload
            ) {
                if !noteReceived {
                    noteReceived = true
                    receivedNoteFromNdb.fulfill()
                    
                    // Poll for metadata to be preloaded
                    for _ in 0..<20 {
                        try? await Task.sleep(for: .milliseconds(200))
                        if (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false {
                            metadataPreloaded.fulfill()
                            break
                        }
                    }
                }
            }
        }
        
        // Then: Should receive note from NDB and preload metadata from network
        await fulfillment(of: [receivedNoteFromNdb, metadataPreloaded], timeout: 30.0)
    }
    
    /// Test that preloading doesn't duplicate requests for the same pubkey
    func testPreloadDeduplication() async throws {
        // Given: A relay with multiple notes from the same author and their metadata
        let relay = try await setupRelay(port: 9105)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = test_keypair
        
        let note1 = NostrEvent(content: "Note 1", keypair: authorKeypair)!
        let note2 = NostrEvent(content: "Note 2", keypair: authorKeypair)!
        let note3 = NostrEvent(content: "Note 3", keypair: authorKeypair)!
        let metadata = createMetadataEvent(keypair: authorKeypair, displayName: "Test Author")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note1, note2, note3, metadata], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedAllNotes = XCTestExpectation(description: "Received all notes")
        receivedAllNotes.expectedFulfillmentCount = 3
        
        let metadataPreloaded = XCTestExpectation(description: "Metadata preloaded")
        
        // When: Streaming multiple notes from same author with .preload strategy
        Task {
            var notesReceived = 0
            for await _ in networkManager.reader.streamIndefinitely(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                notesReceived += 1
                receivedAllNotes.fulfill()
                
                if notesReceived == 3 {
                    // Poll for metadata to be preloaded
                    for _ in 0..<20 {
                        try? await Task.sleep(for: .milliseconds(200))
                        if (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false {
                            metadataPreloaded.fulfill()
                            break
                        }
                    }
                }
            }
        }
        
        // Then: Should receive all notes and preload metadata once (deduplication happens internally)
        await fulfillment(of: [receivedAllNotes, metadataPreloaded], timeout: 10.0)
        
        // Verify the metadata is in NDB
        let hasProfile = (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false
        XCTAssertTrue(hasProfile, "Metadata should be preloaded")
    }
    
    /// Test preloading with streamExistingEvents (which stops at EOSE)
    func testPreloadWithStreamExistingEvents() async throws {
        // Given: A relay with a note and metadata
        let relay = try await setupRelay(port: 9106)
        let relayUrl = RelayURL(await relay.url().description)!
        
        let authorKeypair = test_keypair
        let note = NostrEvent(content: "Test note", keypair: authorKeypair)!
        let metadata = createMetadataEvent(keypair: authorKeypair, displayName: "Test Author")!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([note, metadata], to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        let receivedNote = XCTestExpectation(description: "Received note")
        let streamEnded = XCTestExpectation(description: "Stream ended at EOSE")
        let metadataPreloaded = XCTestExpectation(description: "Metadata preloaded")
        
        // When: Using streamExistingEvents with preload
        Task {
            var noteReceived = false
            for await _ in networkManager.reader.streamExistingEvents(
                filters: [NostrFilter(kinds: [.text])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                if !noteReceived {
                    noteReceived = true
                    receivedNote.fulfill()
                }
            }
            streamEnded.fulfill()
            
            // Poll for metadata after stream ends
            for _ in 0..<20 {
                try? await Task.sleep(for: .milliseconds(200))
                if (try? ndb.lookup_profile(authorKeypair.pubkey, borrow: { pr in pr != nil })) ?? false {
                    metadataPreloaded.fulfill()
                    break
                }
            }
        }
        
        // Then: Should receive note, stream should end, and metadata should be preloaded
        await fulfillment(of: [receivedNote, streamEnded, metadataPreloaded], timeout: 30.0)
    }
    
    // MARK: - Follow Pack Race Condition Tests
    
    /// Creates a follow pack event (kind 39089) with the specified users.
    ///
    /// - Parameters:
    ///   - authorKeypair: The keypair of the follow pack creator
    ///   - title: Title of the follow pack
    ///   - packId: Unique identifier for the pack (d-tag)
    ///   - userPubkeys: Array of pubkeys to include in the pack
    /// - Returns: A NostrEvent representing the follow pack
    private func createFollowPackEvent(
        authorKeypair: Keypair,
        title: String,
        packId: String,
        userPubkeys: [Pubkey]
    ) -> NostrEvent? {
        var tags: [[String]] = [
            ["title", title],
            ["d", packId],
            ["description", "Test follow pack for preloader testing"]
        ]
        for pubkey in userPubkeys {
            tags.append(["p", pubkey.hex()])
        }
        return NostrEvent(
            content: "",
            keypair: authorKeypair,
            kind: NostrKind.follow_list.rawValue,
            tags: tags
        )
    }
    
    /// Test that verifies the race condition: when a follow pack event is received,
    /// the profiles listed in the pack are NOT yet available in NDB immediately after
    /// the event is yielded to the stream consumer.
    ///
    /// This test demonstrates the timing issue where views would render before
    /// profile metadata has been preloaded.
    func testFollowPackProfilesNotAvailableImmediately() async throws {
        // Given: A relay with a follow pack event containing 80 users, and metadata for all users
        let relay = try await setupRelay(port: 9107)
        let relayUrl = RelayURL(await relay.url().description)!
        
        // Create 65 users with their metadata
        let allUserKeypairs = (0..<65).map { _ in generate_new_keypair().to_keypair() }
        
        // Create metadata events for all users
        let metadataEvents = allUserKeypairs.map { keypair in
            createMetadataEvent(keypair: keypair, displayName: "User \(keypair.pubkey.hex().prefix(8))")!
        }
        
        // Create a follow pack event with p-tags for all users
        let packAuthorKeypair = test_keypair
        let followPackEvent = createFollowPackEvent(
            authorKeypair: packAuthorKeypair,
            title: "Test Follow Pack",
            packId: "test-pack-\(UUID().uuidString)",
            userPubkeys: allUserKeypairs.map { $0.pubkey }
        )!
        
        // Also create metadata for the pack author
        let packAuthorMetadata = createMetadataEvent(keypair: packAuthorKeypair, displayName: "Pack Author")!
        
        // Debug: verify the follow pack event has all p-tags
        let pTagCount = Array(followPackEvent.referenced_pubkeys).count
        print("🔍 DEBUG: Follow pack event has \(pTagCount) referenced pubkeys (expected 80)")
        
        // Send all events to the relay
        let relayConnection = await connectToRelay(url: relayUrl)
        print("🔍 DEBUG: Sending \(metadataEvents.count + 2) events to relay (1 follow pack + 1 author metadata + \(metadataEvents.count) user metadata)")
        try await sendEvents([followPackEvent, packAuthorMetadata] + metadataEvents, to: relayConnection)
        
        // Give the relay more time to store all events - 82 events need time to be processed
        try await Task.sleep(for: .seconds(10))
        print("🔍 DEBUG: Waited 3 seconds for relay to store events")
        
        // NDB starts empty (no profiles)
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        // Track which profiles were available immediately vs after delay
        var profilesAvailableImmediately: [Pubkey] = []
        var profilesAvailableAfterDelay: [Pubkey] = []
        
        let receivedFollowPack = XCTestExpectation(description: "Received follow pack event")
        let checkedImmediateAvailability = XCTestExpectation(description: "Checked immediate availability")
        let checkedDelayedAvailability = XCTestExpectation(description: "Checked delayed availability")
        
        // When: Streaming follow pack events with .preload strategy
        Task {
            for await lender in networkManager.reader.streamExistingEvents(
                filters: [NostrFilter(kinds: [.follow_list])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                receivedFollowPack.fulfill()
                
                // IMMEDIATELY check which profiles are available (simulates view rendering)
                // This is what happens when FollowPackView renders its profile pictures
                for keypair in allUserKeypairs {
                    let hasProfile = (try? ndb.lookup_profile(keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                    if hasProfile {
                        profilesAvailableImmediately.append(keypair.pubkey)
                    }
                }
                checkedImmediateAvailability.fulfill()
                
                print("📊 IMMEDIATE CHECK: \(profilesAvailableImmediately.count)/\(allUserKeypairs.count) profiles available")
                
                // Now wait for preloading to complete (up to 5 seconds)
                // The preloader batches after 1 second, plus time for negentropy sync
                for iteration in 0..<25 {
                    try? await Task.sleep(for: .milliseconds(200))
                    
                    var allLoaded = true
                    for keypair in allUserKeypairs {
                        let hasProfile = (try? ndb.lookup_profile(keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                        if !hasProfile {
                            allLoaded = false
                        }
                    }
                    
                    if iteration % 5 == 0 {
                        var loadedCount = 0
                        for keypair in allUserKeypairs {
                            if (try? ndb.lookup_profile(keypair.pubkey, borrow: { pr in pr != nil })) ?? false {
                                loadedCount += 1
                            }
                        }
                        print("⏱️  Iteration \(iteration): \(loadedCount)/\(allUserKeypairs.count) profiles loaded")
                    }
                    
                    if allLoaded { break }
                }
                
                // Check which profiles are available after the delay
                for keypair in allUserKeypairs {
                    let hasProfile = (try? ndb.lookup_profile(keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                    if hasProfile {
                        profilesAvailableAfterDelay.append(keypair.pubkey)
                    }
                }
                checkedDelayedAvailability.fulfill()
                
                print("📊 DELAYED CHECK: \(profilesAvailableAfterDelay.count)/\(allUserKeypairs.count) profiles available")
                
                break  // Only process first event
            }
        }
        
        await fulfillment(of: [receivedFollowPack, checkedImmediateAvailability, checkedDelayedAvailability], timeout: 60.0)
        
        // Then: This assertion demonstrates the race condition
        // In the current implementation, profiles are NOT available immediately
        // but SHOULD be available after the preloader has time to fetch them
        
        print("📊 FINAL RESULTS:")
        print("   - Profiles available immediately: \(profilesAvailableImmediately.count)/\(allUserKeypairs.count)")
        print("   - Profiles available after delay: \(profilesAvailableAfterDelay.count)/\(allUserKeypairs.count)")
        
        // This is the key assertion that demonstrates the bug:
        // If preloading worked synchronously (or the view waited for preloading),
        // all profiles would be available immediately.
        // But with the current async preloading, they're not.
        XCTAssertLessThan(
            profilesAvailableImmediately.count,
            allUserKeypairs.count,
            "Bug demonstration: Profiles should NOT all be available immediately due to async preloading race condition. " +
            "Got \(profilesAvailableImmediately.count)/\(allUserKeypairs.count) immediately available."
        )
        
        // But they should eventually be available after preloading completes
        XCTAssertEqual(
            profilesAvailableAfterDelay.count,
            allUserKeypairs.count,
            "After preloading delay, all profiles should be available. " +
            "Got \(profilesAvailableAfterDelay.count)/\(allUserKeypairs.count) after delay."
        )
    }
    
    /// Test that measures the actual delay between receiving a follow pack event
    /// and having all profiles available in NDB.
    ///
    /// This helps quantify the user-perceived latency of profile "pop-in".
    func testFollowPackPreloadingDelay() async throws {
        // Given: A relay with a follow pack event containing users and their metadata
        let relay = try await setupRelay(port: 9108)
        let relayUrl = RelayURL(await relay.url().description)!
        
        // Create 3 users with their metadata (smaller set for timing measurement)
        let user1Keypair = generate_new_keypair().to_keypair()
        let user2Keypair = generate_new_keypair().to_keypair()
        let user3Keypair = generate_new_keypair().to_keypair()
        
        let allUserKeypairs = [user1Keypair, user2Keypair, user3Keypair]
        
        let metadataEvents = allUserKeypairs.map { keypair in
            createMetadataEvent(keypair: keypair, displayName: "User \(keypair.pubkey.hex().prefix(8))")!
        }
        
        let packAuthorKeypair = test_keypair
        let followPackEvent = createFollowPackEvent(
            authorKeypair: packAuthorKeypair,
            title: "Timing Test Pack",
            packId: "timing-test-\(UUID().uuidString)",
            userPubkeys: allUserKeypairs.map { $0.pubkey }
        )!
        
        let relayConnection = await connectToRelay(url: relayUrl)
        try await sendEvents([followPackEvent] + metadataEvents, to: relayConnection)
        
        let ndb = await generate_test_damus_state(mock_profile_info: nil).ndb
        let networkManager = try await setupNetworkManager(with: [relayUrl], ndb: ndb)
        
        var eventReceivedTime: Date?
        var allProfilesLoadedTime: Date?
        
        let testCompleted = XCTestExpectation(description: "Test completed")
        
        // When: Streaming and measuring time to profile availability
        Task {
            for await _ in networkManager.reader.streamExistingEvents(
                filters: [NostrFilter(kinds: [.follow_list])],
                streamMode: .ndbAndNetworkParallel(networkOptimization: nil),
                preloadStrategy: .preload
            ) {
                eventReceivedTime = Date()
                
                // Poll until all profiles are loaded (max 10 seconds)
                for _ in 0..<100 {
                    try? await Task.sleep(for: .milliseconds(100))
                    
                    var allLoaded = true
                    for keypair in allUserKeypairs {
                        let hasProfile = (try? ndb.lookup_profile(keypair.pubkey, borrow: { pr in pr != nil })) ?? false
                        if !hasProfile {
                            allLoaded = false
                            break
                        }
                    }
                    
                    if allLoaded {
                        allProfilesLoadedTime = Date()
                        break
                    }
                }
                
                testCompleted.fulfill()
                break
            }
        }
        
        await fulfillment(of: [testCompleted], timeout: 30.0)
        
        // Then: Measure and report the delay
        guard let receivedTime = eventReceivedTime else {
            XCTFail("Event was never received")
            return
        }
        
        guard let loadedTime = allProfilesLoadedTime else {
            XCTFail("Profiles were never fully loaded within timeout")
            return
        }
        
        let delay = loadedTime.timeIntervalSince(receivedTime)
        print("⏱️  PRELOAD DELAY MEASUREMENT:")
        print("   - Event received at: \(receivedTime)")
        print("   - All profiles loaded at: \(loadedTime)")
        print("   - Total delay: \(String(format: "%.2f", delay)) seconds")
        
        // But it shouldn't take too long (within reasonable network latency)
        XCTAssertLessThan(
            delay,
            3.0,
            "Preloading should complete within a reasonable time. Actual delay: \(delay)s"
        )
    }
}

// MARK: - Test Doubles

/// Test delegate for NostrNetworkManager that provides minimal configuration for testing
private final class TestNetworkDelegate: NostrNetworkManager.Delegate {
    var ndb: Ndb
    var keypair: Keypair
    var latestContactListEvent: NostrEvent?
    var bootstrapRelays: [RelayURL]
    var developerMode: Bool = false
    var experimentalLocalRelayModelSupport: Bool = false
    var relayModelCache: RelayModelCache
    var relayFilters: RelayFilters
    var nwcWallet: WalletConnectURL?
    
    init(ndb: Ndb, keypair: Keypair, bootstrapRelays: [RelayURL]) {
        self.ndb = ndb
        self.keypair = keypair
        self.bootstrapRelays = bootstrapRelays
        self.relayModelCache = RelayModelCache()
        self.relayFilters = RelayFilters(our_pubkey: keypair.pubkey)
    }
}
