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

import Foundation
import LibSignalClient
import XCTest

@testable import SignalServiceKit

class PhoneNumberChangedMessageInserterTest: XCTestCase {
    func testDidLearnAssociation() {
        let ssaCache = SignalServiceAddressCache()

        let myAci = Aci.constantForTesting("00000000-0000-4000-8000-000000000000")
        let myPhoneNumber1 = E164("+16505550111")!
        let myPhoneNumber2 = E164("+16505550122")!
        let myAddress1 = ssaCache.makeAddress(serviceId: myAci, phoneNumber: myPhoneNumber1)

        let aliceAci = Aci.constantForTesting("00000000-0000-4000-8000-00000000000A")
        let alicePhoneNumber1 = E164("+16505550133")!
        let alicePhoneNumber2 = E164("+16505550144")!
        let aliceAddress1 = ssaCache.makeAddress(serviceId: aliceAci, phoneNumber: alicePhoneNumber1)

        let bobAci = Aci.constantForTesting("00000000-0000-4000-8000-00000000000B")
        let bobPhoneNumber1: E164? = nil
        let bobPhoneNumber2 = E164("+16505550166")!
        let bobPhoneNumber3 = E164("+16505550177")!
        let bobAddress1 = ssaCache.makeAddress(serviceId: bobAci, phoneNumber: bobPhoneNumber1)

        let groupWithEveryone = TSGroupThread.forUnitTest(groupId: 1, groupMembers: [myAddress1, aliceAddress1, bobAddress1])
        let groupWithoutAlice = TSGroupThread.forUnitTest(groupId: 2, groupMembers: [myAddress1, bobAddress1])
        let groupArchived = TSGroupThread.forUnitTest(groupId: 3, groupMembers: [myAddress1, aliceAddress1, bobAddress1])

        let myThread = TSContactThread(contactAddress: myAddress1)
        myThread.shouldThreadBeVisible = true
        let aliceThread = TSContactThread(contactAddress: aliceAddress1)
        aliceThread.shouldThreadBeVisible = true

        let threadStore = MockThreadStore()
        threadStore.insertThreads([groupWithEveryone, groupWithoutAlice, groupArchived, myThread, aliceThread])

        let mockDB = MockDB()

        let groupMemberStore = MockGroupMemberStore()
        mockDB.write { tx in
            // Create group members for all of the full members.
            for thread in [groupWithEveryone, groupWithoutAlice, groupArchived] {
                for fullMemberAddress in thread.groupMembership.fullMembers {
                    groupMemberStore.insert(
                        fullGroupMember: TSGroupMember(
                            address: NormalizedDatabaseRecordAddress(address: fullMemberAddress)!,
                            groupThreadId: thread.uniqueId,
                            lastInteractionTimestamp: 0),
                        tx: tx
                    )
                }
            }
        }

        let threadAssociatedDataStore = MockThreadAssociatedDataStore()
        threadAssociatedDataStore.values = Dictionary(uniqueKeysWithValues: threadStore.threads.map {
            ($0.uniqueId, ThreadAssociatedData(threadUniqueId: $0.uniqueId))
        })
        threadAssociatedDataStore.values[groupArchived.uniqueId] = ThreadAssociatedData(
            threadUniqueId: groupArchived.uniqueId,
            isArchived: true,
            isMarkedUnread: false,
            mutedUntilTimestamp: 0,
            audioPlaybackRate: 1
        )

        let interactionStore = MockInteractionStore()

        let mergeObserver = PhoneNumberChangedMessageInserter(
            groupMemberStore: groupMemberStore,
            interactionStore: interactionStore,
            threadAssociatedDataStore: threadAssociatedDataStore,
            threadStore: threadStore
        )

        // Alice changes her number.
        interactionStore.insertedInteractions = []
        mockDB.write { tx in
            mergeObserver.didLearnAssociation(
                mergedRecipient: makeRecipient(
                    aci: aliceAci,
                    oldPhoneNumber: alicePhoneNumber1,
                    newPhoneNumber: alicePhoneNumber2,
                    isLocalRecipient: false
                ),
                tx: tx
            )

            let threadIds = interactionStore.insertedInteractions.map { $0.uniqueThreadId }
            XCTAssertEqual(Set(threadIds), [groupWithEveryone.uniqueId, aliceThread.uniqueId])
        }

        // Bob acquires a number for the first time.
        interactionStore.insertedInteractions = []
        mockDB.write { tx in
            mergeObserver.didLearnAssociation(
                mergedRecipient: makeRecipient(
                    aci: bobAci,
                    oldPhoneNumber: bobPhoneNumber1,
                    newPhoneNumber: bobPhoneNumber2,
                    isLocalRecipient: false
                ),
                tx: tx
            )

            let threadIds = interactionStore.insertedInteractions.map { $0.uniqueThreadId }
            XCTAssertEqual(Set(threadIds), [])
        }

        // Bob changes his number.
        interactionStore.insertedInteractions = []
        mockDB.write { tx in
            mergeObserver.didLearnAssociation(
                mergedRecipient: makeRecipient(
                    aci: bobAci,
                    oldPhoneNumber: bobPhoneNumber2,
                    newPhoneNumber: bobPhoneNumber3,
                    isLocalRecipient: false
                ),
                tx: tx
            )

            let threadIds = interactionStore.insertedInteractions.map { $0.uniqueThreadId }
            XCTAssertEqual(Set(threadIds), [groupWithEveryone.uniqueId, groupWithoutAlice.uniqueId])
        }

        // The local user changes their number.
        interactionStore.insertedInteractions = []
        mockDB.write { tx in
            mergeObserver.didLearnAssociation(
                mergedRecipient: makeRecipient(
                    aci: myAci,
                    oldPhoneNumber: myPhoneNumber1,
                    newPhoneNumber: myPhoneNumber2,
                    isLocalRecipient: true
                ),
                tx: tx
            )

            let threadIds = interactionStore.insertedInteractions.map { $0.uniqueThreadId }
            XCTAssertEqual(Set(threadIds), [])
        }
    }

    private func makeRecipient(
        aci: Aci,
        oldPhoneNumber: E164?,
        newPhoneNumber: E164,
        isLocalRecipient: Bool
    ) -> MergedRecipient {
        let oldRecipient = SignalRecipient(aci: aci, pni: nil, phoneNumber: oldPhoneNumber)
        let newRecipient = oldRecipient.copyRecipient()
        newRecipient.phoneNumber = .init(stringValue: newPhoneNumber.stringValue, isDiscoverable: true)
        return MergedRecipient(isLocalRecipient: isLocalRecipient, oldRecipient: oldRecipient, newRecipient: newRecipient)
    }
}
