//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

@testable import ElementX

import Combine
import XCTest

@MainActor
class RoomScreenViewModelTests: XCTestCase {
    var userIndicatorControllerMock: UserIndicatorControllerMock!
    var cancellables = Set<AnyCancellable>()

    override func setUp() async throws {
        AppSettings.resetAllSettings()
        cancellables.removeAll()
        userIndicatorControllerMock = UserIndicatorControllerMock.default
    }

    override func tearDown() async throws {
        userIndicatorControllerMock = nil
    }
    
    // MARK: - Message Grouping

    func testMessageGrouping() {
        // Given 3 messages from Bob.
        let items = [
            TextRoomTimelineItem(text: "Message 1",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 2",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 3",
                                 sender: "bob")
        ]
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = items
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "")),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        // Then the messages should be grouped together.
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the middle message from being grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
    }
    
    func testMessageGroupingMultipleSenders() {
        // Given some interleaved messages from Bob and Alice.
        let items = [
            TextRoomTimelineItem(text: "Message 1",
                                 sender: "alice"),
            TextRoomTimelineItem(text: "Message 2",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 3",
                                 sender: "alice"),
            TextRoomTimelineItem(text: "Message 4",
                                 sender: "alice"),
            TextRoomTimelineItem(text: "Message 5",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 6",
                                 sender: "bob")
        ]
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = items
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "")),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        // Then the messages should be grouped by sender.
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "A message should not be grouped when the sender changes.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .single, "A message should not be grouped when the sender changes.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[3].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[4].groupStyle, .first, "A group should start with a new sender if there are more messages from that sender.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[5].groupStyle, .last, "A group should be ended when the sender changes in the next message.")
    }
    
    func testMessageGroupingWithLeadingReactions() {
        // Given 3 messages from Bob where the first message has a reaction.
        let items = [
            TextRoomTimelineItem(text: "Message 1",
                                 sender: "bob",
                                 addReactions: true),
            TextRoomTimelineItem(text: "Message 2",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 3",
                                 sender: "bob")
        ]
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = items
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "")),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        // Then the first message should not be grouped but the other two should.
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .single, "When the first message has reactions it should not be grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .first, "A new group should be made when the preceding message has reactions.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Nothing should prevent the last message from being grouped.")
    }
    
    func testMessageGroupingWithInnerReactions() {
        // Given 3 messages from Bob where the middle message has a reaction.
        let items = [
            TextRoomTimelineItem(text: "Message 1",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 2",
                                 sender: "bob",
                                 addReactions: true),
            TextRoomTimelineItem(text: "Message 3",
                                 sender: "bob")
        ]
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = items
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "")),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        // Then the first and second messages should be grouped and the last one should not.
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .last, "When the message has reactions, the group should end here.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .single, "The last message should not be grouped when the preceding message has reactions.")
    }
    
    func testMessageGroupingWithTrailingReactions() {
        // Given 3 messages from Bob where the last message has a reaction.
        let items = [
            TextRoomTimelineItem(text: "Message 1",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 2",
                                 sender: "bob"),
            TextRoomTimelineItem(text: "Message 3",
                                 sender: "bob",
                                 addReactions: true)
        ]
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = items
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "")),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        // Then the messages should be grouped together.
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[0].groupStyle, .first, "Nothing should prevent the first message from being grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[1].groupStyle, .middle, "Nothing should prevent the second message from being grouped.")
        XCTAssertEqual(viewModel.state.timelineViewState.itemViewStates[2].groupStyle, .last, "Reactions on the last message should not prevent it from being grouped.")
    }
    
    // MARK: - Sending

    func testRetrySend() async throws {
        let timelineController = MockRoomTimelineController()
        let roomProxyMock = RoomProxyMock(with: .init(name: ""))
        
        let timelineProxy = TimelineProxyMock()
        timelineProxy.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
        
        roomProxyMock.underlyingTimeline = timelineProxy
        timelineController.roomProxy = roomProxyMock

        let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())

        viewModel.context.send(viewAction: .retrySend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test retry send id")))
        
        try? await Task.sleep(for: .milliseconds(100))
        
        XCTAssert(timelineProxy.retrySendTransactionIDCallsCount == 1)
        XCTAssert(timelineProxy.retrySendTransactionIDReceivedInvocations == ["test retry send id"])
    }

    func testRetrySendNoTransactionID() async {
        let timelineController = MockRoomTimelineController()
        let roomProxyMock = RoomProxyMock(with: .init(name: ""))
        
        let timelineProxy = TimelineProxyMock()
        timelineProxy.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
        
        roomProxyMock.underlyingTimeline = timelineProxy

        let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())

        viewModel.context.send(viewAction: .retrySend(itemID: .random))
        
        try? await Task.sleep(for: .milliseconds(100))
        
        XCTAssert(timelineProxy.retrySendTransactionIDCallsCount == 0)
    }

    func testCancelSend() async {
        let timelineController = MockRoomTimelineController()
        let roomProxyMock = RoomProxyMock(with: .init(name: ""))
        
        let timelineProxy = TimelineProxyMock()
        timelineProxy.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
        
        roomProxyMock.underlyingTimeline = timelineProxy
        timelineController.roomProxy = roomProxyMock

        let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())

        viewModel.context.send(viewAction: .cancelSend(itemID: .init(timelineID: UUID().uuidString, transactionID: "test cancel send id")))
        
        try? await Task.sleep(for: .milliseconds(100))
        
        XCTAssert(timelineProxy.cancelSendTransactionIDCallsCount == 1)
        XCTAssert(timelineProxy.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"])
    }

    func testCancelSendNoTransactionID() async {
        let timelineController = MockRoomTimelineController()
        let roomProxyMock = RoomProxyMock(with: .init(name: ""))
        
        let timelineProxy = TimelineProxyMock()
        timelineProxy.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
        
        roomProxyMock.underlyingTimeline = timelineProxy

        let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock,
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())

        viewModel.context.send(viewAction: .cancelSend(itemID: .random))

        try? await Task.sleep(for: .milliseconds(100))
        
        XCTAssert(timelineProxy.cancelSendTransactionIDCallsCount == 0)
    }
    
    // MARK: - Read Receipts
    
    // swiftlint:disable force_unwrapping
    func testSendReadReceipt() async throws {
        // Given a room with only text items in the timeline
        let items = [TextRoomTimelineItem(eventID: "t1"),
                     TextRoomTimelineItem(eventID: "t2"),
                     TextRoomTimelineItem(eventID: "t3")]
        let (viewModel, roomProxy, timelineProxy, _, notificationCenter) = readReceiptsConfiguration(with: items)
        
        // When sending a read receipt for the last item.
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
        try await Task.sleep(for: .milliseconds(100))
        
        // Then the receipt should be sent.
        XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, true)
        let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
        XCTAssertEqual(arguments?.eventID, "t3")
        XCTAssertEqual(arguments?.type, .read)
        
        // And the notifications should be cleared.
        XCTAssertEqual(notificationCenter.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead)
        let roomID = notificationCenter.postNameObjectReceivedArguments?.anObject as? String
        XCTAssertEqual(roomID, roomProxy.id)
    }
    
    func testSendMoreReadReceipts() async throws {
        // Given a room with only text items in the timeline that are all read.
        let items = [TextRoomTimelineItem(eventID: "t1"),
                     TextRoomTimelineItem(eventID: "t2"),
                     TextRoomTimelineItem(eventID: "t3")]
        let (viewModel, _, timelineProxy, timelineController, _) = readReceiptsConfiguration(with: items)
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
        try await Task.sleep(for: .milliseconds(100))
        XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1)
        var arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
        XCTAssertEqual(arguments?.eventID, "t3")
        XCTAssertEqual(arguments?.type, .read)
        
        // When sending a receipt for the first item in the timeline.
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id))
        try await Task.sleep(for: .milliseconds(100))
        
        // When a new message is received and marked as read.
        let newMessage = TextRoomTimelineItem(eventID: "t4")
        timelineController.timelineItems.append(newMessage)
        timelineController.callbacks.send(.updatedTimelineItems)
        try await Task.sleep(for: .milliseconds(100))
        
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(newMessage.id))
        try await Task.sleep(for: .milliseconds(100))
        
        // Then the request should be made.
        XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 3)
        arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
        XCTAssertEqual(arguments?.eventID, "t4")
        XCTAssertEqual(arguments?.type, .read)
    }
    
    func testSendReadReceiptWithoutEvents() async throws {
        // Given a room with only virtual items.
        let items = [SeparatorRoomTimelineItem(timelineID: "v1"),
                     SeparatorRoomTimelineItem(timelineID: "v2"),
                     SeparatorRoomTimelineItem(timelineID: "v3")]
        let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items)
        
        // When sending a read receipt for the last item.
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
        try await Task.sleep(for: .milliseconds(100))
        
        // Then nothing should be sent.
        XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, false)
    }
    
    func testSendReadReceiptVirtualLast() async throws {
        // Given a room where the last event is a virtual item.
        let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"),
                                                 TextRoomTimelineItem(eventID: "t2"),
                                                 SeparatorRoomTimelineItem(timelineID: "v3")]
        let (viewModel, _, _, _, _) = readReceiptsConfiguration(with: items)
        
        // When sending a read receipt for the last item.
        viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
        try await Task.sleep(for: .milliseconds(100))
    }
    
    // swiftlint:enable force_unwrapping
    // swiftlint:disable:next large_tuple
    private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (RoomScreenViewModel,
                                                                                       RoomProxyMock,
                                                                                       TimelineProxyMock,
                                                                                       MockRoomTimelineController,
                                                                                       NotificationCenterMock) {
        let notificationCenter = NotificationCenterMock()
        let roomProxy = RoomProxyMock(with: .init(name: ""))
        
        let timelineProxy = TimelineProxyMock()
        timelineProxy.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
        
        roomProxy.timeline = timelineProxy
        let timelineController = MockRoomTimelineController()
        
        timelineProxy.sendReadReceiptForTypeReturnValue = .success(())
        
        timelineController.timelineItems = items
        timelineController.roomProxy = roomProxy

        let viewModel = RoomScreenViewModel(roomProxy: roomProxy,
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: notificationCenter)
        return (viewModel, roomProxy, timelineProxy, timelineController, notificationCenter)
    }
    
    func testShowReadReceipts() async throws {
        let receipts: [ReadReceipt] = [.init(userID: "@alice:matrix.org", formattedTimestamp: "12:00"),
                                       .init(userID: "@charlie:matrix.org", formattedTimestamp: "11:00")]
        // Given 3 messages from Bob where the middle message has a reaction.
        let message = TextRoomTimelineItem(text: "Test",
                                           sender: "bob",
                                           addReadReceipts: receipts)
        let id = message.id
        
        // When showing them in a timeline.
        let timelineController = MockRoomTimelineController()
        timelineController.timelineItems = [message]
        let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(name: "",
                                                                                 members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])),
                                            timelineController: timelineController,
                                            mediaProvider: MockMediaProvider(),
                                            mediaPlayerProvider: MediaPlayerProviderMock(),
                                            voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
                                            userIndicatorController: userIndicatorControllerMock,
                                            application: ApplicationMock.default,
                                            appSettings: ServiceLocator.shared.settings,
                                            analyticsService: ServiceLocator.shared.analytics,
                                            notificationCenter: NotificationCenterMock())
        
        let deferred = deferFulfillment(viewModel.context.$viewState) { value in
            value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
        }
        
        viewModel.context.send(viewAction: .showReadReceipts(itemID: id))
        try await deferred.fulfill()
    }
}

private extension TextRoomTimelineItem {
    init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) {
        let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(id: sender, timestamp: Date())])] : []
        self.init(id: .random,
                  timestamp: "10:47 am",
                  isOutgoing: sender == "bob",
                  isEditable: sender == "bob",
                  canBeRepliedTo: true,
                  isThreaded: false,
                  sender: .init(id: "@\(sender):server.com", displayName: sender),
                  content: .init(body: text),
                  properties: RoomTimelineItemProperties(reactions: reactions, orderedReadReceipts: addReadReceipts))
    }
}

private extension SeparatorRoomTimelineItem {
    init(timelineID: String) {
        self.init(id: .init(timelineID: timelineID), text: "")
    }
}

private extension TextRoomTimelineItem {
    init(eventID: String) {
        self.init(id: .init(timelineID: UUID().uuidString, eventID: eventID),
                  timestamp: "",
                  isOutgoing: false,
                  isEditable: false,
                  canBeRepliedTo: true,
                  isThreaded: false,
                  sender: .init(id: ""),
                  content: .init(body: "Hello, World!"))
    }
}
