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

import GRDB
import LibSignalClient
import XCTest

@testable import SignalServiceKit

class MessageSendLogTests: SSKBaseTest {
    private var messageSendLog: MessageSendLog { SSKEnvironment.shared.messageSendLogRef }

    func testStoreAndRetrieveValidPayload() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to a recipient
            let serviceId = Aci.randomForTesting()
            let deviceId = UInt32.random(in: 0..<100)
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                message: newMessage,
                tx: writeTx
            )

            // Re-fetch the payload
            let fetchedPayload = messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                timestamp: newMessage.timestamp,
                tx: writeTx
            )!

            XCTAssertEqual(fetchedPayload.contentHint, .implicit)
            XCTAssertEqual(fetchedPayload.plaintextContent, payloadData)
            XCTAssertEqual(fetchedPayload.uniqueThreadId, newMessage.uniqueThreadId)
        }
    }

    func testStoreAndRetrievePayloadForInvalidRecipient() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to one recipient
            let serviceId = Aci.randomForTesting()
            let deviceId = UInt32.random(in: 0..<100)
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                message: newMessage,
                tx: writeTx
            )

            // Expect no results when re-fetching the payload with a different deviceId
            XCTAssertNil(messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: deviceId+1,
                timestamp: newMessage.timestamp,
                tx: writeTx
            ))

            // Expect no results when re-fetching the payload with a different address
            XCTAssertNil(messageSendLog.fetchPayload(
                recipientAci: Aci.randomForTesting(),
                recipientDeviceId: deviceId,
                timestamp: newMessage.timestamp,
                tx: writeTx
            ))
        }
    }

    func testStoreAndRetrievePayloadForDeliveredRecipient() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to two devices
            let serviceId = Aci.randomForTesting()
            for deviceId: UInt32 in [0, 1] {
                messageSendLog.recordPendingDelivery(
                    payloadId: payloadId,
                    recipientAci: serviceId,
                    recipientDeviceId: deviceId,
                    message: newMessage,
                    tx: writeTx
                )
            }

            // Mark the payload as "delivered" to the first device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                tx: writeTx
            )

            // Expect no results when re-fetching the payload for the first device
            XCTAssertNil(messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: 0,
                timestamp: newMessage.timestamp,
                tx: writeTx
            ))

            // Expect some results when re-fetching the payload for the second device
            XCTAssertNotNil(messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: 1,
                timestamp: newMessage.timestamp,
                tx: writeTx
            ))
        }
    }

    func testStoreAndRetrieveExpiredPayload() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload. Outgoing message date is long ago
            let newMessage = createOutgoingMessage(date: Date(timeIntervalSince1970: 10000), transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to a recipient
            let serviceId = Aci.randomForTesting()
            let deviceId = UInt32.random(in: 0..<100)
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                message: newMessage,
                tx: writeTx
            )

            // Expect no results when re-fetching the payload since it's expired
            XCTAssertNil(messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                timestamp: newMessage.timestamp,
                tx: writeTx
            ))
        }
    }

    func testFinalDeliveryRemovesPayload() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to one recipient, two devices
            let serviceId = Aci.randomForTesting()
            for deviceId: UInt32 in [0, 1] {
                messageSendLog.recordPendingDelivery(
                    payloadId: payloadId,
                    recipientAci: serviceId,
                    recipientDeviceId: deviceId,
                    message: newMessage,
                    tx: writeTx
                )
            }
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)

            // Deliver to first device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                tx: writeTx
            )

            // Verify the payload still exists
            XCTAssertTrue(isPayloadAlive(index: payloadId, transaction: writeTx))

            // Deliver to second device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                tx: writeTx
            )

            // Verify the payload was deleted
            XCTAssertFalse(isPayloadAlive(index: payloadId, transaction: writeTx))
        }
    }

    func testReceiveDeliveryBeforeSendFinished() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            let serviceId = Aci.randomForTesting()

            // "Send" the message to one device. It acks delivery
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                message: newMessage,
                tx: writeTx
            )
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                tx: writeTx
            )

            // Verify the payload still exists since we haven't finished sending
            XCTAssertTrue(isPayloadAlive(index: payloadId, transaction: writeTx))

            // "Send" the message to two more devices. Mark send as complete
            for deviceId: UInt32 in [1, 2] {
                messageSendLog.recordPendingDelivery(
                    payloadId: payloadId,
                    recipientAci: serviceId,
                    recipientDeviceId: deviceId,
                    message: newMessage,
                    tx: writeTx
                )
            }
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)

            // Deliver to second device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                tx: writeTx
            )

            // Verify the payload still exists
            XCTAssertTrue(isPayloadAlive(index: payloadId, transaction: writeTx))

            // Deliver to third device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 2,
                tx: writeTx
            )

            // Verify the payload was deleted
            XCTAssertFalse(isPayloadAlive(index: payloadId, transaction: writeTx))
        }
    }

    func testPartialFailureReusesPayloadEntry() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let initialPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            let serviceId = Aci.randomForTesting()

            // "Send" the message to one device. Complete send but don't mark as delivered.
            messageSendLog.recordPendingDelivery(
                payloadId: initialPayloadId,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                message: newMessage,
                tx: writeTx
            )
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)

            // Simulate a "retry" of a failed send. Try a fresh insert of the payload data
            // Then send to another device and complete the send.
            let retryPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))
            messageSendLog.recordPendingDelivery(
                payloadId: initialPayloadId,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                message: newMessage,
                tx: writeTx
            )
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)

            // Both payloadIds should be the same. The payload is still alive.
            XCTAssertEqual(initialPayloadId, retryPayloadId)
            XCTAssertTrue(isPayloadAlive(index: initialPayloadId, transaction: writeTx))

            // Deliver to first device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                tx: writeTx
            )

            // Verify the payload still exists
            XCTAssertTrue(isPayloadAlive(index: initialPayloadId, transaction: writeTx))

            // Deliver to second device
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                tx: writeTx
            )

            // Verify the payload was deleted
            XCTAssertFalse(isPayloadAlive(index: initialPayloadId, transaction: writeTx))
        }
    }

    func testRetryPartialFailureAfterAllInitialRecipientsAcked() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let serviceId = Aci.randomForTesting()

            // Record + Send + Deliver + Complete (Deliver and Complete can happen in either order)
            let initialPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))
            messageSendLog.recordPendingDelivery(
                payloadId: initialPayloadId,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                message: newMessage,
                tx: writeTx
            )
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 0,
                tx: writeTx
            )
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)

            // Verify payload is deleted:
            XCTAssertFalse(isPayloadAlive(index: initialPayloadId, transaction: writeTx))

            // Record + Send + Complete + Deliver (Deliver and Complete can happen in either order)
            let secondPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))
            messageSendLog.recordPendingDelivery(
                payloadId: secondPayloadId,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                message: newMessage,
                tx: writeTx
            )
            messageSendLog.sendComplete(message: newMessage, tx: writeTx)
            messageSendLog.recordSuccessfulDelivery(
                message: newMessage,
                recipientAci: serviceId,
                recipientDeviceId: 1,
                tx: writeTx
            )

            // Verify payload is deleted:
            XCTAssertFalse(isPayloadAlive(index: secondPayloadId, transaction: writeTx))
            // Verify the ID was not reusued
            XCTAssertNotEqual(initialPayloadId, secondPayloadId)
        }
    }

    // Test disabled since it exercises an owsFailDebug()
    // Works correctly if assertions are disabled and the test is enabled.
    func testPlaintextMismatchFails() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!

            let initialPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // append a byte so the payload doesn't match 
            let secondPayloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData + Data([1]), for: newMessage, tx: writeTx))

            XCTAssertNotNil(initialPayloadId)
            XCTAssertNil(secondPayloadId)
        }
    }

    func testDeleteMessageWithOnePayload() throws {
        try databaseStorage.write { writeTx in
            // Create and save the message payload
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let payloadData = CommonGenerator.sentence.data(using: .utf8)!
            let payloadId = try XCTUnwrap(messageSendLog.recordPayload(payloadData, for: newMessage, tx: writeTx))

            // "Send" the message to a recipient
            let serviceId = Aci.randomForTesting()
            let deviceId = UInt32.random(in: 0..<100)
            messageSendLog.recordPendingDelivery(
                payloadId: payloadId,
                recipientAci: serviceId,
                recipientDeviceId: deviceId,
                message: newMessage,
                tx: writeTx
            )

            // Delete the message
            messageSendLog.deleteAllPayloadsForInteraction(newMessage, tx: writeTx)

            // Verify the corresponding payload was deleted
            XCTAssertFalse(isPayloadAlive(index: payloadId, transaction: writeTx))
        }
    }

    func testDeleteMessageWithManyPayloads() throws {
        try databaseStorage.write { writeTx in
            // Create and save several message payloads
            let message1 = createOutgoingMessage(transaction: writeTx)
            let data1 = CommonGenerator.sentence.data(using: .utf8)!
            let index1 = try XCTUnwrap(messageSendLog.recordPayload(data1, for: message1, tx: writeTx))
            let message2 = createOutgoingMessage(transaction: writeTx)
            let data2 = CommonGenerator.sentence.data(using: .utf8)!
            let index2 = try XCTUnwrap(messageSendLog.recordPayload(data2, for: message2, tx: writeTx))

            let readReceiptMessage = createOutgoingMessage(relatedMessageIds: [message1.uniqueId, message2.uniqueId], transaction: writeTx)
            let data3 = CommonGenerator.sentence.data(using: .utf8)!
            let index3 = try XCTUnwrap(messageSendLog.recordPayload(data3, for: readReceiptMessage, tx: writeTx))

            // "Send" the messages to a recipient
            let serviceId = Aci.randomForTesting()
            let deviceId = UInt32.random(in: 0..<100)
            for index in [index1, index2, index3] {
                messageSendLog.recordPendingDelivery(
                    payloadId: index,
                    recipientAci: serviceId,
                    recipientDeviceId: deviceId,
                    message: message1,
                    tx: writeTx
                )
            }

            // Delete message1.
            messageSendLog.deleteAllPayloadsForInteraction(message1, tx: writeTx)

            // We expect that the read receipt message is deleted because it relates to the deleted message
            // We expect message2's payload to stick around, because none of its content is dependent on message1
            XCTAssertFalse(isPayloadAlive(index: index1, transaction: writeTx))
            XCTAssertTrue(isPayloadAlive(index: index2, transaction: writeTx))
            XCTAssertFalse(isPayloadAlive(index: index3, transaction: writeTx))
        }
    }

    func testCleanupExpiredPayloads() throws {
        let (oldId, newId) = try databaseStorage.write { writeTx in
            let oldMessage = createOutgoingMessage(date: Date(timeIntervalSince1970: 1000), transaction: writeTx)
            let oldData = CommonGenerator.sentence.data(using: .utf8)!
            let oldId = try XCTUnwrap(messageSendLog.recordPayload(oldData, for: oldMessage, tx: writeTx))
            let newMessage = createOutgoingMessage(transaction: writeTx)
            let newData = CommonGenerator.sentence.data(using: .utf8)!
            let newId = try XCTUnwrap(messageSendLog.recordPayload(newData, for: newMessage, tx: writeTx))

            // Verify both messages exist
            XCTAssertTrue(isPayloadAlive(index: oldId, transaction: writeTx))
            XCTAssertTrue(isPayloadAlive(index: newId, transaction: writeTx))

            return (oldId, newId)
        }

        try messageSendLog.cleanUpExpiredEntries()

        databaseStorage.read { tx in
            // Verify only the old message was deleted
            XCTAssertFalse(isPayloadAlive(index: oldId, transaction: tx))
            XCTAssertTrue(isPayloadAlive(index: newId, transaction: tx))
        }
    }

    func testTimestampMismatch() throws {
        // IOS-1762: Greyson reported an issue where a resent message would have a timestamp mismatch on the outside vs
        // inside of the envelope. In his case, the outside had a timestamp of 1629210680139 versus the inside
        // which had 1629210680140.
        //
        // Casting back and forth from Date/TimeInterval and millisecond timestamps would lead to float math
        // incorrectly coercing to the wrong timestamp. In this case, constructing a Date from that millisecond
        // timestamp would result in a time interval of 1629210680.1399999. Reconverting back to a timestamp and
        // we get 1629210680139.
        try databaseStorage.write { writeTx in
            let messageSendLog = MessageSendLog(
                db: DependenciesBridge.shared.db,
                dateProvider: { Date(timeIntervalSince1970: 1629270000) }
            )

            let originalTimestamp: UInt64 = 1629210680140
            let originalDate = Date(millisecondsSince1970: originalTimestamp)
            XCTAssertEqual(originalDate.ows_millisecondsSince1970, originalTimestamp)

            let serviceId = Aci.randomForTesting()
            let message = createOutgoingMessage(date: originalDate, transaction: writeTx)
            let data = CommonGenerator.sentence.data(using: .utf8)!
            XCTAssertEqual(message.timestamp, originalTimestamp)

            let index = try XCTUnwrap(messageSendLog.recordPayload(data, for: message, tx: writeTx))
            messageSendLog.recordPendingDelivery(payloadId: index, recipientAci: serviceId, recipientDeviceId: 1, message: message, tx: writeTx)

            let fetchedPayload = messageSendLog.fetchPayload(
                recipientAci: serviceId,
                recipientDeviceId: 1,
                timestamp: originalTimestamp,
                tx: writeTx
            )
            XCTAssertEqual(fetchedPayload?.sentTimestamp, originalTimestamp)
        }
    }

    // MARK: - Helpers

    class MSLTestMessage: TSOutgoingMessage {
        override init(outgoingMessageWithBuilder outgoingMessageBuilder: TSOutgoingMessageBuilder, transaction: SDSAnyReadTransaction) {
            super.init(outgoingMessageWithBuilder: outgoingMessageBuilder, transaction: transaction)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        required init(dictionary dictionaryValue: [String: Any]!) throws {
            fatalError("init(dictionary:) has not been implemented")
        }

        var _contentHint: SealedSenderContentHint = .resendable
        override var contentHint: SealedSenderContentHint { _contentHint }

        var _relatedMessageIds: [String] = []
        override var relatedUniqueIds: Set<String> { Set(_relatedMessageIds) }
    }

    func createOutgoingMessage(
        date: Date? = nil,
        contentHint: SealedSenderContentHint = .implicit,
        relatedMessageIds: [String] = [],
        transaction writeTx: SDSAnyWriteTransaction
    ) -> TSOutgoingMessage {

        let resolvedDate = date ?? {
            let newDate = Date()
            usleep(2000)    // If we're taking the timestamp of Now, wait a bit to avoid collisions
            return newDate
        }()

        let builder = TSOutgoingMessageBuilder(thread: ContactThreadFactory().create(transaction: writeTx),
                                               timestamp: resolvedDate.ows_millisecondsSince1970)
        let testMessage = MSLTestMessage(outgoingMessageWithBuilder: builder, transaction: writeTx)
        testMessage._contentHint = contentHint
        testMessage._relatedMessageIds = [testMessage.uniqueId] + relatedMessageIds
        return testMessage
    }

    func isPayloadAlive(index: Int64, transaction tx: SDSAnyReadTransaction) -> Bool {
        let count = try! MessageSendLog.Payload
            .filter(Column("payloadId") == index)
            .fetchCount(tx.unwrapGrdbRead.database)
        return count > 0
    }
}
