import {Page, Response, Route, expect} from '@playwright/test';

import {
    ControlQA,
    DashCommonQa,
    DashEntryQa,
    DashRelationTypes,
    DatalensTabsQa,
    DialogConfirmQA,
    DialogDashWidgetQA,
    DialogGroupControlQa,
    DialogTabsQA,
    EntryDialogQA,
    LOADED_DASH_CLASS,
    SelectQa,
    WysiwygEditorQa,
    YfmQa,
} from '../../../src/shared/constants';
import {COMMON_DASH_SELECTORS} from '../../suites/dash/constants';
import {
    clickDropDownOption,
    clickGSelectOption,
    cssSlct,
    deleteEntity,
    entryDialogFillAndSave,
    getAddress,
    getUniqueTimestamp,
    isEnabledFeature,
    openTestPage,
    slct,
    waitForCondition,
} from '../../utils';
import {COMMON_SELECTORS} from '../../utils/constants';
import {BasePage, BasePageProps} from '../BasePage';
import Revisions from '../common/Revisions';

import {Locator} from 'playwright-core';
import {
    DashboardDialogSettingsQa,
    DialogDashTitleQA,
    DialogDashWidgetItemQA,
    Feature,
} from '../../../src/shared';
import {
    ActionPanelDashSaveControlsQa,
    ActionPanelEntryContextMenuQa,
} from '../../../src/shared/constants/qa/action-panel';
import {
    DashBodyQa,
    DashKitOverlayMenuQa,
    DashTabsQA,
    DashboardAddWidgetQa,
    DashkitQa,
} from '../../../src/shared/constants/qa/dash';
import {WorkbookPageQa} from '../../../src/shared/constants/qa/workbooks';
import {WorkbookIds, WorkbooksUrls} from '../../constants/constants';
import {getUrlStateParam} from '../../suites/dash/helpers';
import {COMMON_CHARTKIT_SELECTORS} from '../constants/chartkit';
import {CommonUrls} from '../constants/common-urls';
import {DialogCreateEntry} from '../workbook/DialogCreateEntry';
import {EditEntityButton} from '../workbook/EditEntityButton';
import {Workbook} from '../workbook/Workbook';
import {ChartkitControl} from './ChartkitControl';
import ControlActions from './controlActions/ControlActions';
import {DashTabs} from './DashTabs';
import DashboardSettings from './DashboardSettings';
import Description from './Description';
import {FixedHeader} from './FixedHeader';
import TableOfContent from './TableOfContent';

export const BUTTON_CHECK_TIMEOUT = 3000;
export const RENDER_TIMEOUT = 4000;
export const DASH_LOADER_CHECK_TIMEOUT = 2000;
export const API_TIMEOUT = 3000;

export const URLS = {
    navigationOnDelete: '/dashboards',
    createLock: '/gateway/root/us/createLock',
    deleteLock: '/gateway/root/us/deleteLock',
    savePath: '/api/dash/v1/dashboards',
};

export interface DashboardPageProps extends BasePageProps {}

type LocatorOptionsType = Parameters<Page['locator']>[1];
type LocatorClickOptionsType = Parameters<Locator['click']>[0];

class DashboardPage extends BasePage {
    static selectors = {
        title: '.dashkit-plugin-title',
        text: '.dashkit-plugin-text',

        mobileModal: '.g-mobile-modal',
        tabsContainer: '.gc-adaptive-tabs',
        tabsList: '.gc-adaptive-tabs__tabs-list',
        tabItem: '.gc-adaptive-tabs__tab',
        tabItemActive: '.gc-adaptive-tabs__tab_active',
        tabItemDisabled: '.gc-adaptive-tabs__tab_disabled',

        selectItems: '.g-select-list',
        selectItemsMobile: '.g-select-list_mobile',
        selectItemTitle: '.g-select-list__option',
        selectItemTitleDisabled: '.g-select-list__option_disabled',

        chartGridItemContainer: `${slct(DashkitQa.GRID_ITEM)} .chartkit`,
    };

    revisions: Revisions;
    tableOfContent: TableOfContent;
    description: Description;
    dashTabs: DashTabs;
    chartkitControl: ChartkitControl;
    dialogCreateEntry: DialogCreateEntry;
    editEntityButton: EditEntityButton;
    controlActions: ControlActions;
    fixedHeader: FixedHeader;

    constructor({page}: DashboardPageProps) {
        super({page});
        this.revisions = new Revisions(page);
        this.description = new Description(page);
        this.tableOfContent = new TableOfContent(page, this);
        this.dashTabs = new DashTabs(page);
        this.chartkitControl = new ChartkitControl(page);
        this.dialogCreateEntry = new DialogCreateEntry(page);
        this.editEntityButton = new EditEntityButton(page);
        this.controlActions = new ControlActions(page);
        this.fixedHeader = new FixedHeader(page);
    }

    async waitForResponses(url: string, timeout = API_TIMEOUT): Promise<Array<Response>> {
        return new Promise((resolve: any) => {
            const res: Array<Response> = [];

            const onResponse = (response: Response) => {
                const responseUrl = new URL(response.url());
                if (responseUrl.pathname === url) {
                    res.push(response);
                }
            };

            this.page.on('response', onResponse);

            setTimeout(() => {
                this.page.off('response', onResponse);
                resolve(res);
            }, timeout);
        });
    }

    async getMarkdownHTML() {
        const makrdownNode = await this.page.waitForSelector('.yfm');

        return makrdownNode.innerHTML();
    }

    async getMarkdownText(gridItemLocator?: Locator) {
        const yfmLocator = (gridItemLocator || this.page).locator(slct(YfmQa.WrapperHtml));

        return yfmLocator.innerText();
    }

    // Fill in the input with the name of the entity being created in the EntryDialog (the dialog that appears when saving entities) and click the "Create" button
    async entryNavigationDialogFillAndSave(page: Page, entryName: string) {
        // waiting for the save dialog to open
        const entryDialog = await page.waitForSelector(slct('entry-dialog-content'));
        const entryDialogInput = await entryDialog!.waitForSelector('[data-qa=path-select] input');
        // filling in the input
        await entryDialogInput!.fill(entryName);

        // save
        await page.click(slct(EntryDialogQA.Apply));
    }

    async expectControlsRequests({
        controlTitles,
        action,
        waitForLoader,
    }: {
        controlTitles: string[];
        waitForLoader?: boolean;
        action?: () => Promise<void>;
    }) {
        const loader = this.page.locator(slct(ControlQA.groupCommonLockedBlock));

        const handler = async (route: Route) => {
            await expect(loader).toBeVisible();

            await route.continue();
        };

        if (waitForLoader) {
            // check for loader appearence and passing request without changes
            await this.page.route(CommonUrls.ApiRun, handler);
        }

        // check that requests for passed selectors have completed successfully
        const controlResponses = controlTitles.map((title) => {
            const predicate = (response: Response) => {
                const isCorrespondingRequest =
                    response.status() === 200 &&
                    response.url().includes(CommonUrls.ApiRun) &&
                    response.request().postDataJSON()?.config?.data?.shared?.title === title;

                return isCorrespondingRequest;
            };

            return this.page.waitForResponse(predicate);
        });

        await action?.();

        await Promise.all(controlResponses);

        if (waitForLoader) {
            await expect(loader).toBeHidden();
            await this.page.unroute(CommonUrls.ApiRun, handler);
        }
    }

    async createDashboard({
        editDash,
        waitingRequestOptions,
        workbookId,
    }: {
        editDash: () => Promise<void>;
        waitingRequestOptions?: {
            controlTitles: string[];
            waitForLoader?: boolean;
        };
        workbookId?: string;
    }) {
        // some page need to be loaded so we can get data of feature flag from DL var
        await openTestPage(this.page, '/');
        const isCollectionsE2ETestsMode = await isEnabledFeature(
            this.page,
            Feature.CollectionsE2ETestsMode,
        );
        const createDashUrl = isCollectionsE2ETestsMode
            ? `/workbooks/${workbookId ?? WorkbookIds.E2EWorkbook}/dashboards`
            : '/dashboards/new';
        await openTestPage(this.page, createDashUrl);

        // callback with start actions with dash in edit mode
        await editDash();

        await this.clickSaveButton();

        const dashName = `e2e-entry-${getUniqueTimestamp()}`;

        const expectRequestPromise = waitingRequestOptions
            ? this.expectControlsRequests({
                  controlTitles: waitingRequestOptions.controlTitles,
                  waitForLoader: waitingRequestOptions.waitForLoader,
              })
            : null;

        // waiting for the dialog to open, specify the name, save
        if (isCollectionsE2ETestsMode) {
            await this.dialogCreateEntry.createEntryWithName(dashName);
        } else {
            await entryDialogFillAndSave(this.page, dashName);
        }

        // check that the dashboard has loaded by its name
        await this.page.waitForSelector(`${slct(DashEntryQa.EntryName)} >> text=${dashName}`);

        await expectRequestPromise;
    }

    async duplicateDashboard({dashId, useUserFolder}: {dashId?: string; useUserFolder?: boolean}) {
        const newDashName = `e2e-test-dash-copy-${getUniqueTimestamp()}`;

        const isCollectionsE2ETestsMode = await isEnabledFeature(
            this.page,
            Feature.CollectionsE2ETestsMode,
        );

        if (isCollectionsE2ETestsMode) {
            await this.duplicateDashboardFromWorkbook(dashId as string, newDashName);
            return;
        }

        await this.copyDashboard(newDashName, useUserFolder);
    }

    async copyDashboard(newDashName: string, useUserFolder?: boolean) {
        // click on the ellipsis in the upper panel
        await this.page.click(slct(COMMON_SELECTORS.ENTRY_PANEL_MORE_BTN));
        await this.page.waitForSelector(cssSlct(COMMON_SELECTORS.ENTRY_CONTEXT_MENU_KEY));

        // select Duplicate item
        await clickDropDownOption(this.page, ActionPanelEntryContextMenuQa.Copy);

        // click the Apply button in the dashboard copy dialog
        await this.page.waitForSelector(slct(EntryDialogQA.Apply));

        // waiting for the dialog to open, specify the name, save
        // waiting for the transition to the dashboard page
        await Promise.all([
            this.page.waitForNavigation(),
            entryDialogFillAndSave(this.page, newDashName, useUserFolder),
        ]);
    }

    async duplicateDashboardFromWorkbook(dashId: string, newDashName: string) {
        const workbookPO = new Workbook(this.page);

        await workbookPO.openE2EWorkbookPage();
        await this.page.locator(slct(DatalensTabsQa.Item), {hasText: 'Dashboards'}).click();

        await workbookPO.openWorkbookItemMenu(dashId);

        await this.page.locator(slct(WorkbookPageQa.MenuItemDuplicate)).click();

        // waiting for the dialog to open, specify the name, save
        await workbookPO.dialogCreateEntry.createEntryWithName(newDashName);

        await this.page
            .locator(`${slct(WorkbookPageQa.ListItem)}:has-text('${newDashName}')`)
            .click();
    }

    async clickAddChart() {
        await this.page.click(slct(DashboardAddWidgetQa.AddWidget));
    }

    async addChart({
        url,
        name,
        hideTitle,
        enableAutoHeight,
        addChartTab,
    }: {
        url: string;
        name: string;
        hideTitle?: boolean;
        enableAutoHeight?: boolean;
        addChartTab?: boolean;
    }) {
        // adding a chart
        if (!addChartTab) {
            await this.clickAddChart();
        }
        // click on "specify link
        await this.page.click(slct('navigation-input-use-link-button'));

        // specify the link
        await this.page.fill('[data-qa=navigation-input] input', getAddress(url));

        // adding
        await this.page.click(slct('navigation-input-ok-button'));
        // making sure that the necessary chart is selected
        await this.page.waitForSelector(`[data-qa=entry-title] * >> text=${name}`);

        if (hideTitle) {
            await this.page.locator(slct(DashCommonQa.WidgetShowTitleCheckbox)).click();
        }

        if (enableAutoHeight) {
            await this.page.click(slct(DashCommonQa.WidgetEnableAutoHeightCheckbox));
        }

        // adding to the dashboard
        await this.page.click(slct(DialogDashWidgetQA.Apply));
    }

    async copyWidget(counter?: number) {
        await this.page
            .locator(slct(ControlQA.controlMenu))
            .nth(counter || 0)
            .click();
        await this.page.locator(slct(DashKitOverlayMenuQa.CopyButton)).click();
    }

    async clickAddText() {
        await this.page.click(slct(DashboardAddWidgetQa.AddText));
    }

    async chooseMarkupText() {
        await this.page.click(slct(WysiwygEditorQa.SettingsButton));
        await this.page.click(slct(WysiwygEditorQa.ModeMarkupItemMenu));
    }

    async addText({
        text,
        timeout,
        markup,
        options,
    }: {
        text: string;
        timeout?: number;
        markup?: boolean;
        options?: {autoHeight?: boolean};
    }) {
        await this.clickAddText();

        await this.page.waitForSelector(slct(DialogDashWidgetItemQA.Text));

        if (markup) {
            await this.chooseMarkupText();
        }

        const isCollectionsE2ETestsMode = await isEnabledFeature(
            this.page,
            Feature.CollectionsE2ETestsMode,
        );
        if (isCollectionsE2ETestsMode) {
            await this.page.fill(`${slct(WysiwygEditorQa.Editor)} [contenteditable=true]`, text);
        } else {
            await this.page.fill(
                `${slct(DialogDashWidgetItemQA.Text)} [contenteditable=true]`,
                text,
                {timeout},
            );
        }
        if (options) {
            if (options.autoHeight) {
                await this.page.click(slct(DashCommonQa.WidgetEnableAutoHeightCheckbox));
            }
        }
        await this.page.click(slct(DialogDashWidgetQA.Apply));
    }

    async clickAddTitle() {
        await this.page.click(slct(DashboardAddWidgetQa.AddTitle));
    }

    async addTitle(text: string) {
        await this.clickAddTitle();
        await this.page.waitForSelector(slct(DialogDashWidgetItemQA.Title));
        await this.page.fill(`${slct(DialogDashTitleQA.Input)} input`, text);
        await this.page.click(slct(DialogDashWidgetQA.Apply));
    }

    getDashKitTextItem(text: string) {
        return this.page.locator(slct(DashkitQa.GRID_ITEM)).getByText(text, {exact: true});
    }

    async deleteSelector(controlTitle: string) {
        const control = this.page.locator(slct('dashkit-grid-item'), {
            has: this.page.locator(slct(ControlQA.chartkitControl, controlTitle)),
        });
        const controlSwitcher = control.locator(slct(ControlQA.controlMenu));

        await expect(
            controlSwitcher,
            `The control menu ${controlTitle} was not found, make sure the dashboard is in edit mode and the controlTitle is correct`,
        ).toBeVisible();

        await controlSwitcher.click();

        await this.page.click(slct(DashKitOverlayMenuQa.RemoveButton));
    }

    async editDashboard({editDash}: {editDash: () => Promise<void>}) {
        await this.enterEditMode();

        await editDash();

        await this.saveChanges();
    }

    async enterEditMode() {
        // switch to edit mode from view
        const createLockPromise = this.page.waitForRequest(URLS.createLock);
        await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
        await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
        await createLockPromise;
        await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN));
    }

    async forceEnterEditMode() {
        // switch to edit mode from view
        const createLockPromise = this.page.waitForRequest(URLS.createLock);
        await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));

        // waiting for the opening of the warning dialog or the Cancel edit button
        const elem = await this.page.waitForSelector(
            `${slct(DialogConfirmQA.Dialog)}, ${slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN)}`,
        );

        const qaAttr = await elem.getAttribute('data-qa');

        if (qaAttr !== DialogConfirmQA.Dialog) {
            await createLockPromise;
            return;
        }

        // click "Edit anyway"
        await this.page.click(slct(DialogConfirmQA.ApplyButton));
        await createLockPromise;
        await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN));
    }

    async exitEditMode() {
        // exiting the default editing mode of an empty dashboard
        try {
            const deleteLockPromise = this.page.waitForRequest(URLS.deleteLock);
            await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN));
            await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN));

            // if there are changes, a dialog with a warning about the unsaved changes will appear
            const warningCancelDialog = this.page.locator(slct(DialogConfirmQA.Dialog));
            const editButton = this.page.locator(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));

            await expect(editButton.or(warningCancelDialog)).toBeVisible();

            if (await editButton.isVisible()) {
                await deleteLockPromise;
                return;
            }

            // if there is a dialog, click the apply button
            const applyBtn = await this.page.waitForSelector(slct(DialogConfirmQA.ApplyButton));
            await applyBtn.click();
            await deleteLockPromise;
            await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
        } catch {
            // dash is not in edit mode
        }
    }

    async editDashWithoutSaving() {
        // adding a plug selector to the dashboard
        await this.controlActions.addSelector({
            appearance: {title: 'testSelector'},
            fieldName: 'testName',
        });
    }

    async makeDraft() {
        // creating a draft version (you need to enter edit mode in advance)
        await this.editDashWithoutSaving();
        await this.saveChangesAsDraft();
    }

    async clickOnLinksBtn() {
        // click on the "connections" button
        await this.page.click(slct(COMMON_SELECTORS.ACTION_BTN_CONNECTIONS));
    }

    async openDashConnections() {
        // switch to edit mode from view
        await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
        // waiting for the opening of the warning dialog or the Cancel edit button
        const elem = await this.page.waitForSelector(
            `${slct(DialogConfirmQA.Dialog)}, ${slct(COMMON_SELECTORS.ACTION_PANEL_CANCEL_BTN)}`,
        );

        const qaAttr = await elem.getAttribute('data-qa');

        if (qaAttr === DialogConfirmQA.Dialog) {
            // click "Edit anyway"
            await this.page.click(slct(DialogConfirmQA.ApplyButton));
        }

        // click on the "connections" button
        await this.clickOnLinksBtn();
    }

    async applyAliasesChanges() {
        await this.page.locator(slct(DashCommonQa.AliasAddApplyBtn)).click();
    }

    async cancelAliasesChanges() {
        await this.page.locator(slct(DashCommonQa.AliasesCancelBtn)).click();
    }

    async applyRelationsChanges() {
        await this.page.locator(slct(DashCommonQa.RelationsApplyBtn)).click();
    }

    async cancelRelationsChanges() {
        await this.page.locator(slct(DashCommonQa.RelationsCancelBtn)).click();
    }

    async openControlRelationsDialog() {
        await this.enterEditMode();
        // open dialog relations by control icon click
        const selectorElem = await this.controlActions.getDashControlLinksIconElem();
        await selectorElem.click();
    }

    async setupIgnoreAllLinks(widgetElem: Locator) {
        await widgetElem.click();
        await this.page.locator(slct(DashCommonQa.RelationsDisconnectAllSwitcher)).click();
        await this.page.locator(slct(DashCommonQa.RelationsDisconnectAllWidgets)).click();
        await this.applyRelationsChanges();
    }

    async setupIgnoreLink(widgetElem: Locator) {
        // open dialog relations by click on control item links icon
        await widgetElem.click();

        // choose new link
        await this.page.click(slct(DashCommonQa.RelationTypeButton));
        await this.page.click(slct(DashRelationTypes.ignore));

        await this.applyRelationsChanges();
    }

    async setupNewLinks({
        widgetElem,
        selectorName,

        linkType,
        firstParamName,
        secondParamName,
    }: {
        linkType: DashRelationTypes;
        firstParamName: string;
        secondParamName: string;
    } & (
        | {widgetElem: Locator; selectorName?: undefined}
        | {selectorName: string; widgetElem?: undefined}
    )) {
        if (widgetElem) {
            // open dialog relations by click on control item links icon
            await widgetElem.click();
        } else if (selectorName) {
            // click on global links button
            await this.clickOnLinksBtn();

            await clickGSelectOption({
                page: this.page,
                key: DashCommonQa.RelationsWidgetSelect,
                optionText: selectorName,
            });
        } else {
            throw new Error('Relation dialog needs selectorName or widgetElement param');
        }

        // choose new link
        await this.page.click(slct(DashCommonQa.RelationTypeButton));
        await this.page.click(slct(linkType));

        // select field for first item
        await this.page.click(slct(DashCommonQa.AliasSelectLeft));
        await this.page.locator(slct(SelectQa.POPUP, firstParamName)).click();

        // select field for second item
        await this.page.click(slct(DashCommonQa.AliasSelectRight));
        await this.page.locator(slct(SelectQa.POPUP, secondParamName)).click();

        // click apply in all relation dialogs
        await this.page.click(slct(DashCommonQa.AliasAddBtn));
        await this.applyAliasesChanges();
        await this.applyRelationsChanges();
    }

    async hasChanges() {
        const saveButton = await this.page.locator(slct(ActionPanelDashSaveControlsQa.Save));
        const disabledAttribute = await saveButton.getAttribute('disabled');

        return disabledAttribute === null;
    }

    async clickSaveButton() {
        // save the changes made on the dashboard
        await this.page.click(slct(ActionPanelDashSaveControlsQa.Save));
    }

    async saveChanges() {
        const savePromise = this.page.waitForRequest((request) =>
            request.url().includes(URLS.savePath),
        );
        const deleteLockPromise = this.page.waitForRequest(URLS.deleteLock);
        await this.clickSaveButton();
        await Promise.all([deleteLockPromise, savePromise]);
        await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
    }

    async saveChangesAsDraft() {
        // save the changes made on the dashboard as a draft
        await this.page
            .waitForSelector(slct(ActionPanelDashSaveControlsQa.SaveAsDraft), {
                timeout: BUTTON_CHECK_TIMEOUT,
            })
            .then(async () => {
                // in ActionPanel, the default save button is "Save as Draft"
                const deleteLockPromise = this.page.waitForRequest(URLS.deleteLock);
                const savePromise = this.page.waitForRequest(URLS.savePath);
                await this.page.click(slct(ActionPanelDashSaveControlsQa.SaveAsDraft));
                await Promise.all([deleteLockPromise, savePromise]);
                await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_EDIT_BTN));
            })
            .catch(async () => {
                // in ActionPanel, the default save button is "Save"
                await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_AS_BTN));
                await clickDropDownOption(
                    this.page,
                    ActionPanelDashSaveControlsQa.SaveAsDraftDropdownItem,
                );
            });
    }

    async saveChangesAndPublish() {
        // save and publish the changes made on the dashboard
        await this.page
            .waitForSelector(slct(ActionPanelDashSaveControlsQa.SaveAsDraft), {
                timeout: BUTTON_CHECK_TIMEOUT,
            })
            .then(async () => {
                // in ActionPanel, the default save button is "Save as Draft"
                await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_AS_BTN));
                await clickDropDownOption(
                    this.page,
                    ActionPanelDashSaveControlsQa.SaveAndPublishDropdownItem,
                );
            })
            .catch(async () => {
                // in ActionPanel, the default save button is "Save"
                await this.page.click(slct(ActionPanelDashSaveControlsQa.Save));
            });
    }

    async clickSaveChangesAsNewDash() {
        await this.page.waitForSelector(slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_AS_BTN));
        await this.page.click(slct(COMMON_SELECTORS.ACTION_PANEL_SAVE_AS_BTN));

        await clickDropDownOption(this.page, ActionPanelDashSaveControlsQa.SaveAsNewDropdownItem);
    }

    async saveChangesAsNewDash(dashName: string) {
        // save your changes as a new dashboard
        await this.clickSaveChangesAsNewDash();

        // click the Apply button in the dashboard copy dialog
        await this.page.waitForSelector(slct(EntryDialogQA.Apply));
        await Promise.all([
            this.page.waitForNavigation(),
            entryDialogFillAndSave(this.page, dashName),
        ]);
    }

    async deleteDash(args?: {workbookId?: string}) {
        const {workbookId = WorkbooksUrls.E2EWorkbook} = args ?? {};
        const isCollectionsE2ETestsMode = await isEnabledFeature(
            this.page,
            Feature.CollectionsE2ETestsMode,
        );
        const urlOnDelete = isCollectionsE2ETestsMode ? workbookId : '/dashboards';

        await deleteEntity(this.page, urlOnDelete);
    }

    async deleteDashFromEditMode() {
        try {
            await this.exitEditMode();
            await this.deleteDash();
        } catch {
            // can't delete dash from edit mode
        }
    }

    async deleteDashFromViewMode() {
        try {
            await this.deleteDash();
        } catch {
            // can't delete dash from view mode
        }
    }

    async closeMobileModal() {
        this.page.mouse.click(0, 0);
        await waitForCondition(async () => {
            const elements = await this.page.$$(DashboardPage.selectors.mobileModal);
            return elements.length === 0;
        });
    }

    async clickTabs() {
        // click on the "tabs" button
        await this.page.click(slct(COMMON_SELECTORS.ACTION_BTN_TABS));
    }

    async addTab(name?: string) {
        // adding tab (by default: Tab 2)
        await this.clickTabs();
        await this.page.click(slct(DialogTabsQA.RowAdd));
        if (name) {
            await this.page.locator(slct(DialogTabsQA.ReadOnlyTabItem)).last().dblclick();
            await this.page.locator(`${slct(DialogTabsQA.EditTabItem)} input`).fill(name);
        }
        await this.page.click(slct(DialogTabsQA.Save));
    }

    async changeTab({tabName, index}: {tabName?: string; index?: number}) {
        const tabsContainerLocator = this.page.locator(slct(DashTabsQA.Root));

        await expect(tabsContainerLocator).toBeVisible();

        let tabLocator;
        if (tabName) {
            tabLocator = tabsContainerLocator.getByText(tabName);
        } else if (typeof index === 'number') {
            tabLocator = tabsContainerLocator
                .locator(slct(DatalensTabsQa.Item))
                .or(tabsContainerLocator.locator(slct(DatalensTabsQa.MobileItem)))
                .nth(index);
        } else {
            throw new Error('Tabs selector not found');
        }

        const isTabLocatorVisible = await tabLocator.isVisible();

        if (isTabLocatorVisible) {
            await tabLocator.click();
            return;
        }

        // it can be single switcher with tab name or some tabs and "more" switcher
        const switcherLocator = tabsContainerLocator.locator(slct(DatalensTabsQa.SwitcherItem));
        const switcherTabLocator = tabsContainerLocator
            .locator(slct(DatalensTabsQa.MobileItem))
            .first();

        const isSwitcherVisible = await switcherLocator.isVisible();
        const isSwitcherTabVisible = await switcherTabLocator.isVisible();
        let mobileTabLocator;

        if (isSwitcherVisible) {
            mobileTabLocator = switcherLocator;
        } else if (isSwitcherTabVisible) {
            mobileTabLocator = switcherTabLocator;
        } else {
            throw new Error('Tabs selector not found');
        }

        await mobileTabLocator.click();
        await expect(this.page.locator(slct(SelectQa.SHEET))).toBeVisible();

        if (tabName) {
            await this.page
                .locator(
                    `${DashboardPage.selectors.selectItems}${DashboardPage.selectors.selectItemsMobile} ${DashboardPage.selectors.selectItemTitle} >> text=${tabName}`,
                )
                .click();
        } else if (typeof index === 'number') {
            await this.page
                .locator(
                    `${DashboardPage.selectors.selectItems}${DashboardPage.selectors.selectItemsMobile} ${DashboardPage.selectors.selectItemTitle}`,
                )
                .nth(index)
                .click();
        }
        return;
    }

    async changeTabAndGetState({tabName, timeout}: {tabName: string; timeout: number}) {
        await Promise.all([this.page.waitForNavigation(), this.changeTab({tabName})]);
        await this.page.waitForTimeout(timeout);

        const stateParam = getUrlStateParam(this.page);
        return stateParam;
    }

    async changeWidgetTab(tabName: string) {
        const tabsContainer = await this.page.waitForSelector(
            `${slct(DashkitQa.GRID_ITEM)} ${DashboardPage.selectors.tabsContainer}`,
        );

        // check for desktop tabs
        const fullTab = await tabsContainer.$(DashboardPage.selectors.tabsList);

        if (fullTab) {
            const tab = await fullTab.waitForSelector(
                `${DashboardPage.selectors.tabItem}:not(${DashboardPage.selectors.tabItemDisabled}) >> text=${tabName}`,
            );
            await tab.click();
            return;
        }

        // check for mobile tabs
        const shortTab = await tabsContainer.$(
            `${DashboardPage.selectors.tabItem}${DashboardPage.selectors.tabItemActive}`,
        );

        if (shortTab) {
            await shortTab.click();
            const tab = await this.page.waitForSelector(
                `${DashboardPage.selectors.selectItems} ${DashboardPage.selectors.selectItemTitle}:not(${DashboardPage.selectors.selectItemTitleDisabled}) >> text=${tabName}`,
            );
            await tab.click();
            return;
        }

        throw new Error('Tabs selector not found');
    }

    async enableDashboardTOC() {
        const dashboardSettings = new DashboardSettings(this.page);

        await dashboardSettings.open();
        await this.page.click(slct(DashboardDialogSettingsQa.TOCSwitch));
        await dashboardSettings.save();
    }

    async waitForLoaderDisappear() {
        try {
            const dashLoader = await this.page.waitForSelector(
                `.${COMMON_DASH_SELECTORS.DASH_LOADER}`,
                {
                    timeout: DASH_LOADER_CHECK_TIMEOUT,
                },
            );
            if (dashLoader) {
                await this.page.waitForSelector(`.${COMMON_DASH_SELECTORS.DASH_LOADER}`, {
                    state: 'detached',
                });
            }
        } catch {
            // the loader was not appeared
        }
    }

    async waitForOpeningRevisionsList() {
        await this.revisions.openList();
    }

    async waitForOpeningDraft() {
        await this.revisions.openDraft();
    }

    async waitForOpeningActual() {
        await this.revisions.openActualVersion();
        await this.waitForLoaderDisappear();
    }

    async waitForOpenTableOfContent() {
        await this.tableOfContent.open();
    }

    async waitForCloseTableOfContent() {
        await this.tableOfContent.close();
    }

    async waitForOpenDescription() {
        await this.description.open();
    }

    async waitForCloseDescription() {
        await this.description.close();
    }

    async isDescriptionViewMode() {
        await this.description.isViewMode();
    }

    async isDescriptionEditMode() {
        await this.description.isEditMode();
    }

    getSelectorLocatorByTitle({
        title,
        counter,
        type = 'select',
    }: {
        title: string;
        counter?: number;
        type?: 'select' | 'input';
    }) {
        const controlSelector =
            type === 'select' ? slct(ControlQA.controlSelect) : slct(ControlQA.controlInput);
        return this.page
            .locator(slct(ControlQA.chartkitControl))
            .filter({hasText: title})
            .locator(controlSelector)
            .nth(counter === undefined ? 0 : counter);
    }

    async checkSelectValueByTitle({
        title,
        value,
    }: {
        title: string;
        counter?: number;
        value: string;
    }) {
        const selectLocator = this.getSelectorLocatorByTitle({title});
        await expect(selectLocator).toContainText(value);
    }

    async setSelectWithTitle(
        {title, counter}: {title: string; counter?: number},
        valueTitle: string,
    ) {
        const selectLocator = this.getSelectorLocatorByTitle({title, counter});
        await selectLocator.click();

        await this.page
            .locator(slct(ControlQA.controlSelectItems))
            .locator(`[data-value] >> text="${valueTitle}"`)
            .click();
        await expect(selectLocator).toContainText(valueTitle);
    }

    async waitForSomeItemVisible() {
        await this.page.waitForSelector(slct('dashkit-grid-item'));
    }

    async waitForSomeChartItemVisible() {
        const chartItemLocator = this.page.locator(DashboardPage.selectors.chartGridItemContainer);
        await expect(chartItemLocator).toBeVisible();
    }

    async shouldNotContainsChartItems() {
        await waitForCondition(async () => {
            const elems = await this.page.$$(DashboardPage.selectors.chartGridItemContainer);
            return elems.length === 0;
        });
    }

    async clickFirstControlSettingsButton() {
        const controlSettingsButton = await this.page.waitForSelector(
            slct(ControlQA.controlSettings),
        );
        await controlSettingsButton.click();
    }

    async disableAutoupdateInFirstControl() {
        await this.enterEditMode();
        await this.clickFirstControlSettingsButton();
        await this.page.locator(slct(DialogGroupControlQa.groupSettingsTab)).click();
        await this.page
            .locator(`${slct(DialogGroupControlQa.updateControlOnChangeCheckbox)} input`)
            .setChecked(false);
        await this.controlActions.applyControlSettings();
        await this.saveChanges();
    }

    /**
     *  Selector for gridItem
     *
     * @param filter
     * filter.byHeader - search by header text in tabs,
     * if tab is not shown currently in tabs list then filter will fail
     *
     * filter.byEntiryId - filters only currently rendered charts entities
     * if element is not presented in DOM selector will fail
     *
     * Otherwise default Locator options can be used: `has`, `hasNot`, `hasText`, `hasNotText`
     *
     * @return {Locator}
     */
    async getGridItem(filter: {byHeader?: string; byEntiryId?: string} & LocatorOptionsType) {
        let gridItemFilter: LocatorOptionsType;

        if (filter.byHeader) {
            gridItemFilter = {
                has: this.page.locator(slct('widget-chart-tab', filter.byHeader)),
            };
        } else if (filter.byEntiryId) {
            gridItemFilter = {
                has: this.page.locator(slct(`chartkit-body-entry-${filter.byEntiryId}`)),
            };
        } else {
            gridItemFilter = filter;
        }

        return this.page.locator(`.${DashkitQa.GRID_ITEM}`, gridItemFilter);
    }

    async waitForChartsRender(gridItemLocators: Locator[]) {
        return Promise.all(
            gridItemLocators.map((item) => {
                return waitForCondition(() =>
                    item.locator(`.${COMMON_CHARTKIT_SELECTORS.chartkit}`).isVisible(),
                );
            }),
        );
    }

    async waitForWidgetsRender() {
        // content-wrapper has loaded class when all widgets are rendered
        const loadedWrapper = this.page.locator(slct(DashBodyQa.ContentWrapper));
        await expect(loadedWrapper).toHaveClass(new RegExp(LOADED_DASH_CLASS));
    }

    async getTableFirstRowTexts(gridItemLocator: Locator) {
        const firstCellLocator = gridItemLocator.locator('tbody tr td:first-child');

        return firstCellLocator.allInnerTexts();
    }

    async filterTableByText(
        gridItemLocator: Locator,
        text: string,
        options?: LocatorClickOptionsType,
    ) {
        const cellLocator = gridItemLocator.locator('tbody tr td:first-child').getByText(text);

        return Promise.all([
            this.waitForSuccessfulResponse(CommonUrls.PartialCreateDashState),
            cellLocator.first().click(options),
        ]);
    }

    async resetChartFiltering(gridItemLocator: Locator) {
        return gridItemLocator.locator(slct(ControlQA.filtersClear)).click();
    }

    // it cannot be used with single selectors as they have different loading times
    async getGroupSelectorLabels() {
        const controlLabelLocator = this.page.locator(slct(ControlQA.controlLabel));
        const firstLabelLocator = controlLabelLocator.first();

        // if one of selectors is visible, group selector is loaded
        await expect(firstLabelLocator).toBeVisible();
        const allLabelsLocators = await controlLabelLocator.all();
        const labels = await Promise.all(allLabelsLocators.map((locator) => locator.textContent()));

        return labels;
    }

    async getNewStateHashAfterAction(action: () => Promise<void>) {
        const requestPromise = this.page.waitForRequest(CommonUrls.CreateDashState);
        await action();
        const finishedPromise = await requestPromise;

        // check new state
        const responseState = await finishedPromise.response();
        const jsonState = await responseState?.json();

        return jsonState?.hash;
    }

    async checkNoScroll({selector, locator}: {selector?: string; locator?: Locator}) {
        const elementLocator = selector ? this.page.locator(selector) : locator;
        if (!elementLocator) {
            return;
        }

        await waitForCondition(async () => {
            const hasNoScroll = await elementLocator.evaluate((element) => {
                return element?.clientHeight === element?.scrollHeight;
            });
            return hasNoScroll;
        });
    }

    /**
     * Scroll page to the bottom using keyboard End key
     * @returns Promise that resolves when scrolling is complete
     */
    async scrollToBottom(): Promise<void> {
        await this.page.locator(slct(DashBodyQa.App)).click();
        await this.page.keyboard.press('End');
    }

    /**
     * Click on action panel more button (ellipsis in the upper panel)
     * @returns Promise that resolves when click is complete
     */
    async clickActionPanelMoreButton(): Promise<void> {
        await this.page.click(slct(COMMON_SELECTORS.ENTRY_PANEL_MORE_BTN));
    }
}

export default DashboardPage;
