//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//

import MatrixRustSDK
import XCTest

@testable import ElementX

@MainActor
class NotificationSettingsEditScreenViewModelTests: XCTestCase {
    private var viewModel: NotificationSettingsEditScreenViewModelProtocol!
    private var notificationSettingsProxy: NotificationSettingsProxyMock!
    private var userSession: UserSessionProtocol!
    
    private var context: NotificationSettingsEditScreenViewModelType.Context {
        viewModel.context
    }
    
    @MainActor override func setUpWithError() throws {
        let clientProxy = ClientProxyMock(.init(userID: "@a:b.com"))
        userSession = UserSessionMock(.init(clientProxy: clientProxy))
        notificationSettingsProxy = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .allMessages
    }
    
    func testFetchSettings() async throws {
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
            switch (isEncrypted, isOneToOne) {
            case (_, true):
                return .allMessages
            case (_, _):
                return .mentionsAndKeywordsOnly
            }
        }
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)

        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        // `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
        let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
        
        XCTAssertEqual(invocations.count, 2)
        // First call for encrypted group chats
        XCTAssertEqual(invocations[0].isEncrypted, true)
        XCTAssertEqual(invocations[0].isOneToOne, false)
        // Second call for unencrypted group chats
        XCTAssertEqual(invocations[1].isEncrypted, false)
        XCTAssertEqual(invocations[1].isOneToOne, false)
        
        XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
        XCTAssertNil(context.viewState.bindings.alertInfo)
        XCTAssertFalse(context.viewState.canPushEncryptedEvents)
        XCTAssertNotNil(context.viewState.description(for: .mentionsAndKeywordsOnly))
    }
    
    func testFetchSettingsWithCanPushEncryptedEvents() async throws {
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in
            switch (isEncrypted, isOneToOne) {
            case (_, true):
                return .allMessages
            case (_, _):
                return .mentionsAndKeywordsOnly
            }
        }
        notificationSettingsProxy.canPushEncryptedEventsToDeviceClosure = {
            true
        }
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)

        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        // `getDefaultRoomNotificationModeIsEncryptedIsOneToOne` must have been called twice (for encrypted and unencrypted group chats)
        let invocations = notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReceivedInvocations
        
        XCTAssertEqual(invocations.count, 2)
        // First call for encrypted group chats
        XCTAssertEqual(invocations[0].isEncrypted, true)
        XCTAssertEqual(invocations[0].isOneToOne, false)
        // Second call for unencrypted group chats
        XCTAssertEqual(invocations[1].isEncrypted, false)
        XCTAssertEqual(invocations[1].isOneToOne, false)
        
        XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
        XCTAssertNil(context.viewState.bindings.alertInfo)
        XCTAssertTrue(context.viewState.canPushEncryptedEvents)
        XCTAssertNil(context.viewState.description(for: .mentionsAndKeywordsOnly))
    }
    
    func testSetModeAllMessages() async throws {
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)
        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        var deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.pendingMode, transitionValues: [nil, .allMessages, nil])

        context.send(viewAction: .setMode(.allMessages))
        
        try await deferredViewState.fulfill()
        
        // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats)
        let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
        XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
        // First call for encrypted group chats
        XCTAssertEqual(invocations[0].isEncrypted, true)
        XCTAssertEqual(invocations[0].isOneToOne, false)
        XCTAssertEqual(invocations[0].mode, .allMessages)
        // Second call for unencrypted group chats
        XCTAssertEqual(invocations[1].isEncrypted, false)
        XCTAssertEqual(invocations[1].isOneToOne, false)
        XCTAssertEqual(invocations[1].mode, .allMessages)
        
        deferredViewState = deferFulfillment(viewModel.context.$viewState,
                                             keyPath: \.defaultMode,
                                             transitionValues: [.allMessages])
        
        try await deferredViewState.fulfill()

        XCTAssertEqual(context.viewState.defaultMode, .allMessages)
        XCTAssertNil(context.viewState.bindings.alertInfo)
    }

    func testSetModeMentions() async throws {
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)
        
        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        var deferredViewState = deferFulfillment(viewModel.context.$viewState,
                                                 keyPath: \.pendingMode,
                                                 transitionValues: [nil, .mentionsAndKeywordsOnly, nil])
                
        context.send(viewAction: .setMode(.mentionsAndKeywordsOnly))
        
        try await deferredViewState.fulfill()
        
        // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted group chats)
        let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
        XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
        // First call for encrypted group chats
        XCTAssertEqual(invocations[0].isEncrypted, true)
        XCTAssertEqual(invocations[0].isOneToOne, false)
        XCTAssertEqual(invocations[0].mode, .mentionsAndKeywordsOnly)
        // Second call for unencrypted group chats
        XCTAssertEqual(invocations[1].isEncrypted, false)
        XCTAssertEqual(invocations[1].isOneToOne, false)
        XCTAssertEqual(invocations[1].mode, .mentionsAndKeywordsOnly)
        
        deferredViewState = deferFulfillment(viewModel.context.$viewState,
                                             keyPath: \.defaultMode,
                                             transitionValues: [.mentionsAndKeywordsOnly])
        
        try await deferredViewState.fulfill()

        XCTAssertEqual(context.viewState.defaultMode, .mentionsAndKeywordsOnly)
        XCTAssertNil(context.viewState.bindings.alertInfo)
    }

    func testSetModeDirectChats() async throws {
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
        // Initialize for direct chats
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)
        
        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        let deferredViewState = deferFulfillment(viewModel.context.$viewState,
                                                 keyPath: \.pendingMode,
                                                 transitionValues: [nil, .allMessages, nil])
        
        context.send(viewAction: .setMode(.allMessages))
        
        try await deferredViewState.fulfill()

        // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats)
        let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations
        XCTAssertEqual(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount, 2)
        // First call for encrypted direct chats
        XCTAssertEqual(invocations[0].isEncrypted, true)
        XCTAssertEqual(invocations[0].isOneToOne, true)
        XCTAssertEqual(invocations[0].mode, .allMessages)
        // Second call for unencrypted direct chats
        XCTAssertEqual(invocations[1].isEncrypted, false)
        XCTAssertEqual(invocations[1].isOneToOne, true)
        XCTAssertEqual(invocations[1].mode, .allMessages)
    }

    func testSetModeFailure() async throws {
        notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly
        notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeThrowableError = NotificationSettingsError.Generic(msg: "error")
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)
        
        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.defaultMode != nil
        }
        
        viewModel.fetchInitialContent()
        
        try await deferred.fulfill()
        
        let deferredViewState = deferFulfillment(viewModel.context.$viewState,
                                                 keyPath: \.pendingMode,
                                                 transitionValues: [nil, .allMessages, nil])

        context.send(viewAction: .setMode(.allMessages))
        
        try await deferredViewState.fulfill()
        
        XCTAssertNotNil(context.viewState.bindings.alertInfo)
    }

    func testSelectRoom() async throws {
        let roomID = "!roomidentifier:matrix.org"
        viewModel = NotificationSettingsEditScreenViewModel(chatType: .oneToOneChat,
                                                            userSession: userSession,
                                                            notificationSettingsProxy: notificationSettingsProxy)
        
        let deferredActions = deferFulfillment(viewModel.actions) { action in
            switch action {
            case .requestRoomNotificationSettingsPresentation:
                return true
            }
        }
        
        context.send(viewAction: .selectRoom(roomIdentifier: roomID))
        
        let sentAction = try await deferredActions.fulfill()
        
        let expectedAction = NotificationSettingsEditScreenViewModelAction.requestRoomNotificationSettingsPresentation(roomID: roomID)
        guard case let .requestRoomNotificationSettingsPresentation(roomID: receivedRoomID) = sentAction, receivedRoomID == roomID else {
            XCTFail("Expected action \(expectedAction), but was \(sentAction)")
            return
        }
    }
}
