import * as fs from "fs";
import { Readable } from "stream";

import { hyphenate, isPresent } from "@bigbinary/neeto-cist";
import { faker } from "@faker-js/faker";
import {
  CustomCommands,
  EMBED_SELECTORS,
  getClipboardContent,
  hexToRGB,
  NEETO_FILTERS_SELECTORS,
  squish,
} from "@neetoplaywright";
import { APIRequestContext, expect, Locator, Page } from "@playwright/test";
import { Dayjs } from "dayjs";
import * as ExcelJS from "exceljs";
import * as csv from "fast-csv";
import { TFunction } from "i18next";
import * as pdfParse from "pdf-parse";
import { getI18nInstance } from "playwright-i18next-fixture";
import { isNotNil } from "ramda";

import {
  COMMON_REGEXES,
  FULL_MONTH_YEAR_FORMAT,
  ROUTING_FORM_SUBMISSION_DATE_FORMAT,
  SELECTED_DATE_FORMAT,
  STANDARD_DATE_FORMAT,
  VISIBILITY_TIMEOUT,
  YEAR_MONTH_FORMAT,
} from "@constants/common";
import { ROUTES } from "@constants/routes";
import {
  EUI_SELECTORS,
  MESSAGE_SELECTORS,
  QUESTIONS_SELECTORS,
  TRANSACTIONS_SELECTORS,
  THEME_SELECTORS,
  BOOKING_SELECTORS,
  MORE_SIDENAV_SELECTORS,
  COMMON_SELECTORS,
  SIDEBAR_SELECTORS,
} from "@selectors";
import {
  BOOKING_TEXTS,
  COLOR_MAPPING,
  EVENT_COLORS,
  GOOGLE_EVENT_COLORS,
  PROFILE_TEXTS,
  SCHEDULING_LINK_COLORS,
} from "@texts";
import {
  AssertEmptyStateProps,
  ClientDetails,
  KeyValuePairs,
  ReportContent,
  SearchProps,
  TimelineCard,
  VerifyButtonAttributesProps,
  VerifyTimeAndTooltipProps,
} from "@types";
import {
  shouldWaitForFloatingActionMenu,
  getTableDateFormats,
  getTooltipDateFormats,
} from "@utils";

import { ChangeRelativeOrderParams } from "../types";

export default class CommonUtils extends CustomCommands {
  request: APIRequestContext;
  t: TFunction;
  WEEK_DAYS: string[];

  constructor(
    public page: Page,
    request?: APIRequestContext
  ) {
    const actualRequest = request ?? page.request;
    super(page, actualRequest);
    this.page = page;
    this.request = actualRequest;
    this.t = getI18nInstance().t;

    this.WEEK_DAYS = [
      "sunday",
      "monday",
      "tuesday",
      "wednesday",
      "thursday",
      "friday",
      "saturday",
    ].map(day => this.t(`live.preBook.weekdays.${day}`));
  }

  switchTab = async ({
    tabName,
    tabItem = COMMON_SELECTORS.tabItem,
    customPageContext = this.page,
  }: {
    tabName: string;
    tabItem?: string;
    customPageContext?: Page;
  }) => {
    await this.waitForUIComponentsToLoad(customPageContext);

    const targetTab = customPageContext
      .getByTestId(tabItem)
      .filter({ hasText: new RegExp(`^${tabName}`, "i") });

    await targetTab.click({ delay: 1_000 });
    await this.waitForNeetoPageLoad(customPageContext.url(), customPageContext);

    await expect(targetTab).toHaveClass(/active/);
  };

  exportTableCSV = async () => {
    await this.page.getByTestId(TRANSACTIONS_SELECTORS.downloadButton).click();
    await expect(
      this.page.getByTestId(COMMON_SELECTORS.modalHeader)
    ).toBeVisible();

    await this.page
      .getByTestId(TRANSACTIONS_SELECTORS.modalFooter)
      .getByText(this.t("buttons.export"))
      .click();
  };

  downloadTableCSV = async () => {
    await this.exportTableCSV();
    await expect(
      this.page
        .getByTestId(COMMON_SELECTORS.modalForever)
        .locator(COMMON_SELECTORS.buttonSpinner)
    ).toBeHidden({ timeout: 7 * 60 * 1000 });

    const downloadButton = this.page
      .getByTestId(TRANSACTIONS_SELECTORS.modalFooter)
      .getByText(this.t("buttons.download"));

    await expect(downloadButton).toBeVisible({ timeout: 7 * 60 * 1000 });

    const downloadPromise = this.page.waitForEvent("download", {
      timeout: 50_000,
    });

    await downloadButton.click({ timeout: VISIBILITY_TIMEOUT });
    const download = await downloadPromise;

    await expect(
      this.page.getByTestId(COMMON_SELECTORS.modalHeader)
    ).toBeHidden();

    return download.path();
  };

  private parseBuffer = ({
    path,
    buffer,
  }: {
    path?: string;
    buffer?: Buffer;
  }) => (path ? fs.readFileSync(path) : buffer);

  verifyCsvContent = async ({ path, buffer, content }: ReportContent) => {
    const stream = buffer
      ? Readable.from(buffer.toString())
      : fs.createReadStream(path as string);

    let csvText = "";
    const normalizedExpectedValues = content.map(value => value.toLowerCase());

    await new Promise<void>((resolve, reject) => {
      csv
        .parseStream(stream, { headers: true })
        .on("data", data => {
          csvText += `${Object.values(data)
            .map(value => (value as string).toLowerCase())
            .join(" ")} `;
        })
        .on("end", () => {
          const allKeywordsExist = normalizedExpectedValues.every(
            expectedValue => csvText.includes(expectedValue)
          );

          try {
            expect(allKeywordsExist).toBe(true);
            resolve();
          } catch (error) {
            reject(error);
          }
        })
        .on("error", reject);
    });
  };

  verifyExcelContent = async ({ path, content, buffer }: ReportContent) => {
    const dataBuffer = this.parseBuffer({ path, buffer });

    // Using ExcelJS to parse instead of converting to plain string
    const workbook = new ExcelJS.Workbook();
    const excelBuffer = Buffer.from(dataBuffer);
    // @ts-expect-error - ExcelJS accepts Node.js Buffer at runtime
    await workbook.xlsx.load(excelBuffer);

    const worksheet = workbook.worksheets[0];
    const textFragments: string[] = [];

    worksheet.eachRow({ includeEmpty: false }, (row: ExcelJS.Row) =>
      row.eachCell({ includeEmpty: false }, (cell: ExcelJS.Cell) => {
        const value = cell.text || cell.value;
        const normalizedValue = squish(String(value));
        textFragments.push(normalizedValue);
      })
    );

    const cleanedText = textFragments.join(" ");
    content.forEach(text => expect(cleanedText).toContain(text));
  };

  verifyPdfContent = async ({ path, content, buffer }: ReportContent) => {
    const dataBuffer = this.parseBuffer({ path, buffer });

    const data = await pdfParse.default(dataBuffer);
    const cleanedText = squish(data.text);

    content.forEach(text => expect(cleanedText).toContain(text));
  };

  // This method is used in cases where we have some other div elements in input container which causes .clear() to fail
  clearTextInput = async ({ inputField }: { inputField: Locator }) => {
    await inputField.clear();
    await inputField?.evaluate(node => {
      while (node.firstChild) {
        node.removeChild(node.firstChild);
      }
    });

    await inputField.click();
    await this.page.keyboard.press("Enter");
  };

  clearTextBox = async (textBox: Locator) => {
    await textBox.click({ clickCount: 3 });
    await this.page.keyboard.press("Backspace");
  };

  navigateAndWaitForPageLoad = async (
    url: string,
    customPageContext = this.page
  ) => {
    await customPageContext.goto(url);

    await this.waitForNeetoPageLoad(url, customPageContext);
  };

  waitForFloatingActionMenu = (customPageContext: Page = this.page) =>
    expect(
      customPageContext.getByTestId(COMMON_SELECTORS.floatingActionMenuButton)
    ).toBeVisible({ timeout: VISIBILITY_TIMEOUT });

  verifyUnsavedChangeModal = async (shouldLeavePage: boolean = false) => {
    const alertBox = this.page.getByTestId(COMMON_SELECTORS.alertBox);
    await expect(alertBox.getByTestId(COMMON_SELECTORS.alertTitle)).toHaveText(
      this.t("alert.unsavedChangesAlert.title")
    );

    const buttonSelector = shouldLeavePage
      ? COMMON_SELECTORS.alertCancelButton
      : COMMON_SELECTORS.alertModalSubmitButton;

    await alertBox.getByTestId(buttonSelector).click();
    await expect(alertBox).toBeHidden();
  };

  clickSaveChangesButton = (customPage: Page | Locator = this.page) =>
    this.clickOnButton(COMMON_SELECTORS.saveChangesButton, customPage);

  clickOnButton = async (
    buttonLabel: string | RegExp,
    customPage: Page | Locator = this.page
  ) => {
    const saveChangesButton = customPage.getByTestId(buttonLabel);

    await saveChangesButton.click();
    await expect(
      saveChangesButton.getByTestId(COMMON_SELECTORS.uiSpinner)
    ).toBeHidden({ timeout: VISIBILITY_TIMEOUT });
  };

  reorderElements = async (source: Locator, target: Locator) => {
    const [sourceBoundingBox, targetBoundingBox] = await Promise.all([
      source.boundingBox(),
      target.boundingBox(),
    ]);

    if (!sourceBoundingBox || !targetBoundingBox) {
      throw new Error(
        "Could not retrieve bounding boxes for source or target custom fields."
      );
    }

    const sourceCenter = {
      x: sourceBoundingBox.x + sourceBoundingBox.width / 2,
      y: sourceBoundingBox.y + sourceBoundingBox.height / 2,
    };

    const targetCenter = {
      x: targetBoundingBox.x + targetBoundingBox.width / 2,
      y: targetBoundingBox.y + targetBoundingBox.height / 2,
    };

    await this.page.mouse.move(sourceCenter.x, sourceCenter.y);
    await this.page.mouse.down();
    await this.page.mouse.move(targetCenter.x, targetCenter.y, { steps: 10 });
    await this.page.mouse.up();
  };

  changeRelativeOrder = async (
    { currentOrder, sourceIndex, targetIndex, bar }: ChangeRelativeOrderParams,
    shouldSaveChanges: boolean = true
  ) => {
    const [sourceCustomField, targetCustomField] = [
      sourceIndex,
      targetIndex,
    ].map(idx => bar.filter({ hasText: currentOrder[idx] }));

    await this.reorderElements(sourceCustomField, targetCustomField);

    shouldSaveChanges && (await this.clickSaveChangesButton());
  };

  performActionFromMoreDropdown = async (
    parentLocator: Locator,
    action: string
  ) => {
    const baseLocator = parentLocator;
    await baseLocator
      .getByTestId(new RegExp(COMMON_SELECTORS.dropdownIcon))
      .click();

    const dropdownContainer = this.page.getByTestId(
      COMMON_SELECTORS.dropdownContainer
    );

    await dropdownContainer
      .getByRole("menuitem", { name: action })
      // eslint-disable-next-line playwright/no-nth-methods
      .first() //TODO: Use data-testid once this resolves https://github.com/neetozone/neeto-cal-playwright/issues/856
      .click();
    await expect(dropdownContainer).toBeHidden();
  };

  selectCheckboxAndSaveChanges = async (checkboxLabels: string[]) => {
    for (const checkboxLabel of checkboxLabels) {
      await this.page
        .getByTestId(COMMON_SELECTORS.customCheckboxLabel(checkboxLabel))
        .click();
    }

    await this.saveChanges();
  };

  verifyCellsInTable = (cells: string[], uniqueCellText: string) =>
    Promise.all(
      cells.map(cell =>
        expect(
          this.page
            .getByRole("row", { name: uniqueCellText })
            .getByRole("cell", { name: cell })
        ).toBeVisible()
      )
    );

  fillMultipleEmails = async (emails: string[]) => {
    await this.page
      .getByTestId(QUESTIONS_SELECTORS.multiEmailInputContainer)
      .click();

    const clearAllButton = this.page.getByTestId(COMMON_SELECTORS.clearAll);
    (await clearAllButton.isVisible()) && (await clearAllButton.click());

    for (const email of emails) {
      await this.page.keyboard.type(email);
      await this.page.keyboard.press(",");
    }
  };

  getClipboardContent = (customPageContext: Page = this.page) =>
    getClipboardContent(customPageContext);

  verifySortedValues = async (firstValue: string, secondValue: string) => {
    const arrayOfValues = await this.page
      .getByRole("row")
      .getByRole("cell")
      .allInnerTexts();

    const firstValueIndex = arrayOfValues.findIndex(cellText =>
      cellText.includes(firstValue)
    );

    const secondValueIndex = arrayOfValues.findIndex(cellText =>
      cellText.includes(secondValue)
    );

    expect(firstValueIndex).toBeLessThan(secondValueIndex);
  };

  verifyCSSProperties = async (
    locator: Locator,
    properties: KeyValuePairs,
    haveCss: boolean = true
  ) => {
    if (haveCss) {
      await Promise.all(
        properties.map(async ({ key, value }) => {
          const actualCssValue = await locator.evaluate(
            (el, cssKey) => getComputedStyle(el).getPropertyValue(cssKey),
            key
          );
          const expectedValue = hexToRGB(value);
          expect(actualCssValue).toContain(expectedValue);
        })
      );
    } else {
      await Promise.all(
        properties.map(async ({ key, value }) => {
          const actualCssValue = await locator.evaluate(
            (el, cssKey) => getComputedStyle(el).getPropertyValue(cssKey),
            key
          );
          const expectedValue = hexToRGB(value);
          expect(actualCssValue).not.toContain(expectedValue);
        })
      );
    }
  };

  verifyTimeAndTooltip = async ({
    date,
    locator,
    customPageContext = this.page,
    tableDateFormat = ROUTING_FORM_SUBMISSION_DATE_FORMAT,
  }: VerifyTimeAndTooltipProps) => {
    await expect(locator).toBeVisible();

    const tableDateFormats = getTableDateFormats(date, tableDateFormat);
    await expect(locator).toHaveText(RegExp(tableDateFormats));

    await this.verifyTooltip({
      content: () => getTooltipDateFormats(date),
      triggerElement: locator,
      customPageContext,
    });
  };

  waitForSlotsVisibility = async (customPageContext = this.page) => {
    await expect(
      customPageContext.getByTestId(EUI_SELECTORS.monthLabel)
    ).toBeVisible({ timeout: 35_000 });

    await expect(
      customPageContext.getByTestId(COMMON_SELECTORS.uiSpinner)
    ).toHaveCount(0, { timeout: 30_000 });

    await expect(
      customPageContext.getByTestId(EUI_SELECTORS.slotsList)
    ).toBeVisible();
  };

  getTotalAvailableSlots = () =>
    this.page.getByTestId(EUI_SELECTORS.startTimeSlotButton).all();

  fillAndSubmitClientDetails = async (
    { name, email }: ClientDetails,
    customPageContext: Page = this.page
  ) => {
    await expect(
      customPageContext.getByTestId(EUI_SELECTORS.meetingNameHeader)
    ).toBeVisible();

    await this.waitForPageLoad({ customPageContext });

    await customPageContext
      .getByTestId(EUI_SELECTORS.nameInput)
      .fill(name, { timeout: 15_000 });

    await customPageContext.getByTestId(EUI_SELECTORS.emailInput).fill(email);
    await this.submitClientDetails(customPageContext);
  };

  submitClientDetails = (customPageContext = this.page) =>
    this.clickOnButton(
      new RegExp(
        `${EUI_SELECTORS.submitBtn}|${COMMON_SELECTORS.saveChangesButton}`
      ),
      customPageContext
    );

  assertBookingConfirmation = (customPageContext = this.page) =>
    expect(
      customPageContext.getByTestId(EUI_SELECTORS.bookingStatusTitle)
    ).toHaveText(
      this.t("live.booking.statuses.title", {
        status: this.t("live.booking.statuses.confirmed"),
      }),
      { timeout: VISIBILITY_TIMEOUT }
    );

  submitClientDetailsAndWaitForBookingConfirmation = async (
    customPageContext = this.page
  ) => {
    await this.submitClientDetails(customPageContext);
    await this.assertBookingConfirmation(customPageContext);
  };

  bringElementIntoView = (locator: Locator) =>
    expect(async () => {
      await locator.scrollIntoViewIfNeeded();
      await expect(locator).toBeInViewport();
    }).toPass({ timeout: 30_000 });

  search = async ({
    customPage = this.page,
    inputField = COMMON_SELECTORS.inputField,
    searchTerm = faker.word.words(3),
    shouldWaitForSearchTermBlock = true,
  }: SearchProps = {}) => {
    const searchInput = customPage.getByTestId(inputField);

    await searchInput.fill(searchTerm);
    await searchInput.blur();

    await this.waitForNeetoPageLoad();
    shouldWaitForSearchTermBlock &&
      (await expect(
        customPage.getByTestId(NEETO_FILTERS_SELECTORS.searchTermBlock)
      ).toBeVisible());
  };

  assertEmptyState = async ({
    customPage = this.page,
    title,
    helpText,
  }: AssertEmptyStateProps) => {
    isPresent(title) &&
      (await expect(
        customPage.getByTestId(COMMON_SELECTORS.noDataTitle)
      ).toHaveText(title));

    isPresent(helpText) &&
      (await expect(
        customPage.getByTestId(COMMON_SELECTORS.noDataHelpText)
      ).toHaveText(helpText));
  };

  verifyButtonAttributes = async ({
    locator,
    href,
    label,
    isExternal = false,
  }: VerifyButtonAttributesProps) => {
    await Promise.all([
      expect(locator).toHaveAttribute("href", href),
      expect(locator).toContainText(label),
    ]);

    isExternal
      ? await expect(locator).toHaveAttribute("target", "_blank")
      : await expect(locator).not.toHaveAttribute("target", "_blank");
  };

  verifyTimeline = async (
    timelineCards: TimelineCard[],
    customPageContext = this.page
  ) => {
    for (const [arrayIndex, { date, statuses }] of timelineCards.entries()) {
      const timelineCard = customPageContext
        .getByTestId(MESSAGE_SELECTORS.timelineCard)
        // eslint-disable-next-line playwright/no-nth-methods
        .nth(arrayIndex); // Required to assert the relative order of timeline cards

      await timelineCard.scrollIntoViewIfNeeded();

      await this.verifyTimeAndTooltip({
        date,
        locator: timelineCard.getByTestId(COMMON_SELECTORS.timelineTimeStamp),
        customPageContext,
      });

      await Promise.all(
        statuses.map(status => expect(timelineCard).toContainText(status))
      );
    }
  };

  reloadAndWaitForPageLoad = async (customPageContext = this.page) => {
    const url = customPageContext.url();
    await customPageContext.reload();

    await this.waitForNeetoPageLoad(url, customPageContext);
  };

  waitForNeetoPageLoad = async (url?: string, pageContext = this.page) => {
    await expect(
      pageContext.getByTestId(COMMON_SELECTORS.pageLoader)
    ).toHaveCount(0, { timeout: VISIBILITY_TIMEOUT });

    shouldWaitForFloatingActionMenu(isNotNil(url) ? url : pageContext.url()) &&
      (await this.waitForFloatingActionMenu(pageContext));

    await expect(
      pageContext.getByTestId(COMMON_SELECTORS.uiSpinner)
    ).toHaveCount(0, { timeout: VISIBILITY_TIMEOUT });
  };

  waitForUIComponentsToLoad = (customPageContext = this.page) =>
    expect(
      customPageContext.getByTestId(COMMON_SELECTORS.uiSpinner)
    ).toHaveCount(0, { timeout: 35_000 });

  verifyElementVisibility = async (
    selector: string,
    route: string = ROUTES.adminPanel.general.index
  ) => {
    await this.navigateAndWaitForPageLoad(route);
    await expect(this.page.getByTestId(selector)).toBeVisible();
  };

  selectCountryLabel = async (
    phoneNumberFieldLabel: string,
    page: Page = this.page
  ) => {
    const phoneNumberSelectContainer = page
      .getByTestId(QUESTIONS_SELECTORS.fieldInput(phoneNumberFieldLabel))
      .getByTestId(COMMON_SELECTORS.customSelectValueContainer());
    const dropdownMenu = page.getByTestId(COMMON_SELECTORS.dropdownMenu);

    await expect(async () => {
      await phoneNumberSelectContainer.click();
      await expect(dropdownMenu).toBeVisible({ timeout: 5_000 });
      await dropdownMenu
        .getByText(PROFILE_TEXTS.india, { exact: true })
        .click();

      await expect(phoneNumberSelectContainer).toContainText(
        BOOKING_TEXTS.indiaPhoneLabel,
        { timeout: 5_000 }
      );
    }).toPass({ timeout: 30_000 });
  };

  pickPreferredEventColor = async (
    shouldSaveChanges: boolean = true,
    isGoogleCalendar: boolean = false
  ) => {
    const colors = isGoogleCalendar
      ? GOOGLE_EVENT_COLORS
      : SCHEDULING_LINK_COLORS;

    const colourPalleteItem = this.page.locator(
      THEME_SELECTORS.colorPaletteItem
    );

    const randomColor = faker.number.int({ max: colors.length - 1 });

    await expect(async () => {
      await this.page.getByTestId(EMBED_SELECTORS.colorPickerTarget).click();

      await expect(this.page.locator(THEME_SELECTORS.colorPalette)).toBeVisible(
        { timeout: 10_000 }
      );
    }).toPass({ timeout: 60_000 });

    await expect(async () => {
      // eslint-disable-next-line playwright/no-nth-methods
      await colourPalleteItem.nth(randomColor).click(); // Required to select the random nth color from pallette

      await expect(
        this.page.getByTestId(
          COMMON_SELECTORS.customDropdownContainer(colors[randomColor])
        )
      ).toBeVisible();
    }).toPass({ timeout: 15_000 });

    shouldSaveChanges && (await this.saveChanges());

    return {
      index: randomColor,
      id:
        EVENT_COLORS[colors[randomColor]] ||
        EVENT_COLORS[COLOR_MAPPING[colors[randomColor]]],
    };
  };

  verifyActiveStatusInTable = (name: string, isActive: boolean = true) => {
    const icon = isActive ? "checkIcon" : "closeIcon";

    return expect(
      this.page
        .getByRole("row", { name })
        .getByTestId(COMMON_SELECTORS.neetoUiSwitch)
        .getByTestId(COMMON_SELECTORS[icon])
    ).toBeVisible();
  };

  enablePasswordProtection = async (password: string) => {
    await this.page.getByTestId(COMMON_SELECTORS.passwordSwitchLabel).click();

    await this.page
      .getByTestId(COMMON_SELECTORS.passwordInputField)
      .fill(password);
  };

  simulateHumanMouseMovement = async (
    customPageContext = this.page,
    durationMs = 5_000
  ) => {
    const { width, height } = customPageContext.viewportSize() || {
      width: 1280,
      height: 720,
    };
    const startTime = Date.now();
    let currentX = width / 2;
    let currentY = height / 2;

    while (Date.now() - startTime < durationMs) {
      const x = Math.floor(Math.random() * width);
      const y = Math.floor(Math.random() * height);

      const steps = 10 + Math.floor(Math.random() * 10);

      for (let i = 1; i <= steps; i++) {
        const stepX = currentX + ((x - currentX) * i) / steps;
        const stepY = currentY + ((y - currentY) * i) / steps;
        await customPageContext.mouse.move(stepX, stepY, { steps: 1 });

        await customPageContext.waitForTimeout(10 + Math.random() * 20); // small random delay
      }

      currentX = x;
      currentY = y;
    }
  };

  assertSelectedDateInCalendar = async (
    expectedDate: Dayjs,
    isPreview = false
  ) => {
    await this.waitForSlotsVisibility();

    await Promise.all([
      expect(this.page.getByTestId(EUI_SELECTORS.monthLabel)).toHaveText(
        expectedDate.format(FULL_MONTH_YEAR_FORMAT)
      ),
      expect(
        this.page.locator(
          BOOKING_SELECTORS.calendarCell(
            expectedDate.format(STANDARD_DATE_FORMAT)
          )
        )
      ).toHaveClass(COMMON_REGEXES.current),
      expect(
        this.page.getByTestId(BOOKING_SELECTORS.selectedDateAndDay)
      ).toHaveText(expectedDate.format(SELECTED_DATE_FORMAT)),
    ]);

    if (isPreview) {
      return;
    }

    const currentUrl = this.page.url();
    const urlParams = new URLSearchParams(new URL(currentUrl).search);

    expect(urlParams.get("month")).toBe(expectedDate.format(YEAR_MONTH_FORMAT));

    expect(urlParams.get("date")).toBe(
      expectedDate.format(STANDARD_DATE_FORMAT)
    );
  };

  pinMoreDropdownLinks = async (dropdownLinks = ["messages", "reports"]) => {
    const moreNavLinks = this.page.locator(MORE_SIDENAV_SELECTORS.moreNavLinks);
    await this.assertNeetoLogoVisibility();

    if (await moreNavLinks.isHidden()) {
      return;
    }

    const moreDropdownContainer = this.page.getByTestId(
      COMMON_SELECTORS.dropdownContainer
    );

    await this.toggleMoreDropdown();

    for (const link of dropdownLinks) {
      const linkSelector = MORE_SIDENAV_SELECTORS[link];
      const linkButton = moreDropdownContainer
        .locator(linkSelector)
        .getByRole("button");

      if (await linkButton.isHidden()) {
        continue;
      }

      await linkButton.click();

      await expect(
        this.page
          .getByTestId(COMMON_SELECTORS.sideBarWrapper)
          .locator(linkSelector)
      ).toBeVisible();
    }

    await this.toggleMoreDropdown(false);
  };

  assertNeetoLogoVisibility = () =>
    expect(this.page.getByTestId(COMMON_SELECTORS.neetoLogo)).toBeVisible({
      timeout: VISIBILITY_TIMEOUT,
    });

  toggleMoreDropdown = async (shouldOpen = true) => {
    await this.assertNeetoLogoVisibility();

    await expect(async () => {
      await this.page.locator(MORE_SIDENAV_SELECTORS.moreNavLinks).click();
      await expect(this.page.getByTestId(COMMON_SELECTORS.dropdownContainer))[
        shouldOpen ? "toBeVisible" : "toBeHidden"
      ]();
    }).toPass({ timeout: 20_000 });
  };

  getBaseUrl = (customPageContext = this.page) => {
    const currentUrl = customPageContext.url();
    const url = new URL(currentUrl);

    return url.origin;
  };

  operationToAction = (
    operation: () => Promise<void>,
    actionFxn: () => Promise<void>
  ) =>
    expect(async () => {
      await operation();

      await actionFxn();
    }).toPass({ timeout: 20_000 });

  toggleSidebarSublinks = (
    link = SIDEBAR_SELECTORS.bookings,
    sublink = SIDEBAR_SELECTORS.myBookings
  ) =>
    this.operationToAction(
      () => this.page.getByTestId(link).click(),
      () => expect(this.page.getByTestId(sublink)).toBeVisible()
    );

  poll = <T>(
    pollFn: () => Promise<T>,
    options: {
      timeout?: number;
      intervals?: number[];
    } = {}
  ) => {
    const {
      timeout = 3 * 60 * 1000,
      intervals = [1_000, 3_000, 7_000, 10_000, 15_000],
    } = options;

    // eslint-disable-next-line playwright/valid-expect
    return expect.poll(pollFn, { timeout, intervals });
  };

  closePane = async () => {
    await this.page.getByTestId(COMMON_SELECTORS.paneModalCrossIcon).click();
    await expect(this.page.getByTestId(COMMON_SELECTORS.paneBody)).toBeHidden();
  };

  closeSidebar = async () => {
    await this.page.getByTestId(COMMON_SELECTORS.sidebarToggle).click();
    await expect(this.page.getByTestId(COMMON_SELECTORS.sideBar)).toBeHidden();
  };

  selectTimezone = async (timezone: string, selector = "timezoneSelector") => {
    const timezoneSearchBox = this.page.getByTestId(
      COMMON_SELECTORS.searchBox(selector)
    );

    await expect(async () => {
      await this.page
        .getByTestId(COMMON_SELECTORS.selectButton(selector))
        .click();
      await expect(timezoneSearchBox).toBeVisible();
    }).toPass({ timeout: 15_000 });

    await timezoneSearchBox.pressSequentially(timezone);

    const hyphenatedTimezone = hyphenate(timezone);
    const timezoneRegex = new RegExp(`^${hyphenatedTimezone}`);
    await this.page.getByTestId(timezoneRegex).click();
  };

  resetToDefault = async (customPageContext = this.page) => {
    const resetToDefaultButton = customPageContext.getByTestId(
      COMMON_SELECTORS.resetToDefaultButton
    );

    await resetToDefaultButton.click();
    await this.saveChanges({ customPageContext });

    await expect(resetToDefaultButton).toBeHidden({
      timeout: VISIBILITY_TIMEOUT,
    });
  };

  setPageZoom = (zoom = 0.75) =>
    this.page.evaluate(zoomValue => {
      document.body.style.zoom = String(zoomValue);
    }, zoom);

  resetPageZoom = () => this.setPageZoom(1);
}
