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

import LibSignalClient
import XCTest

@testable import SignalServiceKit

class TSOutgoingMessageTest: SSKBaseTest {
    private var identityManager: OWSIdentityManager { DependenciesBridge.shared.identityManager }

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

    func testIsUrgent() {
        write { transaction in
            let otherAddress = SignalServiceAddress(phoneNumber: "+12223334444")
            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            let message = messageBuilder.build(transaction: transaction)

            XCTAssertTrue(message.isUrgent)
        }
    }

    func testShouldNotStartExpireTimerWithMessageThatDoesNotExpire() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            let message = messageBuilder.build(transaction: transaction)

            XCTAssertFalse(message.shouldStartExpireTimer())

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: false, transaction: transaction)

            XCTAssertFalse(message.shouldStartExpireTimer())
        }
    }

    func testShouldStartExpireTimerWithSentMessage() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            messageBuilder.expiresInSeconds = 10
            let message = messageBuilder.build(transaction: transaction)

            XCTAssertFalse(message.shouldStartExpireTimer())

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: false, transaction: transaction)

            XCTAssertTrue(message.shouldStartExpireTimer())
        }
    }

    func testShouldNotStartExpireTimerWithAttemptingOutMessage() {
        write { transaction in
            let otherAddress = SignalServiceAddress(phoneNumber: "+12223334444")
            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            messageBuilder.expiresInSeconds = 10
            let message = messageBuilder.build(transaction: transaction)

            message.updateAllUnsentRecipientsAsSending(transaction: transaction)

            XCTAssertFalse(message.shouldStartExpireTimer())
        }
    }

    func testNoPniSignatureByDefault() {
        write { transaction in
            let otherAddress = SignalServiceAddress(serviceId: Aci.randomForTesting(), phoneNumber: "+12223334444")
            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!
            let content = try! SSKProtoContent(serializedData: messageData)
            XCTAssertNil(content.pniSignatureMessage)
        }
    }

    func testPniSignatureWhenNeeded() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = 100
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!
            let content = try! SSKProtoContent(serializedData: messageData)

            let messagePni = content.pniSignatureMessage!.pni
            XCTAssertEqual(messagePni, DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction.asV2Read)!.pni!.rawUUID.data)

            let aciKeyPair = identityManager.identityKeyPair(for: .aci, tx: transaction.asV2Read)!.identityKeyPair
            let pniKeyPair = identityManager.identityKeyPair(for: .pni, tx: transaction.asV2Read)!.identityKeyPair
            XCTAssert(try! pniKeyPair.identityKey.verifyAlternateIdentity(
                aciKeyPair.identityKey,
                signature: content.pniSignatureMessage!.signature!))
        }
    }

    func testReceiptClearsSharePhoneNumber() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = Date.ows_millisecondTimestamp()
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: true, transaction: transaction)

            let messageSendLog = SSKEnvironment.shared.messageSendLogRef
            let payloadId = messageSendLog.recordPayload(messageData, for: message, tx: transaction)!
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 1,
                message: message,
                tx: transaction
            )

            // Nothing changed yet...
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 1,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            XCTAssertFalse(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
        }
    }

    func testReceiptClearsSharePhoneNumberOnlyOnLastDevice() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = Date.ows_millisecondTimestamp()
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: true, transaction: transaction)

            let messageSendLog = SSKEnvironment.shared.messageSendLogRef
            let payloadId = messageSendLog.recordPayload(messageData, for: message, tx: transaction)!
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 1,
                message: message,
                tx: transaction
            )
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 2,
                message: message,
                tx: transaction
            )

            // Nothing changed yet...
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 1,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            // Still waiting on device #2!
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 2,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            // There we go.
            XCTAssertFalse(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
        }
    }

    func testReceiptDoesNotClearSharePhoneNumberIfNotSealedSender() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = Date.ows_millisecondTimestamp()
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: false, transaction: transaction)

            let messageSendLog = SSKEnvironment.shared.messageSendLogRef
            let payloadId = messageSendLog.recordPayload(messageData, for: message, tx: transaction)!
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 1,
                message: message,
                tx: transaction
            )

            // Nothing changed yet...
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 1,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            // Still not changed!
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
        }
    }

    func testReceiptDoesNotClearSharePhoneNumberIfNoPniSignature() {
        write { transaction in
            let otherAci = Aci.randomForTesting()
            let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = Date.ows_millisecondTimestamp()
            let message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: true, transaction: transaction)

            let messageSendLog = SSKEnvironment.shared.messageSendLogRef
            let payloadId = messageSendLog.recordPayload(messageData, for: message, tx: transaction)!
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 1,
                message: message,
                tx: transaction
            )

            // If we set it now...
            XCTAssertFalse(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 1,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            // ...it should stay active.
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
        }
    }

    func testReceiptDoesNotClearSharePhoneNumberIfPniHasChanged() {
        let otherAci = Aci.randomForTesting()
        let otherAddress = SignalServiceAddress(serviceId: otherAci, phoneNumber: "+12223334444")
        var message: TSOutgoingMessage!

        write { transaction in
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            let thread = TSContactThread.getOrCreateThread(withContactAddress: otherAddress, transaction: transaction)
            let messageBuilder = TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread, messageBody: nil)
            messageBuilder.timestamp = Date.ows_millisecondTimestamp()
            message = messageBuilder.build(transaction: transaction)
            let messageData = message.buildPlainTextData(thread, transaction: transaction)!

            message.update(withSentRecipient: ServiceIdObjC.wrapValue(otherAci), wasSentByUD: true, transaction: transaction)

            let messageSendLog = SSKEnvironment.shared.messageSendLogRef
            let payloadId = messageSendLog.recordPayload(messageData, for: message, tx: transaction)!
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: otherAci,
                recipientDeviceId: 1,
                message: message,
                tx: transaction
            )
        }

        // Change our PNI, using registerForTests(...) instead of updateLocalPhoneNumber(...) because the latter kicks
        // off a request to check with the server.
        let aci = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction!.aci
        databaseStorage.write { tx in
            (DependenciesBridge.shared.registrationStateChangeManager as! RegistrationStateChangeManagerImpl).registerForTests(
                localIdentifiers: .init(
                    aci: aci,
                    pni: Pni.randomForTesting(),
                    e164: .init("+17775550199")!
                ),
                tx: tx.asV2Write
            )
        }

        write { transaction in
            // Changing your number resets this setting.
            XCTAssertFalse(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
            identityManager.setShouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Write)

            message.update(
                withDeliveredRecipient: otherAddress,
                deviceId: 1,
                deliveryTimestamp: NSDate.ows_millisecondTimeStamp(),
                context: PassthroughDeliveryReceiptContext(),
                tx: transaction
            )

            // Still on, because our PNI changed!
            XCTAssert(identityManager.shouldSharePhoneNumber(with: otherAci, tx: transaction.asV2Read))
        }
    }
}
