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

@testable import SignalServiceKit

import XCTest

final class DeleteForMeOutgoingSyncMessageManagerTest: XCTestCase {
    private var mockAddressableMessageFinder: MockDeleteForMeAddressableMessageFinder!
    private var mockSyncMessageSender: MockSyncMessageSender!
    private var mockRecipientDatabaseTable: MockRecipientDatabaseTable!
    private var mockThreadStore: MockThreadStore!
    private var mockTSAccountManager: MockTSAccountManager!

    private var outgoingSyncMessageManager: DeleteForMeOutgoingSyncMessageManagerImpl!

    override func setUp() {
        mockAddressableMessageFinder = MockDeleteForMeAddressableMessageFinder()
        mockSyncMessageSender = MockSyncMessageSender()
        mockRecipientDatabaseTable = MockRecipientDatabaseTable()
        mockThreadStore = MockThreadStore()
        mockTSAccountManager = MockTSAccountManager()

        outgoingSyncMessageManager = DeleteForMeOutgoingSyncMessageManagerImpl(
            addressableMessageFinder: mockAddressableMessageFinder,
            deleteForMeSyncMessageSettingsStore: MockDeleteForMeSyncMessageSettingsStore(),
            recipientDatabaseTable: mockRecipientDatabaseTable,
            syncMessageSender: mockSyncMessageSender,
            threadStore: mockThreadStore,
            tsAccountManager: mockTSAccountManager
        )
    }

    func testBatchedInteractionDeletes() {
        let thread = TSContactThread(contactAddress: .isolatedRandomForTesting())
        let messagesToDelete = (0..<1501).map { _ -> TSOutgoingMessage in
            return TSOutgoingMessage(uniqueId: .uniqueId(), thread: thread)
        }

        /// These should be ignored by the sync message sender, since they are
        /// not addressable.
        let interactionsToDelete = (0..<10).map { _ -> TSInteraction in
            return TSInteraction(uniqueId: .uniqueId(), thread: thread)
        }

        var expectedInteractionBatches: [Int] = [500, 500, 500, 1]
        mockSyncMessageSender.sendSyncMessageMock = { contents in
            guard let expectedBatchSize = expectedInteractionBatches.popFirst() else {
                XCTFail("Unexpected batch!")
                return
            }

            XCTAssertEqual(contents.messageDeletes.count, 1)
            XCTAssertEqual(contents.messageDeletes.first!.addressableMessages.count, expectedBatchSize)
        }

        MockDB().write { tx in
            outgoingSyncMessageManager.send(
                deletedInteractions: messagesToDelete + interactionsToDelete,
                thread: thread,
                tx: tx
            )
        }

        XCTAssertTrue(expectedInteractionBatches.isEmpty)
    }

    func testBatchedThreadDeletes() {
        let threadsToDelete = (0..<301).map { _ -> TSContactThread in
            return TSContactThread(contactAddress: .isolatedRandomForTesting())
        }

        var expectedThreadBatches: [Int] = [100, 100, 100, 1]
        mockSyncMessageSender.sendSyncMessageMock = { contents in
            guard let expectedBatchSize = expectedThreadBatches.popFirst() else {
                XCTFail("Unexpected batch!")
                return
            }

            XCTAssertEqual(contents.localOnlyConversationDelete.count, expectedBatchSize)
        }

        MockDB().write { tx in
            /// These should all be local-only deletes, since the threads have
            /// no messages (the `MockDeleteForMeAddressableMessageFinder` will
            /// return `[]` for all threads, at the time of writing).
            let deletionContexts: [DeleteForMeSyncMessage.Outgoing.ThreadDeletionContext] = threadsToDelete.map { thread in
                outgoingSyncMessageManager.buildThreadDeletionContext(
                    thread: thread,
                    isFullDelete: true,
                    tx: tx
                )!
            }

            outgoingSyncMessageManager.send(
                threadDeletionContexts: deletionContexts,
                tx: tx
            )
        }

        XCTAssertTrue(expectedThreadBatches.isEmpty)
    }
}

private extension String {
    static func uniqueId() -> String {
        return UUID().uuidString
    }
}

// MARK: - Mocks

private final class MockDeleteForMeSyncMessageSettingsStore: DeleteForMeSyncMessageSettingsStore {
    func isSendingEnabled(tx: any DBReadTransaction) -> Bool {
        return true
    }

    func enableSending(tx: any DBWriteTransaction) {
        owsFail("Not implemented!")
    }
}

private final class MockSyncMessageSender: DeleteForMeOutgoingSyncMessageManagerImpl.Shims.SyncMessageSender {
    var sendSyncMessageMock: ((
        _ contents: DeleteForMeOutgoingSyncMessage.Contents
    ) -> Void)!
    func sendSyncMessage(contents: DeleteForMeOutgoingSyncMessage.Contents, localThread: TSContactThread, tx: any DBWriteTransaction) {
        sendSyncMessageMock(contents)
    }
}
