//
// Copyright 2025 Element Creations Ltd.
// Copyright 2023-2025 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
@testable import ElementX
import Testing

@MainActor
struct CompletionSuggestionServiceTests {
    @Test
    func userSuggestions() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: "ali")]
        }
        service.setSuggestionTrigger(.init(type: .user, text: "ali", range: .init()))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        service.setSuggestionTrigger(.init(type: .user, text: "me", range: .init()))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        service.setSuggestionTrigger(.init(type: .user, text: "room", range: .init()))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        service.setSuggestionTrigger(.init(type: .user, text: "everyon", range: .init()))
        try await deferred.fulfill()
    }
    
    @Test
    func userSuggestionsIncludingAllUsers() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID",
                                                      name: "test",
                                                      members: members,
                                                      powerLevelsConfiguration: .init(canUserTriggerRoomNotification: true)))
        
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "ro")]
        }
        service.setSuggestionTrigger(.init(type: .user, text: "ro", range: .init()))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: "every")]
        }
        service.setSuggestionTrigger(.init(type: .user, text: "every", range: .init()))
        try await deferred.fulfill()
    }
    
    @Test
    func userSuggestionsWithEmptyText() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        let bob: RoomMemberProxyMock = .mockBob
        let members: [RoomMemberProxyMock] = [alice, bob, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID",
                                                      name: "test",
                                                      members: members,
                                                      powerLevelsConfiguration: .init(canUserTriggerRoomNotification: true)))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(), rawSuggestionText: ""),
                            .init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(), rawSuggestionText: ""),
                            .init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(), rawSuggestionText: "")]
        }
        service.setSuggestionTrigger(.init(type: .user, text: "", range: .init()))
        try await deferred.fulfill()
        
        // Let's test the same with the processTextMessage method
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .allUsers(.room(id: "roomID", name: "test", avatarURL: nil)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
                            .init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: ""),
                            .init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 0, length: 1), rawSuggestionText: "")]
        }
        service.processTextMessage("@", selectedRange: .init(location: 0, length: 1))
        try await deferred.fulfill()
    }
    
    @Test
    func userSuggestionInDifferentMessagePositions() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
        }
        service.processTextMessage("@al hello", selectedRange: .init(location: 0, length: 1))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
        }
        service.processTextMessage("test @al", selectedRange: .init(location: 5, length: 1))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 5, length: 3), rawSuggestionText: "al")]
        }
        service.processTextMessage("test @al test", selectedRange: .init(location: 5, length: 1))
        try await deferred.fulfill()
    }
    
    @Test
    func userSuggestionWithMultipleMentionSymbol() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        let bob: RoomMemberProxyMock = .mockBob
        let members: [RoomMemberProxyMock] = [alice, bob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 0, length: 3), rawSuggestionText: "al")]
        }
        service.processTextMessage("@al test @bo", selectedRange: .init(location: 0, length: 1))
        try await deffered.fulfill()
        
        deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: bob.userID, displayName: bob.displayName, avatarURL: bob.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "bo")]
        }
        service.processTextMessage("@al test @bo", selectedRange: .init(location: 9, length: 1))
        try await deffered.fulfill()
        
        deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
            suggestion == []
        }
        service.processTextMessage("@al test @bo", selectedRange: .init(location: 4, length: 1))
        try await deffered.fulfill()
    }
    
    @Test
    func roomSuggestions() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        // We keep the users in the tests since they should not appear in the suggestions when using the room trigger
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        
        try await deferred.fulfill()
        
        // The empty # should trigger suggestions from any room with an alias
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "2",
                                                              canonicalAlias: "#foundation-and-empire:matrix.org",
                                                              name: "Foundation and Empire",
                                                              avatar: .room(id: "2",
                                                                            name: "Foundation and Empire",
                                                                            avatarURL: .mockMXCAvatar))),
                                  range: .init(),
                                  rawSuggestionText: ""),
                            .init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(),
                                  rawSuggestionText: "")]
        }
        service.setSuggestionTrigger(.init(type: .room, text: "", range: .init()))
        try await deferred.fulfill()
        
        // Same but with the processTextMessage method
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "2",
                                                              canonicalAlias: "#foundation-and-empire:matrix.org",
                                                              name: "Foundation and Empire",
                                                              avatar: .room(id: "2",
                                                                            name: "Foundation and Empire",
                                                                            avatarURL: .mockMXCAvatar))),
                                  range: .init(location: 0, length: 1),
                                  rawSuggestionText: ""),
                            .init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 0, length: 1),
                                  rawSuggestionText: "")]
        }
        service.processTextMessage("#", selectedRange: .init(location: 0, length: 1))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(),
                                  rawSuggestionText: "prelude")]
        }
        service.setSuggestionTrigger(.init(type: .room, text: "prelude", range: .init()))
        try await deferred.fulfill()
    }
    
    @Test
    func roomSuggestionInDifferentMessagePositions() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        // We keep the users in the tests since they should not appear in the suggestions when using the room trigger
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 0, length: 3),
                                  rawSuggestionText: "pr")]
        }
        service.processTextMessage("#pr hello", selectedRange: .init(location: 0, length: 1))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 5, length: 3),
                                  rawSuggestionText: "pr")]
        }
        service.processTextMessage("test #pr", selectedRange: .init(location: 5, length: 1))
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 5, length: 3),
                                  rawSuggestionText: "pr")]
        }
        service.processTextMessage("test #pr test", selectedRange: .init(location: 5, length: 1))
        try await deferred.fulfill()
    }
    
    @Test
    func roomSuggestionWithMultipleMentionSymbol() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        // We keep the users in the tests since they should not appear in the suggestions when using the room trigger
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 0, length: 3),
                                  rawSuggestionText: "pr")]
        }
        service.processTextMessage("#pr test #fo", selectedRange: .init(location: 0, length: 1))
        try await deffered.fulfill()
        
        deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "2",
                                                              canonicalAlias: "#foundation-and-empire:matrix.org",
                                                              name: "Foundation and Empire",
                                                              avatar: .room(id: "2",
                                                                            name: "Foundation and Empire",
                                                                            avatarURL: .mockMXCAvatar))),
                                  range: .init(location: 9, length: 3),
                                  rawSuggestionText: "fo"),
                            .init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 9, length: 3),
                                  rawSuggestionText: "fo")]
        }
        service.processTextMessage("#pr test #fo", selectedRange: .init(location: 9, length: 1))
        try await deffered.fulfill()
        
        deffered = deferFulfillment(service.suggestionsPublisher) { suggestion in
            suggestion == []
        }
        service.processTextMessage("#pr test #fo", selectedRange: .init(location: 4, length: 1))
        try await deffered.fulfill()
    }
    
    @Test
    func suggestionsWithMultipleDifferentTriggers() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        // We keep the users in the tests since they should not appear in the suggestions when using the room trigger
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(location: 0, length: 3),
                                  rawSuggestionText: "pr")]
        }
        service.processTextMessage("#pr test @al", selectedRange: .init(location: 0, length: 1))
        try await deffered.fulfill()
        
        deffered = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .user(.init(id: alice.userID, displayName: alice.displayName, avatarURL: alice.avatarURL)), range: .init(location: 9, length: 3), rawSuggestionText: "al")]
        }
        service.processTextMessage("#pr test @al", selectedRange: .init(location: 9, length: 1))
        try await deffered.fulfill()
    }
    
    @Test
    func suggestionsContainingNonAlphanumericCharacters() async throws {
        let alice: RoomMemberProxyMock = .mockAlice
        // We keep the users in the tests since they should not appear in the suggestions when using the room trigger
        let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe]
        let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
        let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
        let service = CompletionSuggestionService(roomProxy: roomProxyMock,
                                                  roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
        
        var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == []
        }
        
        try await deferred.fulfill()
        
        deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in
            suggestions == [.init(suggestionType: .room(.init(id: "6",
                                                              canonicalAlias: "#prelude-foundation:matrix.org",
                                                              name: "Prelude to Foundation",
                                                              avatar: .room(id: "6",
                                                                            name: "Prelude to Foundation",
                                                                            avatarURL: nil))),
                                  range: .init(),
                                  rawSuggestionText: "#prelude-")]
        }
        service.setSuggestionTrigger(.init(type: .room, text: "#prelude-", range: .init()))
        try await deferred.fulfill()
    }
}
