import Quick
import Nimble
import XCTest
import Foundation

@testable import Beam
@testable import BeamCore

class ClusteringManagerTests: XCTestCase, BeamDocumentSource {
    static var sourceId: String { "\(Self.self)" }

    var sessionLinkRanker: SessionLinkRanker!
    var tabGroupingManager: TabGroupingManager!
    var clusteringManager: ClusteringManager!

    var tabIDs: [UUID] = []
    var pageIDs: [UUID] = []
    var documents: [IndexDocument]!
    var informations: [TabIndexingInfo]!
    var notes: [BeamNote]!

    let objectManager = BeamData.shared.objectManager

    override func setUp() {
        sessionLinkRanker = SessionLinkRanker()
        tabGroupingManager = TabGroupingManager()
        clusteringManager = ClusteringManager(ranker: sessionLinkRanker, candidate: 2, navigation: 0.5, text: 0.9, entities: 0.4,
                                              sessionId: UUID(),
                                              tabGroupingManager: tabGroupingManager,
                                              objectManager: objectManager,
                                              forcedClusteringType: .legacy)

        for _ in 0...3 {
            pageIDs.append(UUID())
            tabIDs.append(UUID())
        }
        documents = [
            IndexDocument(id: self.pageIDs[0], title: "Roger Federer"),
            IndexDocument(id: self.pageIDs[1], title: "Rafael Nadal"),
            IndexDocument(id: self.pageIDs[2], title: "Novak Djokovic"),
            IndexDocument(id: self.pageIDs[3], title: "Richard Gasquet")
        ]
        informations = [
            TabIndexingInfo(url: URL(string: "http://www.rogerfederer.com")!, tabId: tabIDs[0], document: documents[0], textContent: "Roger Federer is the best tennis player ever", cleanedTextContentForClustering: ["Roger Federer is the best tennis player ever"]),
            TabIndexingInfo(url: URL(string: "https://rafaelnadal.com/en/")!, tabId: tabIDs[1], document: documents[1], textContent: "Rafael Nadal is also pretty good", cleanedTextContentForClustering: ["Rafael Nadal is also pretty good"]),
            TabIndexingInfo(url: URL(string: "https://novakdjokovic.com/en/")!, tabId: tabIDs[2], document: documents[2], textContent: "Not you", cleanedTextContentForClustering: ["Not you"]),
            TabIndexingInfo(url: URL(string: "http://www.richardgasquet.net")!, tabId: tabIDs[3], document: documents[3], textContent: "Richard Gasquet has a wonderful one-handed backhand", cleanedTextContentForClustering: ["Richard Gasquet has a wonderful one-handed backhand"])
        ]
        notes = [
            try! BeamNote(title: "Tennis"),
            try! BeamNote(title: "Paris"),
            try! BeamNote(title: "Machine Learning"),
            try! BeamNote(title: "Short note")
        ]
        for note in notes {
            note.owner = BeamData.shared.currentDatabase
        }
        notes[0].children = [BeamElement("The official site of Roger Federer is www.rogerfederer.com"), BeamElement("His Wikipedia page is https://en.wikipedia.org/wiki/Roger_Federer"), BeamElement("I wonder if his email is rogerfederer@gmail.com"), BeamElement("Federer has played in an era where he dominated men's tennis together with Rafael Nadal and Novak Djokovic, who have been collectively referred to as the Big Three and are widely considered three of the greatest tennis players of all-time.[c] A Wimbledon junior champion in 1998, Federer won his first Grand Slam singles title at Wimbledon in 2003 at age 21. In 2004, he won three out of the four major singles titles and the ATP Finals,[d] a feat he repeated in 2006 and 2007. From 2005 to 2010, Federer made 18 out of 19 major singles finals. During this span, he won his fifth consecutive titles at both Wimbledon and the US Open. He completed the career Grand Slam at the 2009 French Open after three previous runner-ups to Nadal, his main rival up until 2010. At age 27, he also surpassed Pete Sampras's then-record of 14 Grand Slam men's singles titles at Wimbledon in 2009.")]
        notes[1].children = [BeamElement("Paris is a major railway, highway, and air-transport hub served by two international airports: Paris–Charles de Gaulle (the second-busiest airport in Europe) and Paris–Orly.[10][11] Opened in 1900, the city's subway system, the Paris Métro, serves 5.23 million passengers daily;[12] it is the second-busiest metro system in Europe after the Moscow Metro. Gare du Nord is the 24th-busiest railway station in the world, but the busiest located outside Japan, with 262 million passengers in 2015.[13] Paris is especially known for its museums and architectural landmarks: the Louvre remained the most-visited museum in the world with 2,677,504 visitors in 2020, despite the long museum closings caused by the COVID-19 virus.[14] The Musée d'Orsay, Musée Marmottan Monet and Musée de l'Orangerie are noted for their collections of French Impressionist art. The Pompidou Centre Musée National d'Art Moderne has the largest collection of modern and contemporary art in Europe. The Musée Rodin and Musée Picasso exhibit the works of two noted Parisians. The historical district along the Seine in the city centre is classified as a UNESCO World Heritage Site; popular landmarks there include the Cathedral of Notre Dame de Paris on the Île de la Cité, now closed for renovation after the 15 April 2019 fire. Other popular tourist sites include the Gothic royal chapel of Sainte-Chapelle, also on the Île de la Cité; the Eiffel Tower, constructed for the Paris Universal Exposition of 1889; the Grand Palais and Petit Palais, built for the Paris Universal Exposition of 1900; the Arc de Triomphe on the Champs-Élysées, and the hill of Montmartre with its artistic history and its Basilica of Sacré-Coeur.")]
        notes[2].children = [BeamElement("Machine learning (ML) is the study of computer algorithms that can improve automatically through experience and by the use of data.[1] It is seen as a part of artificial intelligence. Machine learning algorithms build a model based on sample data, known as 'training data', in order to make predictions or decisions without being explicitly programmed to do so.[2] Machine learning algorithms are used in a wide variety of applications, such as in medicine, email filtering, speech recognition, and computer vision, where it is difficult or unfeasible to develop conventional algorithms to perform the needed tasks. A subset of machine learning is closely related to computational statistics, which focuses on making predictions using computers; but not all machine learning is statistical learning. The study of mathematical optimization delivers methods, theory and application domains to the field of machine learning. Data mining is a related field of study, focusing on exploratory data analysis through unsupervised learning.[5][6] Some implementations of machine learning use data and neural networks in a way that mimics the working of a biological brain.[7][8] In its application across business problems, machine learning is also referred to as predictive analytics.")]
        notes[3].children = [BeamElement("This is just a short note")]
    }

    /// Test that adding pages and then notes works correctly
    func testAddPagesThenNotes() throws {
        clusteringManager.addPage(id: documents[0].id, tabId: tabIDs[0], parentId: nil, value: informations[0])
        expect(self.clusteringManager.clusteredPagesId).toEventually(equal([[self.pageIDs[0]]]))

        clusteringManager.addPage(id: documents[1].id, tabId: tabIDs[1], parentId: nil, value: informations[1])
        expect(self.clusteringManager.clusteredPagesId).toEventually(equal([[self.pageIDs[0],self.pageIDs[1]]]) || contain([self.pageIDs[1]]))

        clusteringManager.addNote(note: notes[0])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[0].id]))

        clusteringManager.addNote(note: notes[1])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[1].id]) || contain([notes[0].id, notes[1].id]))
        expect(self.clusteringManager.clusteredPagesId).toEventually(contain([self.pageIDs[0],self.pageIDs[1]]) || contain([self.pageIDs[1]]))

        clusteringManager.addPage(id: documents[2].id, tabId: tabIDs[2], parentId: nil, value: informations[2])
        expect(self.clusteringManager.clusteredPagesId).toEventually(contain([self.pageIDs[0], self.pageIDs[1], self.pageIDs[2]]) || contain([self.pageIDs[2]]) || contain([self.pageIDs[0], self.pageIDs[2]]) || contain([self.pageIDs[1], self.pageIDs[2]]))
    }

    /// Test that adding notes and then pages works correctly. Includes a short note that is not to be added
    func testAddNotesThenPages() throws {
        clusteringManager.addNote(note: notes[0])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[0].id]))

        clusteringManager.addNote(note: notes[1])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[1].id]) || contain([notes[0].id, notes[1].id]))

        // A short note is to be ignored
        clusteringManager.addNote(note: notes[3])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[1].id]) || contain([notes[0].id, notes[1].id]))

        clusteringManager.addPage(id: documents[0].id, tabId: tabIDs[0], parentId: nil, value: informations[0])
        expect(self.clusteringManager.clusteredPagesId).toEventually(contain([self.pageIDs[0]]))

        clusteringManager.addPage(id: documents[1].id, tabId: tabIDs[1], parentId: nil, value: informations[1])
        expect(self.clusteringManager.clusteredPagesId).toEventually(contain([self.pageIDs[0],self.pageIDs[1]]) || contain([self.pageIDs[1]]))

        clusteringManager.addNote(note: notes[2])
        expect(self.clusteringManager.clusteredNotesId).toEventually(contain([notes[0].id, notes[1].id, notes[2].id]) || contain([notes[1].id, notes[2].id]) || contain([notes[2].id]))
    }

    private func swapInfo(at index: Int, tabTree: BrowsingTree, parentBrowsingNode: BrowsingNode?, currentTabTree: BrowsingTree?) {
        let oldInfo = informations[index]

        let newInfo = TabIndexingInfo(url: oldInfo.url, tabId: oldInfo.tabId,
                                      requestedURL: oldInfo.requestedURL,
                                      shouldBeIndexed: oldInfo.shouldBeIndexed,
                                      tabTree: tabTree,
                                      currentTabTree: currentTabTree ?? oldInfo.currentTabTree,
                                      previousTabTree: oldInfo.previousTabTree,
                                      document: oldInfo.document,
                                      textContent: oldInfo.textContent,
                                      cleanedTextContentForClustering: oldInfo.cleanedTextContentForClustering,
                                      isPinnedTab: oldInfo.isPinnedTab)

        informations.remove(at: index)
        informations.insert(newInfo, at: index)
    }

    /// Test that the clusteringManager knows how to extract id-s and parenting relations correctly
    func testGetIdAndParent() throws {
        // Start a new browsing tree
        let tree = BrowsingTree(nil)
        var nodes = [tree.current]

        // Navigate to first page, no parent
        tree.navigateTo(url: informations[0].url.string, title: nil, startReading: false, isLinkActivation: false)
        nodes.append(tree.current)
        swapInfo(at: 0, tabTree: tree, parentBrowsingNode: nodes[0], currentTabTree: nil)
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[0]).0) == nodes[1]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[0]).1).to(beNil())

        // Navigate to second page from first page
        tree.navigateTo(url: informations[1].url.string, title: nil, startReading: false, isLinkActivation: true)
        nodes.append(tree.current)
        swapInfo(at: 1, tabTree: tree, parentBrowsingNode: nodes[1], currentTabTree: nil)
        swapInfo(at: 1, tabTree: tree, parentBrowsingNode: nodes[1], currentTabTree: nil)
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[1]).0) == nodes[2]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[1]).1) == nodes[1]?.link

        // Navigate to third page in a new tab
        tree.openLinkInNewTab()
        let newTabTree = BrowsingTree(nil)
        newTabTree.navigateTo(url: informations[2].url.string, title: nil, startReading: false, isLinkActivation: true)
        nodes.append(newTabTree.current)
        swapInfo(at: 2, tabTree: newTabTree, parentBrowsingNode: nil, currentTabTree: tree)

        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[2]).0) == nodes[3]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[2]).1) == nodes[2]?.link

        // Go back on the original tree, then open a new page in new tab
        tree.goBack()
        tree.openLinkInNewTab()
        let anotherNewTabTree = BrowsingTree(nil)
        anotherNewTabTree.navigateTo(url: informations[3].url.string, title: nil, startReading: false, isLinkActivation: true)
        nodes.append(anotherNewTabTree.current)
        swapInfo(at: 3, tabTree: anotherNewTabTree, parentBrowsingNode: nil, currentTabTree: tree)

        _ = self.clusteringManager.getIdAndParent(tabToIndex: self.informations[3])
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[3]).0) == nodes[4]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[3]).1) == nodes[1]?.link
    }


    func testGetIdAndParentLinkFromNote() throws {
        // Start a new browsing tree and navigate to a page through a link in a note
        let _ = try BeamNote.fetchOrCreate(self, title: notes[0].title)
        let tree = BrowsingTree(.linkFromNote(noteName: notes[0].title))
        var nodes = [tree.current]
        tree.navigateTo(url: informations[0].url.string, title: documents[0].title, startReading: false, isLinkActivation: false)
        nodes.append(tree.current)
        swapInfo(at: 0, tabTree: tree, parentBrowsingNode: nodes[0], currentTabTree: nil)
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[0]).0) == nodes[1]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[0]).1).to(beNil())
        
        // Navigate to second page from first page
        tree.navigateTo(url: informations[1].url.string, title: nil, startReading: false, isLinkActivation: true)
        nodes.append(tree.current)
        swapInfo(at: 1, tabTree: tree, parentBrowsingNode: nodes[1], currentTabTree: nil)
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[1]).0) == nodes[2]?.link
        expect(self.clusteringManager.getIdAndParent(tabToIndex: self.informations[1]).1) == nodes[1]?.link
    }

    /// Test that URLs that are not suggested for any note are extracted correctly for the pourposes of monitoring
    func testOrphanedUrls() throws {
        let noteId = UUID()
        let noteGroups = [[noteId], [], []]
        let urlGroups: [[UUID]] = [
            [self.pageIDs[0]], //this page is in the same cluster as noteId note
            [self.pageIDs[3]]] //this cluster can't be linked to any note
        expect(self.clusteringManager.getOrphanedUrlGroups(urlGroups: urlGroups, noteGroups: noteGroups)) == [[self.pageIDs[3]]]
    }

    /// Test that text cleaning for notes is done correctly
    func testNoteTextCleaning() throws {
        let fullText = self.clusteringManager.cleanTextFrom(note: notes[0])
        expect(fullText[0]) == "Tennis"
        expect(fullText[1]) == "The official site of Roger Federer is "
    }

    func testShouldBeWithAndApart() throws {
        try XCTSkipIf(clusteringManager.typeInUse != .legacy, "New clustering returns different results")
        clusteringManager.addPage(id: documents[0].id, tabId: tabIDs[0], parentId: nil, value: informations[0])
        clusteringManager.addPage(id: documents[1].id, tabId: tabIDs[1], parentId: nil, value: informations[1])
        clusteringManager.addPage(id: documents[2].id, tabId: tabIDs[2], parentId: nil, value: informations[2])
        clusteringManager.addPage(id: documents[3].id, tabId: tabIDs[3], parentId: nil, value: informations[3])

        clusteringManager.shouldBeWithAndApart(pageId: documents[0].id, beWith: [documents[1].id], beApart: [documents[2].id, documents[3].id])
        clusteringManager.shouldBeWithAndApart(pageId: documents[2].id, beWith: [documents[3].id], beApart: [documents[0].id, documents[1].id])
        expect(self.clusteringManager.clusteredPagesId).toEventually(equal([[self.pageIDs[0],self.pageIDs[1]], [self.pageIDs[2], self.pageIDs[3]]]))
    }
}
