//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import LibSignalClient
import XCTest

@testable import SignalServiceKit

class SignalRecipientTest: SSKBaseTest {

    private lazy var localAci = Aci.randomForTesting()
    private lazy var localPhoneNumber = E164("+16505550199")!
    private lazy var localIdentifiers = LocalIdentifiers(
        aci: localAci,
        pni: Pni.randomForTesting(),
        phoneNumber: localPhoneNumber.stringValue
    )

    override func setUp() {
        super.setUp()
        databaseStorage.write { tx in
            (DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
                localIdentifiers: localIdentifiers,
                tx: tx.asV2Write
            )
        }
    }

    func testSelfRecipientWithExistingRecord() {
        write { transaction in
            mergeHighTrust(aci: localAci, phoneNumber: localPhoneNumber, transaction: transaction)
            XCTAssertNotNil(fetchRecipient(aci: localAci, transaction: transaction))
            XCTAssertNotNil(fetchRecipient(phoneNumber: localPhoneNumber, transaction: transaction))
        }
    }

    func testRecipientWithExistingRecord() {
        let aci = Aci.randomForTesting()
        let phoneNumber = E164("+16505550101")!
        write { transaction in
            mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: transaction)
            XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
            XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: transaction))
        }
    }

    // MARK: - Low Trust

    func testLowTrustPhoneNumberOnly() {
        // Phone number only recipients are recorded
        write { tx in
            let recipientFetcher = DependenciesBridge.shared.recipientFetcher
            let phoneNumber = E164(CommonGenerator.e164())!
            _ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx.asV2Write)
            XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: tx))
        }
    }

    func testLowTrustUUIDOnly() {
        // UUID only recipients are recorded
        write { tx in
            let recipientFetcher = DependenciesBridge.shared.recipientFetcher
            let aci = Aci.randomForTesting()
            _ = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx.asV2Write)
            XCTAssertNotNil(fetchRecipient(aci: aci, transaction: tx))
        }
    }

    // MARK: - High Trust

    func testHighTrustUUIDOnly() {
        // UUID only recipients are recorded
        write { transaction in
            let aci = Aci.randomForTesting()
            _ = mergeHighTrust(aci: aci, phoneNumber: nil, transaction: transaction)
            XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
        }
    }

    func testHighTrustFullyQualified() {
        // Fully qualified addresses are recorded in their entirety

        let aci = Aci.randomForTesting()
        let phoneNumber = E164("+16505550101")!

        let addressToBeUpdated = SignalServiceAddress(phoneNumber: phoneNumber.stringValue)
        XCTAssertNil(addressToBeUpdated.serviceId)

        write { transaction in
            let aciProfile = OWSUserProfile.getOrBuildUserProfile(
                for: .otherUser(aci),
                userProfileWriter: .tests,
                tx: transaction
            )
            aciProfile.anyInsert(transaction: transaction)
            aciProfile.update(
                isPhoneNumberShared: .setTo(true),
                userProfileWriter: .tests,
                transaction: transaction,
                completion: nil
            )
            let recipient = mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: transaction)
            XCTAssertEqual(recipient.aci, aci)
            XCTAssertEqual(recipient.phoneNumber?.stringValue, phoneNumber.stringValue)

            // The incomplete address is automatically filled after marking the
            // complete address as registered.
            XCTAssertEqual(addressToBeUpdated.serviceId, aci)

            XCTAssertNotNil(fetchRecipient(aci: aci, transaction: transaction))
            XCTAssertNotNil(fetchRecipient(phoneNumber: phoneNumber, transaction: transaction))
        }
    }

    func testHighTrustMergeWithInvestedPhoneNumber() {
        // If there is a UUID-only contact and a phone number-only contact, and if
        // we later find out they are the same user, we must merge them.
        let aci = Aci.randomForTesting()
        let phoneNumber = E164("+16505550101")!

        write { tx in
            let recipientFetcher = DependenciesBridge.shared.recipientFetcher
            let uuidRecipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx.asV2Write)
            let phoneNumberRecipient = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx.asV2Write)
            let mergedRecipient = mergeHighTrust(aci: aci, phoneNumber: phoneNumber, transaction: tx)

            XCTAssertEqual(mergedRecipient.uniqueId, uuidRecipient.uniqueId)
            XCTAssertNil(SignalRecipient.anyFetch(uniqueId: phoneNumberRecipient.uniqueId, transaction: tx))
        }
    }

    func testHighTrustPhoneNumberChange() {
        let aci = Aci.randomForTesting()
        let oldPhoneNumber = E164("+16505550101")!
        let newPhoneNumber = E164("+16505550102")!
        let oldAddress = SignalServiceAddress(serviceId: aci, phoneNumber: oldPhoneNumber.stringValue)

        // Do this because of SignalServiceAddressTest.test_hashStability2().
        _ = SignalServiceAddress(serviceId: aci, phoneNumber: newPhoneNumber.stringValue)

        write { transaction in
            let oldThread = TSContactThread.getOrCreateThread(
                withContactAddress: oldAddress,
                transaction: transaction
            )

            let messageBuilder: TSIncomingMessageBuilder = .withDefaults(
                thread: oldThread,
                authorAci: aci,
                messageBody: "Test 123"
            )
            let oldMessage = messageBuilder.build()
            oldMessage.anyInsert(transaction: transaction)

            let oldPhoneNumberProfile = OWSUserProfile.getOrBuildUserProfile(
                for: .otherUser(aci),
                userProfileWriter: .tests,
                tx: transaction
            )
            oldPhoneNumberProfile.anyInsert(transaction: transaction)
            oldPhoneNumberProfile.update(
                isPhoneNumberShared: .setTo(true),
                userProfileWriter: .tests,
                transaction: transaction,
                completion: nil
            )
            let newPhoneNumberProfile = OWSUserProfile(
                address: .otherUser(SignalServiceAddress(phoneNumber: newPhoneNumber.stringValue))
            )
            newPhoneNumberProfile.anyInsert(transaction: transaction)

            let oldAccount = SignalAccount(address: oldAddress)
            oldAccount.anyInsert(transaction: transaction)

            mergeHighTrust(aci: aci, phoneNumber: oldPhoneNumber, transaction: transaction)
            mergeHighTrust(aci: aci, phoneNumber: newPhoneNumber, transaction: transaction)

            let newAddress = SignalServiceAddress(serviceId: aci, phoneNumber: newPhoneNumber.stringValue)

            let newThread = TSContactThread.getOrCreateThread(
                withContactAddress: newAddress,
                transaction: transaction
            )
            let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
                uniqueId: oldMessage.uniqueId,
                transaction: transaction
            )!
            let newProfile = OWSUserProfile.getOrBuildUserProfile(
                for: .otherUser(aci),
                userProfileWriter: .tests,
                tx: transaction
            )
            let newAccount = SignalAccount.anyFetch(
                uniqueId: oldAccount.uniqueId,
                transaction: transaction
            )

            // We maintain the same thread, profile, interactions, etc.
            // after the phone number change. They are updated to reflect
            // the new address.
            XCTAssertEqual(oldAddress.phoneNumber, newAddress.phoneNumber)
            XCTAssertEqual(oldAddress.serviceId, newAddress.serviceId)

            XCTAssertEqual(oldThread.uniqueId, newThread.uniqueId)
            XCTAssertNil(oldThread.contactPhoneNumber)
            XCTAssertNil(newThread.contactPhoneNumber)
            XCTAssertEqual(newAddress, newThread.contactAddress)

            XCTAssertEqual(oldMessage.uniqueId, newMessage.uniqueId)
            XCTAssertEqual(newAddress, newMessage.authorAddress)
            XCTAssertNil(oldMessage.authorPhoneNumber)
            XCTAssertNil(newMessage.authorPhoneNumber)

            XCTAssertEqual(newProfile.uniqueId, oldPhoneNumberProfile.uniqueId)
            XCTAssertNil(newProfile.phoneNumber)
            XCTAssertEqual(newProfile.serviceIdString, aci.serviceIdUppercaseString)
            XCTAssertNil(OWSUserProfile.anyFetch(uniqueId: newPhoneNumberProfile.uniqueId, transaction: transaction))

            XCTAssertNil(newAccount)
        }
    }

    func testHighTrustUUIDChange() throws {
        let oldAci = Aci.randomForTesting()
        let newAci = Aci.randomForTesting()
        let phoneNumber = E164("+16505550101")!
        let oldAddress = SignalServiceAddress(serviceId: oldAci, phoneNumber: phoneNumber.stringValue)

        try write { transaction in
            let oldThread = TSContactThread.getOrCreateThread(
                withContactAddress: oldAddress,
                transaction: transaction
            )

            let messageBuilder: TSIncomingMessageBuilder = .withDefaults(
                thread: oldThread,
                authorAci: oldAci,
                messageBody: "Test 123"
            )
            let oldMessage = messageBuilder.build()
            oldMessage.anyInsert(transaction: transaction)

            var oldProfile = OWSUserProfile(address: .otherUser(oldAddress))
            oldProfile.anyInsert(transaction: transaction)
            oldProfile.update(
                isPhoneNumberShared: .setTo(true),
                userProfileWriter: .tests,
                transaction: transaction,
                completion: nil
            )

            let oldAccount = SignalAccount(address: oldAddress)
            oldAccount.anyInsert(transaction: transaction)

            mergeHighTrust(aci: oldAci, phoneNumber: phoneNumber, transaction: transaction)
            mergeHighTrust(aci: newAci, phoneNumber: phoneNumber, transaction: transaction)

            let newAddress = SignalServiceAddress(serviceId: newAci, phoneNumber: phoneNumber.stringValue)

            let newThread = TSContactThread.getOrCreateThread(
                withContactAddress: newAddress,
                transaction: transaction
            )
            let newMessage = TSIncomingMessage.anyFetchIncomingMessage(
                uniqueId: oldMessage.uniqueId,
                transaction: transaction
            )!
            let newProfile = OWSUserProfile.getOrBuildUserProfile(
                for: .otherUser(newAci),
                userProfileWriter: .tests,
                tx: transaction
            )
            let newAccount = SignalAccount.anyFetch(
                uniqueId: oldAccount.uniqueId,
                transaction: transaction
            )!

            // When the UUID changes, we treat it as a new account. Old data
            // should remain associated with the old UUID, but have the phone
            // number stripped.
            XCTAssertNil(oldAddress.phoneNumber)
            XCTAssertNotEqual(oldAddress.phoneNumber, newAddress.phoneNumber)
            XCTAssertNotEqual(oldAddress.serviceId, newAddress.serviceId)

            oldThread.anyReload(transaction: transaction)
            XCTAssertNotEqual(oldThread.uniqueId, newThread.uniqueId)
            XCTAssertNil(oldThread.contactPhoneNumber)
            XCTAssertEqual(newAddress, newThread.contactAddress)
            XCTAssertNotEqual(newAddress, oldThread.contactAddress)

            XCTAssertEqual(oldMessage.uniqueId, newMessage.uniqueId)
            XCTAssertNil(newMessage.authorPhoneNumber)
            XCTAssertNotEqual(newAddress, newMessage.authorAddress)

            oldProfile = try XCTUnwrap(OWSUserProfile.anyFetch(uniqueId: oldProfile.uniqueId, transaction: transaction))
            XCTAssertNotEqual(oldProfile.uniqueId, newProfile.uniqueId)
            XCTAssertNil(oldProfile.phoneNumber)
            XCTAssertEqual(.otherUser(newAddress), newProfile.internalAddress)
            XCTAssertNotEqual(.otherUser(newAddress), oldProfile.internalAddress)

            XCTAssertEqual(newAccount.uniqueId, oldAccount.uniqueId)
            XCTAssertEqual(newAccount.recipientPhoneNumber, phoneNumber.stringValue)
            XCTAssertEqual(newAccount.recipientServiceId, newAci)
        }
    }

    private func createGroupAndThreads(for addresses: [(aci: Aci?, phoneNumber: E164?)]) -> TSGroupThread {
        return self.write { (tx) -> TSGroupThread in
            // Create a group with all the addresses.
            let groupThread = {
                return try! GroupManager.createGroupForTests(
                    members: addresses.map { SignalServiceAddress(serviceId: $0.aci, phoneNumber: $0.phoneNumber?.stringValue) },
                    transaction: tx
                )
            }()
            // Delete the group members that were created automatically.
            TSGroupMember.anyRemoveAllWithInstantiation(transaction: tx)
            // And construct the TSGroupMember members using the specific identifiers
            // that were provided.
            for address in addresses {
                let groupMember = TSGroupMember(
                    address: NormalizedDatabaseRecordAddress(
                        aci: address.aci,
                        phoneNumber: address.phoneNumber?.stringValue,
                        pni: nil
                    )!,
                    groupThreadId: groupThread.uniqueId,
                    lastInteractionTimestamp: NSDate.ows_millisecondTimeStamp()
                )
                groupMember.anyInsert(transaction: tx)

                TSContactThread.getOrCreateThread(
                    withContactAddress: SignalServiceAddress(serviceId: address.aci, phoneNumber: address.phoneNumber?.stringValue),
                    transaction: tx
                )
            }
            return groupThread
        }
    }

    private func assertEqual(groupMembers: [TSGroupMember], expectedAddresses: [(aci: Aci?, phoneNumber: E164?)]) {
        let actualValues = Set(groupMembers.lazy.map {
            "\($0.serviceId?.serviceIdUppercaseString ?? "nil")-\($0.phoneNumber ?? "nil")"
        })
        let expectedValues = Set(expectedAddresses.lazy.map {
            "\($0.aci?.serviceIdUppercaseString ?? "nil")-\($0.phoneNumber?.stringValue ?? "nil")"
        })
        XCTAssertEqual(actualValues, expectedValues)
    }

    // This tests an edge case around uuid<->phone number mapping changes.
    //
    // * There _is_ a SignalRecipient with (u1, p1).
    // * There _is no_ SignalRecipient with u2 or p2.
    // * There is a group g1.
    // * There is a TSGroupMember with (u1, p1, g1).
    // * There is a TSGroupMember with (u2, p2, g1).
    // * p2 becomes associated with u1.
    //
    // Therefore:
    //
    // * p2 must be mapped from u2 -> u1, but there is no existing phoneNumberInstance for p2.
    // * SignalRecipient.clearDBMappings(forPhoneNumber:) must clean up p2 before
    //   SignalRecipient.changePhoneNumber() is called on uuidInstance for u1.
    // * Otherwise we'll end up with two TSGroupMembers with (p2, g1) which violates
    //   a uniqueness constraint.
    func testDBMappingsEdgeCase1() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!
        let aci2 = Aci.randomForTesting()
        let phoneNumber2 = E164(CommonGenerator.e164())!

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: phoneNumber1),
            (aci: aci2, phoneNumber: phoneNumber2)
        ])

        write { tx in
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
            // phoneNumber2 becomes associated with aci1.
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber2, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        // We should still have two group members: (u1, p2) and (u2, nil).
        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci1, phoneNumber: nil),
            (aci: aci2, phoneNumber: nil)
        ])
    }

    // This tests an edge case around uuid<->phone number mapping changes.
    //
    // * There _is_ a SignalRecipient with (u1, p1).
    // * There _is no_ SignalRecipient with u2 or p2.
    // * There is a group g1.
    // * There is a TSGroupMember with (u1, p1, g1).
    // * There is a TSGroupMember with (nil, p2, g1).
    // * p2 becomes associated with u1.
    //
    // Therefore:
    //
    // * p2 must be mapped to u1, but there is no existing phoneNumberInstance for p2.
    // * SignalRecipient.clearDBMappings(forPhoneNumber:) must clean up p2 before
    //   SignalRecipient.changePhoneNumber() is called on uuidInstance for u1.
    // * Otherwise we'll end up with two TSGroupMembers with (p2, g1) which violates
    //   a uniqueness constraint.
    func testDBMappingsEdgeCase2() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!
        let phoneNumber2 = E164(CommonGenerator.e164())!

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: phoneNumber1),
            (aci: nil, phoneNumber: phoneNumber2)
        ])

        write { tx in
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
            // phoneNumber2 becomes associated with aci1.
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber2, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        // We should now have one group member: (u1, p2).
        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci1, phoneNumber: nil)
        ])
    }

    // This tests an edge case around uuid<->phone number mapping changes.
    //
    // * There _is_ a SignalRecipient with (u1, p1).
    // * There _is no_ SignalRecipient with u2 or p2.
    // * There is a group g1.
    // * There is a TSGroupMember with (u1, p1, g1).
    // * There is a TSGroupMember with (u2, p2, g1).
    // * p1 becomes associated with u2.
    //
    // Therefore:
    //
    // * p1 must be mapped from u1 -> u2, but there is no existing uuidInstance for u2.
    // * SignalRecipient.clearDBMappings(forUuid:) must clean up u2 before
    //   SignalRecipient.changePhoneNumber() is called on phoneNumberInstance for p1.
    // * Otherwise we'll end up with two TSGroupMembers with (p1, g1) which violates
    //   a uniqueness constraint.
    func testDBMappingsEdgeCase3() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!
        let aci2 = Aci.randomForTesting()
        let phoneNumber2 = E164(CommonGenerator.e164())!

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: phoneNumber1),
            (aci: aci2, phoneNumber: phoneNumber2)
        ])

        write { tx in
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
            // phoneNumber1 becomes associated with aci2.
            mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        // We should still have two group members: (u2, p1) and (u1, nil).
        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci2, phoneNumber: nil),
            (aci: aci1, phoneNumber: nil)
        ])
    }

    // This tests an edge case around uuid<->phone number mapping changes.
    //
    // * There _is_ a SignalRecipient with (u1, p1).
    // * There _is no_ SignalRecipient with u2 or p2.
    // * There is a group g1.
    // * There is a TSGroupMember with (u1, p1, g1).
    // * There is a TSGroupMember with (u2, nil, g1).
    // * p1 becomes associated with u2.
    //
    // Therefore:
    //
    // * p1 must be mapped from u1 -> u2, but there is no existing uuidInstance for u2.
    // * SignalRecipient.clearDBMappings(forUuid:) must clean up u2 before
    //   SignalRecipient.changePhoneNumber() is called on phoneNumberInstance for p1.
    // * Otherwise we'll end up with two TSGroupMembers with (p1, g1) which violates
    //   a uniqueness constraint.
    func testDBMappingsEdgeCase4() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!
        let aci2 = Aci.randomForTesting()

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: phoneNumber1),
            (aci: aci2, phoneNumber: nil)
        ])

        write { tx in
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
            // phoneNumber1 becomes associated with aci2.
            mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 3)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        // We should now have two group members: (u2, p1), (u1, nil).
        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci1, phoneNumber: nil),
            (aci: aci2, phoneNumber: nil)
        ])
    }

    /// This tests an edge case around groups & merging contacts.
    ///
    /// If a phone number and UUID are both part of a group, and if we later
    /// learn that they refer to the same account, we'll end up with a
    /// "duplicate" group member.
    ///
    /// This should only be possible in GV1 groups -- in GV2, every member
    /// should have a UUID, and that requirement would put us back in
    /// testDBMappingsEdgeCase4 territory.
    func testDBMappingsEdgeCase5() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: nil),
            (aci: nil, phoneNumber: phoneNumber1)
        ])

        write { tx in
            let recipientFetcher = DependenciesBridge.shared.recipientFetcher
            _ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx.asV2Write)
            mergeHighTrust(aci: aci1, phoneNumber: nil, transaction: tx)
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci1, phoneNumber: nil)
        ])
    }

    /// This tests an edge case around groups & merging contacts.
    ///
    /// If we merge an ACI & E164 into a single recipient, and then if another
    /// account claims that phone number, we should ensure that the original ACI
    /// is still in the group but the new ACI is not.
    func testDBMappingsEdgeCase6() {
        let aci1 = Aci.randomForTesting()
        let phoneNumber1 = E164(CommonGenerator.e164())!
        let aci2 = Aci.randomForTesting()

        let groupThread = createGroupAndThreads(for: [
            (aci: aci1, phoneNumber: nil),
            (aci: nil, phoneNumber: phoneNumber1)
        ])

        write { tx in
            let recipientFetcher = DependenciesBridge.shared.recipientFetcher
            _ = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber1, tx: tx.asV2Write)
            mergeHighTrust(aci: aci1, phoneNumber: nil, transaction: tx)
            mergeHighTrust(aci: aci1, phoneNumber: phoneNumber1, transaction: tx)
            mergeHighTrust(aci: aci2, phoneNumber: phoneNumber1, transaction: tx)
        }

        databaseStorage.read { tx in
            XCTAssertEqual(TSThread.anyCount(transaction: tx), 2)
        }

        let finalGroupMembers = databaseStorage.read { tx in
            GroupMemberStoreImpl().sortedFullGroupMembers(in: groupThread.uniqueId, tx: tx.asV2Read)
        }

        assertEqual(groupMembers: finalGroupMembers, expectedAddresses: [
            (aci: aci1, phoneNumber: nil)
        ])
    }

    // MARK: - Helpers

    @discardableResult
    private func mergeHighTrust(aci: Aci, phoneNumber: E164?, transaction tx: SDSAnyWriteTransaction) -> SignalRecipient {
        let recipientMerger = DependenciesBridge.shared.recipientMerger
        return recipientMerger.applyMergeFromContactSync(
            localIdentifiers: localIdentifiers,
            aci: aci,
            phoneNumber: phoneNumber,
            tx: tx.asV2Write
        )
    }

    private func fetchRecipient(aci: Aci, transaction: SDSAnyReadTransaction) -> SignalRecipient? {
        SignalRecipient.fetchRecipient(for: SignalServiceAddress(aci), onlyIfRegistered: false, tx: transaction)
    }

    private func fetchRecipient(phoneNumber: E164, transaction: SDSAnyReadTransaction) -> SignalRecipient? {
        SignalRecipient.fetchRecipient(for: SignalServiceAddress(phoneNumber), onlyIfRegistered: false, tx: transaction)
    }
}

final class SignalRecipient2Test: XCTestCase {
    private enum Constants {
        static let emptyDevices = "62706c6973743030d4010203040506070a582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572d1080954726f6f748001a30b0c0f55246e756c6cd10d0e5624636c6173738002d2101112135a24636c6173736e616d655824636c61737365735c4e534f726465726564536574a214155c4e534f726465726564536574584e534f626a65637408111a24293237494c5153575d6067696e79828f929f00000000000001010000000000000016000000000000000000000000000000a8"
    }

    func testDecodeStableRow() throws {
        let inMemoryDB = InMemoryDB()
        try inMemoryDB.write { tx in
            try InMemoryDB.shimOnlyBridge(tx).db.execute(sql: """
                INSERT INTO "model_SignalRecipient" (
                    "id", "recordType", "uniqueId", "devices", "recipientPhoneNumber", "recipientUUID", "unregisteredAtTimestamp"
                ) VALUES (
                    18,
                    31,
                    '00000000-0000-4000-8000-00000000000A',
                    X'62706c6973743030d4010203040506070a582476657273696f6e592461726368697665725424746f7058246f626a6563747312000186a05f100f4e534b657965644172636869766572d1080954726f6f748001a80b0c191a1b1c1d1e55246e756c6cd60d0e0f1011121314151617185624636c6173735b4e532e6f626a6563742e315b4e532e6f626a6563742e345b4e532e6f626a6563742e305b4e532e6f626a6563742e335b4e532e6f626a6563742e3280078003800680028005800410011002100510041006d21f2021225a24636c6173736e616d655824636c61737365735c4e534f726465726564536574a223245c4e534f726465726564536574584e534f626a65637400080011001a00240029003200370049004c00510053005c0062006f00760082008e009a00a600b200b400b600b800ba00bc00be00c000c200c400c600c800cd00d800e100ee00f100fe0000000000000201000000000000002500000000000000000000000000000107',
                    '+16505550100',
                    '00000000-0000-4000-8000-000000000000',
                    NULL
                ),
                (
                    21,
                    31,
                    '00000000-0000-4000-8000-00000000000B',
                    X'\(Constants.emptyDevices)',
                    '+16505550101',
                    '00000000-0000-4000-8000-000000000001',
                    1683679214631
                );
            """)
        }
        inMemoryDB.read { tx in
            let signalRecipients = try! SignalRecipient.fetchAll(InMemoryDB.shimOnlyBridge(tx).db)
            XCTAssertEqual(signalRecipients.count, 2)

            XCTAssertEqual(signalRecipients[0].id, 18)
            XCTAssertEqual(signalRecipients[0].uniqueId, "00000000-0000-4000-8000-00000000000A")
            XCTAssertEqual(signalRecipients[0].deviceIds, [1, 2, 5, 4, 6])
            XCTAssertEqual(signalRecipients[0].phoneNumber?.stringValue, "+16505550100")
            XCTAssertEqual(signalRecipients[0].phoneNumber?.isDiscoverable, false)
            XCTAssertEqual(signalRecipients[0].aciString, "00000000-0000-4000-8000-000000000000")
            XCTAssertEqual(signalRecipients[0].unregisteredAtTimestamp, nil)

            XCTAssertEqual(signalRecipients[1].id, 21)
            XCTAssertEqual(signalRecipients[1].uniqueId, "00000000-0000-4000-8000-00000000000B")
            XCTAssertEqual(signalRecipients[1].deviceIds, [])
            XCTAssertEqual(signalRecipients[1].phoneNumber?.stringValue, "+16505550101")
            XCTAssertEqual(signalRecipients[1].phoneNumber?.isDiscoverable, false)
            XCTAssertEqual(signalRecipients[1].aciString, "00000000-0000-4000-8000-000000000001")
            XCTAssertEqual(signalRecipients[1].unregisteredAtTimestamp, 1683679214631)
        }
    }

    func testDecodePni() throws {
        let inMemoryDB = InMemoryDB()
        try inMemoryDB.write { tx in
            try InMemoryDB.shimOnlyBridge(tx).db.execute(sql: """
                INSERT INTO "model_SignalRecipient" (
                    "id", "recordType", "uniqueId", "devices", "pni"
                ) VALUES (
                    1,
                    31,
                    '00000000-0000-4000-8000-000000000000',
                    X'\(Constants.emptyDevices)',
                    'PNI:10000000-2000-4000-8000-300000000004'
                );
            """)
        }
        inMemoryDB.read { tx in
            let signalRecipients = try! SignalRecipient.fetchAll(InMemoryDB.shimOnlyBridge(tx).db)
            XCTAssertEqual(signalRecipients.count, 1)

            XCTAssertEqual(signalRecipients[0].id, 1)
            XCTAssertEqual(signalRecipients[0].uniqueId, "00000000-0000-4000-8000-000000000000")
            XCTAssertEqual(signalRecipients[0].deviceIds, [])
            XCTAssertEqual(signalRecipients[0].pni, Pni.constantForTesting("PNI:10000000-2000-4000-8000-300000000004"))
        }
    }

    func testEncodePni() throws {
        let inMemoryDB = InMemoryDB()
        let pni = Pni.constantForTesting("PNI:30000000-5000-4000-8000-3000000000A9")
        inMemoryDB.insert(record: SignalRecipient(aci: nil, pni: pni, phoneNumber: nil))
        inMemoryDB.read { tx in
            let db = InMemoryDB.shimOnlyBridge(tx).db
            let rawPniValue = try! String.fetchOne(db, sql: #"SELECT "pni" FROM "model_SignalRecipient""#)!
            XCTAssertEqual(rawPniValue, pni.serviceIdUppercaseString)
        }
    }

    func testEqualityAndHashing() {
        let someRecipient = SignalRecipient(aci: Aci.randomForTesting(), pni: nil, phoneNumber: nil, deviceIds: [1, 2])
        let copiedRecipient = someRecipient.copyRecipient()
        XCTAssertEqual(copiedRecipient, someRecipient)
        XCTAssertEqual(copiedRecipient.hashValue, someRecipient.hashValue)
        XCTAssertEqual(Set([someRecipient, copiedRecipient]).count, 1)
    }

    func testUnregisteredTimestamps() {
        let aci = Aci.randomForTesting()
        let mockDb = MockDB()
        let recipientTable = MockRecipientDatabaseTable()
        let recipientFetcher = RecipientFetcherImpl(recipientDatabaseTable: recipientTable)
        let recipientManager = SignalRecipientManagerImpl(
            phoneNumberVisibilityFetcher: MockPhoneNumberVisibilityFetcher(),
            recipientDatabaseTable: recipientTable,
            storageServiceManager: FakeStorageServiceManager()
        )
        mockDb.write { tx in
            let recipient = recipientFetcher.fetchOrCreate(serviceId: aci, tx: tx)
            XCTAssertNotNil(recipient.unregisteredAtTimestamp)

            recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: false, tx: tx)
            XCTAssertNil(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp)

            recipientManager.markAsUnregisteredAndSave(recipient, unregisteredAt: .now, shouldUpdateStorageService: false, tx: tx)
            XCTAssertGreaterThan(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp!, 0)

            recipientManager.markAsRegisteredAndSave(recipient, shouldUpdateStorageService: false, tx: tx)
            XCTAssertNil(recipientTable.fetchRecipient(serviceId: aci, transaction: tx)!.unregisteredAtTimestamp)
        }
    }

    func testMarkAsRegistered() {
        struct TestCase {
            var initialDeviceIds: Set<UInt32>
            var addedDeviceId: UInt32
            var expectedDeviceIds: Set<UInt32>
        }
        let testCases: [TestCase] = [
            TestCase(initialDeviceIds: [], addedDeviceId: 1, expectedDeviceIds: [1]),
            TestCase(initialDeviceIds: [], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [1], addedDeviceId: 1, expectedDeviceIds: [1]),
            TestCase(initialDeviceIds: [1], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [2], addedDeviceId: 1, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [2], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [3], addedDeviceId: 1, expectedDeviceIds: [1, 3]),
            TestCase(initialDeviceIds: [3], addedDeviceId: 2, expectedDeviceIds: [1, 2, 3]),
            TestCase(initialDeviceIds: [1, 2], addedDeviceId: 1, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [1, 2], addedDeviceId: 2, expectedDeviceIds: [1, 2]),
            TestCase(initialDeviceIds: [1, 2, 3], addedDeviceId: 1, expectedDeviceIds: [1, 2, 3]),
            TestCase(initialDeviceIds: [1, 2, 3], addedDeviceId: 2, expectedDeviceIds: [1, 2, 3])
        ]
        let mockDb = MockDB()
        let recipientTable = MockRecipientDatabaseTable()
        let recipientFetcher = RecipientFetcherImpl(recipientDatabaseTable: recipientTable)
        let recipientManager = SignalRecipientManagerImpl(
            phoneNumberVisibilityFetcher: MockPhoneNumberVisibilityFetcher(),
            recipientDatabaseTable: recipientTable,
            storageServiceManager: FakeStorageServiceManager()
        )
        mockDb.write { tx in
            for testCase in testCases {
                let recipient = recipientFetcher.fetchOrCreate(serviceId: Aci.randomForTesting(), tx: tx)
                recipientManager.setDeviceIds(testCase.initialDeviceIds, for: recipient, shouldUpdateStorageService: false)
                recipientManager.markAsRegisteredAndSave(recipient, deviceId: testCase.addedDeviceId, shouldUpdateStorageService: false, tx: tx)
                XCTAssertEqual(Set(recipient.deviceIds), testCase.expectedDeviceIds, "\(testCase)")
            }
        }
    }
}
