// Copyright (c) Mito
// Distributed under the terms of the Modified BSD License.

/*
    Contains all the selectors used across Mito frontend tests. 
*/

import { Selector} from 'testcafe';
import { testUser } from './roles';
import { modalAdvanceButtonSelector, modalCloseButtonSelector } from './allModals';
import { 
    getCellSelector, 
    selectKernelButton,
    fileTab,
    fileTabNew,
    fileTabNewNotebook,
    getActiveElement,
    ascendingSortSelector,
    descendingSortSelector,
    signupInputSelector,
} from './selectors';

import { getTabSelector } from '../utils/tabHelpers';

import {
    getColumnHeaderFilterSelector
} from './columnHelpers'
import { exception } from 'console';
import { LOCAL_TEST } from '../config';

// A string you can pass to t.pressKeys that will delete all values from the currently selecte input
export const DELETE_PRESS_KEYS_STRING = 'ctrl+a delete'

/*
    Sorts a column, yo.
*/
export async function sortColumn(t: TestController, columnHeader: string, direction: 'ascending' | 'descending'): Promise<void> {
    
    // Open the filter/sort modal
    await t.click(getColumnHeaderFilterSelector(columnHeader));

    if (direction === 'ascending') {
        await t.click(ascendingSortSelector);
    } else {
        await t.click(descendingSortSelector);
    }
}


/*
    Helper function for checking mulitple tabs in one go!
*/
export async function checkSheets(t: TestController, sheets: Record<string, Record<string, string[]>>): Promise<void> {
    for (const tabName in sheets) {
        await checkSheet(t, sheets[tabName], tabName);
    }
}


/*
    Helper function for checking that a given sheet has the passed data
*/
export async function checkSheet(t: TestController, dataFrame: Record<string, string[]>, tabName?: string): Promise<void> {
    if (tabName !== undefined) {
        await t.click(getTabSelector(tabName));
    }

    for (const key in dataFrame) {
        await checkColumn(t, key, dataFrame[key])
    }
}


/*
    Helper function for checking that a given column has exactly columnValues inside
    of it, and nothing else. 
*/
export async function checkColumn(t: TestController, columnHeader: string, columnValues: string[]): Promise<void> {

    for (let i = 0; i < columnValues.length; i++) {
        // There is a warning generated by this line that is a testcafe bug fixed here: https://github.com/DevExpress/testcafe/issues/5389
        await t.expect(getCellSelector(columnHeader, i.toString()).innerText).eql(columnValues[i])
    }

    // And we check there are no other values
    await t.expect(getCellSelector(columnHeader, columnValues.length.toString()).exists).notOk();
}

export async function createNotebookRunCell(t: TestController, logIn: boolean, cellText: string): Promise<void> {
    // Creates a new notebook, and inserts the given cellText into that notebook

    // Log in
    if (logIn && !LOCAL_TEST) {
        await t.useRole(testUser)
    }

    // First, we check if there is currently a select kernel modal (which there is sometimes
    // when a notebook fails to be deleted, for some reason. If there is, we click it, as many
    // times as it pops up
    try {
        // Keep clicking kernel buttons, as long as they are there!
        while (!(await selectKernelButton.exists)) {
            await t.click(selectKernelButton)
        }
    } catch (e) {
        // We don't care if this fails... it only needs to happen it when does...
    }

    // Then, we delete all the notebooks there are
    await deleteAllNotebooks(t);

    await t
        // Then, we make a new notebook
        .click(fileTab)
        .hover(fileTabNew)
        .click(fileTabNewNotebook)

    // Then, we try to select a kernel (this popup only appears sometimes), 
    // which is why we don't necessarily _need_ to click it

    try {
        await t.click(selectKernelButton)
    } catch (e) {
        console.log("No need to select kernel.")
    }


    if (cellText !== '') {
        await t
            // Then, we create a mito notebook
            .typeText(
                // By default, the first cell is focused on
                getActiveElement(), 
                cellText
            )
            .pressKey('shift+enter')
    }
}

export async function setup(t: TestController): Promise<void> {
    if (!LOCAL_TEST) {
        await t.useRole(testUser)
        await t.wait(25000) // wait 25 seconds            
    }

    await createNotebookRunCell(t, false, '');
    await t.pressKey('ctrl+s');
}


/*
    Helper function that should wrap every test. Is responsible for actually running the tests, and does
    so by:
    1. First, trying to clean out and write in the current notebook.
    2. If that fails, deleting the notebook and creating a new one.
    3. Signing in to Mito if required.

    It tries to run the test 4 times, with an increasing delay between them, four times. This is to ensure
    any testcafe tests that randomly fail can be avoided. As the Mito app doesn't have much concurreny or
    non-determinstic behavior, tests only passing some of the time has been a function of testing issues.
*/
export async function tryTest(t: TestController, cellText: string, test: (t: TestController) => Promise<void>): Promise<void> {
    let failed = false;

    // First, fill up the window
    await t.maximizeWindow();
    
    try {
        await fillCurrentNotebook(t, cellText);
        await signIn(t)
        await test(t);
    } catch (e) {
        console.error(e);
        failed = true;
    }

    // Return if we have not failed
    if (!failed) {
        return;
    } 
    
    failed = false;
    for (let i = 0; i < 3; i++) {
        try {
            console.log(`Try ${i + 1}, running with timeout ${5000}`)
            await t.wait(5000);
            await createNotebookRunCell(t, false, cellText);
            await signIn(t)
            await test(t);
        } catch (e) {
            console.log(`Failed try ${i + 1} with error`);
            console.error(e);
            failed = true;
        }

        if (!failed) {
            return;
        }
    }

    console.log("Waiting 60 seconds before last try")
    await t.wait(60000);

    // Don't wrap the last one, because we want to fail if this fails!
    console.log("Running last test")
    await createNotebookRunCell(t, false, cellText);
    await signIn(t)
    await test(t);
}

/*
    Checks if there is a modal open with a input field and tries to sign in
    if it exists. 

    Otherwise does nothing.
*/
async function signIn(t: TestController): Promise<void> {
    // Sign in if the user is required to
    try {
        // if the signup modal appears, click on it and sign in
        await t
            .click(signupInputSelector)
            .typeText(
                signupInputSelector,
                'testcafe@test.com'
            )
            // then submit by hitting the enter button
            .pressKey('enter')

        // then close the schedule a demo modal
        await t.click(modalCloseButtonSelector)
    } catch (e) {
        // We don't care if the login works, so we suppress this error
    }
}



/*
    Assumes there is an open notebook, where the content can be deleted and replaced.

    All content is deleted, the top cell is filled with cellText, and the cell is run.

    NOTE: We assume that
*/
export async function fillCurrentNotebook(t: TestController, cellText: string): Promise<void> {

    const cellInputBox = Selector('div.jp-InputPrompt')

    await t.wait(10000);
    const inputCount = await cellInputBox.count;

    await t.pressKey('esc')

    for (let i = 0; i < inputCount; i++) {
        await t.pressKey('d').pressKey('d');
    }

    await t
        .pressKey('enter')
        // Then, we create a mito notebook
        .typeText(
            // By default, the first cell is focused on
            getActiveElement(), 
            cellText
        )
        .pressKey('shift+enter')
}

export async function deleteAllNotebooks(t: TestController, failOnFailure?: boolean): Promise<void> {
    const notebookCount = await Selector('ul.jp-DirListing-content').child('li').count

    if (notebookCount === 0) {
        return;
    }
    
    try {
        // First, click the first notebook
        const firstNotebook = Selector('ul.jp-DirListing-content').child('li').nth(0)
        await t.click(firstNotebook)

        // Then, shift click the last notebook
        const last = Selector('ul.jp-DirListing-content').child('li').nth(notebookCount - 1)
        await t.click(last, {modifiers: {shift: true}})

        // Then, delete them all!
        const deleteModalButton = Selector('div.jp-Dialog-buttonLabel')
            .withExactText('Delete')

        await t
            .rightClick(firstNotebook)
            .pressKey('d')
            .click(deleteModalButton)
    } catch (e) {
        if (failOnFailure) {
            throw exception('Failed to delete notebooks!');
        }
        console.log("Failed deleting all notebooks")
    }
}

export async function deleteCurrentNotebook(t: TestController): Promise<void> {
    const notebook = Selector('ul.jp-DirListing-content')
        .child('li')
        .nth(0)

    const deleteModalButton = Selector('div.jp-Dialog-buttonLabel')
        .withExactText('Delete')
    
    await t
        .rightClick(notebook)
        .pressKey('d')
        .click(deleteModalButton)
}
