//
// 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 Combine
import MatrixRustSDK
import SwiftUI
import XCTest

@testable import ElementX

@MainActor
class RoomDetailsScreenViewModelTests: XCTestCase {
    var viewModel: RoomDetailsScreenViewModel!
    var roomProxyMock: JoinedRoomProxyMock!
    var notificationSettingsProxyMock: NotificationSettingsProxyMock!
    var context: RoomDetailsScreenViewModelType.Context { viewModel.context }
    var cancellables = Set<AnyCancellable>()
    
    override func setUp() {
        AppSettings.resetAllSettings()
        cancellables.removeAll()
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
        notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
    }
    
    func testLeaveRoomTappedWhenPublic() async throws {
        let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        let deferred = deferFulfillment(context.$viewState) { state in
            state.bindings.leaveRoomAlertItem != nil
        }
        
        context.send(viewAction: .processTapLeave)
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .public)
        XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertSubtitle)
    }
    
    func testLeaveRoomTappedWhenRoomNotPublic() async throws {
        let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: false, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        let deferred = deferFulfillment(context.$viewState) { state in
            state.bindings.leaveRoomAlertItem != nil
        }
        
        context.send(viewAction: .processTapLeave)
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.state, .private)
        XCTAssertEqual(context.viewState.bindings.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertPrivateSubtitle)
    }
    
    func testLeaveRoomTappedWithLessThanTwoMembers() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: false, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        context.send(viewAction: .processTapLeave)
        XCTAssertEqual(context.leaveRoomAlertItem?.state, .empty)
        XCTAssertEqual(context.leaveRoomAlertItem?.subtitle, L10n.leaveRoomAlertEmptySubtitle)
    }
    
    func testLeaveRoomSuccess() async throws {
        roomProxyMock.leaveRoomClosure = {
            .success(())
        }
        
        let deferred = deferFulfillment(viewModel.actions) { action in
            switch action {
            case .leftRoom:
                return true
            default:
                return false
            }
        }
        
        context.send(viewAction: .confirmLeave)
        
        try await deferred.fulfill()
        
        XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
    }
    
    func testLeaveRoomError() async {
        let expectation = expectation(description: #function)
        roomProxyMock.leaveRoomClosure = {
            defer {
                expectation.fulfill()
            }
            return .failure(.sdkError(ClientProxyMockError.generic))
        }
        context.send(viewAction: .confirmLeave)
        await fulfillment(of: [expectation])
        XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
        XCTAssertNotNil(context.alertInfo)
    }
    
    func testInitialDMDetailsState() async throws {
        let recipient = RoomMemberProxyMock.mockDan
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        let deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.dmRecipient != nil
        }
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
    }
    
    func testIgnoreSuccess() async throws {
        let recipient = RoomMemberProxyMock.mockDan
        
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        var deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.dmRecipient != nil
        }
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
        
        deferred = deferFulfillment(viewModel.context.$viewState,
                                    keyPath: \.isProcessingIgnoreRequest,
                                    transitionValues: [false, true, false])
        
        context.send(viewAction: .ignoreConfirmed)
        
        try await deferred.fulfill()
        
        XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
    }
    
    func testIgnoreFailure() async throws {
        let recipient = RoomMemberProxyMock.mockDan
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
        let clientProxy = ClientProxyMock(.init())
        clientProxy.ignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: clientProxy,
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        var deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.dmRecipient != nil
        }
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
        
        deferred = deferFulfillment(viewModel.context.$viewState,
                                    keyPath: \.isProcessingIgnoreRequest,
                                    transitionValues: [false, true, false])
        
        context.send(viewAction: .ignoreConfirmed)
        
        try await deferred.fulfill()
        
        XCTAssert(context.viewState.dmRecipient?.isIgnored == false)
        XCTAssertNotNil(context.alertInfo)
    }
    
    func testUnignoreSuccess() async throws {
        let recipient = RoomMemberProxyMock.mockIgnored
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        var deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.dmRecipient != nil
        }
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
        
        deferred = deferFulfillment(viewModel.context.$viewState,
                                    keyPath: \.isProcessingIgnoreRequest,
                                    transitionValues: [false, true, false])
        
        context.send(viewAction: .unignoreConfirmed)
                
        try await deferred.fulfill()
        
        XCTAssert(context.viewState.dmRecipient?.isIgnored == false)
    }
    
    func testUnignoreFailure() async throws {
        let recipient = RoomMemberProxyMock.mockIgnored
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
        let clientProxy = ClientProxyMock(.init())
        clientProxy.unignoreUserReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: clientProxy,
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        var deferred = deferFulfillment(viewModel.context.$viewState) { state in
            state.dmRecipient != nil
        }
        
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
        
        deferred = deferFulfillment(viewModel.context.$viewState,
                                    keyPath: \.isProcessingIgnoreRequest,
                                    transitionValues: [false, true, false])
        
        context.send(viewAction: .unignoreConfirmed)
                
        try await deferred.fulfill()
        
        XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
        XCTAssertNotNil(context.alertInfo)
    }
    
    func testCannotInvitePeople() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test",
                                                  isPublic: true,
                                                  members: mockedMembers,
                                                  canUserInvite: false))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertFalse(context.viewState.canInviteUsers)
    }
    
    func testInvitePeople() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isPublic: true, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertTrue(context.viewState.canInviteUsers)
        
        var callbackCorrectlyCalled = false
        viewModel.actions
            .sink { action in
                switch action {
                case .requestInvitePeoplePresentation:
                    callbackCorrectlyCalled = true
                default:
                    callbackCorrectlyCalled = false
                }
            }
            .store(in: &cancellables)
        
        context.send(viewAction: .processTapInvite)
        await Task.yield()
        XCTAssertTrue(callbackCorrectlyCalled)
    }
    
    func testCanEditAvatar() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers))
        roomProxyMock.canUserUserIDSendStateEventClosure = { _, event in
            .success(event == .roomAvatar)
        }
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertTrue(context.viewState.canEditRoomAvatar)
        XCTAssertFalse(context.viewState.canEditRoomName)
        XCTAssertFalse(context.viewState.canEditRoomTopic)
        XCTAssertTrue(context.viewState.canEdit)
    }
    
    func testCanEditName() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers))
        roomProxyMock.canUserUserIDSendStateEventClosure = { _, event in
            .success(event == .roomName)
        }
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertFalse(context.viewState.canEditRoomAvatar)
        XCTAssertTrue(context.viewState.canEditRoomName)
        XCTAssertFalse(context.viewState.canEditRoomTopic)
        XCTAssertTrue(context.viewState.canEdit)
    }
    
    func testCanEditTopic() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers))
        roomProxyMock.canUserUserIDSendStateEventClosure = { _, event in
            .success(event == .roomTopic)
        }
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertFalse(context.viewState.canEditRoomAvatar)
        XCTAssertFalse(context.viewState.canEditRoomName)
        XCTAssertTrue(context.viewState.canEditRoomTopic)
        XCTAssertTrue(context.viewState.canEdit)
    }
    
    func testCannotEditRoom() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertFalse(context.viewState.canEditRoomAvatar)
        XCTAssertFalse(context.viewState.canEditRoomName)
        XCTAssertFalse(context.viewState.canEditRoomTopic)
        XCTAssertFalse(context.viewState.canEdit)
    }
    
    func testCannotEditDirectRoom() async {
        let mockedMembers: [RoomMemberProxyMock] = [.mockMeAdmin, .mockBob, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isPublic: false, members: mockedMembers))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertFalse(context.viewState.canEdit)
    }
    
    // MARK: - Notifications
    
    func testNotificationLoadingSettingsFailure() async throws {
        notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneThrowableError = NotificationSettingsError.Generic(msg: "error")
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        var deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isError
        }
        
        try await deferred.fulfill()
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        
        deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isError
        }
        
        try await deferred.fulfill()
        
        let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
                                          title: L10n.commonError,
                                          message: L10n.screenRoomDetailsErrorLoadingNotificationSettings)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
    }
    
    func testNotificationDefaultMode() async throws {
        notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: true))
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isLoaded
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.notificationSettingsState.label, "Default")
    }
    
    func testNotificationCustomMode() async throws {
        notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false))
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isCustom
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.notificationSettingsState.label, "Custom")
    }
    
    func testNotificationRoomMuted() async throws {
        notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mute, isDefault: false))
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isLoaded
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
        
        XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute)
        XCTAssertEqual(context.viewState.notificationShortcutButtonIcon, \.notificationsOff)
    }
    
    func testNotificationRoomNotMuted() async throws {
        notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false))
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.notificationSettingsState.isLoaded
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonMute)
        XCTAssertEqual(context.viewState.notificationShortcutButtonIcon, \.notifications)
    }
    
    func testUnmuteTappedFailure() async throws {
        try await testNotificationRoomMuted()
        
        let expectation = expectation(description: #function)
        notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { _, _, _ in
            defer {
                expectation.fulfill()
            }
            throw NotificationSettingsError.Generic(msg: "unmute error")
        }
        context.send(viewAction: .processToggleMuteNotifications)
        await fulfillment(of: [expectation])
        
        XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
        XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute)
        
        let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
                                          title: L10n.commonError,
                                          message: L10n.screenRoomDetailsErrorUnmuting)
        
        XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
    }
    
    func testMuteTappedFailure() async throws {
        try await testNotificationRoomNotMuted()
        
        let expectation = expectation(description: #function)
        notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { _, _ in
            defer {
                expectation.fulfill()
            }
            throw NotificationSettingsError.Generic(msg: "mute error")
        }
        context.send(viewAction: .processToggleMuteNotifications)
        await fulfillment(of: [expectation])
        
        XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
        XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonMute)
        
        let expectedAlertInfo = AlertInfo(id: RoomDetailsScreenErrorType.alert,
                                          title: L10n.commonError,
                                          message: L10n.screenRoomDetailsErrorMuting)
        
        XCTAssertEqual(context.viewState.bindings.alertInfo?.id, expectedAlertInfo.id)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.title, expectedAlertInfo.title)
        XCTAssertEqual(context.viewState.bindings.alertInfo?.message, expectedAlertInfo.message)
    }
    
    func testMuteTapped() async throws {
        try await testNotificationRoomNotMuted()
        
        let expectation = expectation(description: #function)
        notificationSettingsProxyMock.setNotificationModeRoomIdModeClosure = { [weak notificationSettingsProxyMock] _, mode in
            notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: mode, isDefault: false))
            expectation.fulfill()
        }
        context.send(viewAction: .processToggleMuteNotifications)
        await fulfillment(of: [expectation])
        
        XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            switch state.notificationSettingsState {
            case .loaded(settings: let settings):
                return settings.mode == .mute
            default:
                return false
            }
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        if case .loaded(let newNotificationSettingsState) = viewModel.state.notificationSettingsState {
            XCTAssertFalse(newNotificationSettingsState.isDefault)
            XCTAssertEqual(newNotificationSettingsState.mode, .mute)
        } else {
            XCTFail("invalid state")
        }
    }
    
    func testUnmuteTapped() async throws {
        try await testNotificationRoomMuted()
        
        let expectation = expectation(description: #function)
        notificationSettingsProxyMock.unmuteRoomRoomIdIsEncryptedIsOneToOneClosure = { [weak notificationSettingsProxyMock] _, _, _ in
            notificationSettingsProxyMock?.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false))
            expectation.fulfill()
        }
        context.send(viewAction: .processToggleMuteNotifications)
        await fulfillment(of: [expectation])
        
        XCTAssertFalse(context.viewState.isProcessingMuteToggleAction)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            switch state.notificationSettingsState {
            case .loaded(settings: let settings):
                return settings.mode == .allMessages
            default:
                return false
            }
        }
        
        notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
        try await deferred.fulfill()
        
        if case .loaded(let newNotificationSettingsState) = viewModel.state.notificationSettingsState {
            XCTAssertFalse(newNotificationSettingsState.isDefault)
            XCTAssertEqual(newNotificationSettingsState.mode, .allMessages)
        } else {
            XCTFail("invalid state")
        }
    }
    
    // MARK: - Knock Requests
    
    func testKnockRequestsCounter() async throws {
        ServiceLocator.shared.settings.knockingEnabled = true
        let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.knockRequestsCount == 2 && state.canSeeKnockingRequests
        }
        try await deferred.fulfill()
        
        let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockingRequests }
        context.send(viewAction: .processTapRequestsToJoin)
        try await deferredAction.fulfill()
    }
    
    func testKnockRequestsCounterIsLoading() async throws {
        ServiceLocator.shared.settings.knockingEnabled = true
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loading, joinRule: .knock))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.knockRequestsCount == 0 && state.canSeeKnockingRequests
        }
        
        try await deferred.fulfill()
    }
    
    func testKnockRequestsCounterIsNotShownIfNoPermissions() async throws {
        ServiceLocator.shared.settings.knockingEnabled = true
        let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), canUserInvite: false, joinRule: .knock))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.knockRequestsCount == 2 &&
                state.dmRecipient == nil &&
                !state.canSeeKnockingRequests &&
                !state.canInviteUsers
        }
        
        try await deferred.fulfill()
    }
    
    func testKnockRequestsCounterIsNotShownIfDM() async throws {
        ServiceLocator.shared.settings.knockingEnabled = true
        let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
        let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
        roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isPublic: false, members: mockedMembers, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
        viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
                                               clientProxy: ClientProxyMock(.init()),
                                               mediaProvider: MediaProviderMock(configuration: .init()),
                                               analyticsService: ServiceLocator.shared.analytics,
                                               userIndicatorController: ServiceLocator.shared.userIndicatorController,
                                               notificationSettingsProxy: notificationSettingsProxyMock,
                                               attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
                                               appMediator: AppMediatorMock.default,
                                               appSettings: ServiceLocator.shared.settings)
        
        let deferred = deferFulfillment(context.$viewState) { state in
            state.knockRequestsCount == 2 &&
                !state.canSeeKnockingRequests &&
                state.dmRecipient != nil &&
                state.canInviteUsers
        }
        
        try await deferred.fulfill()
    }
}
