import { findBy, hyphenate, isNotEmpty, noop } from "@bigbinary/neeto-cist";
import { faker } from "@faker-js/faker";
import {
  MailerUtils,
  currencyUtils,
  TAGS_SELECTORS,
  joinHyphenCase,
  NEETO_EDITOR_SELECTORS,
} from "@neetoplaywright";
import { Locator, Page, expect } from "@playwright/test";
import dayjs, { Dayjs } from "dayjs";
import { TFunction } from "i18next";
import { getI18nInstance } from "playwright-i18next-fixture";
import { isNotNil } from "ramda";

import {
  SchedulingLinkApis,
  BookingApis,
  BookingFormApis,
  PaymentsApis,
} from "@apis";
import {
  COMMON_REGEXES,
  DATE_AND_TIME_FILTERS,
  FILTER_DATE_FORMAT,
  FULL_DAY_MONTH_DAY_FORMAT,
  FULL_DAY_MONTH_ORDINAL_FORMAT,
  FULL_MONTH_NAME_FORMAT,
  MEETING_DATE_FILTERS,
  MESSAGE_DELIVERY_TIMEOUT,
  MONTH_YEAR_FORMAT,
  SLOT_TIME_FORMAT,
  STANDARD_DATE_FORMAT,
  VISIBILITY_TIMEOUT,
} from "@constants/common";
import {
  ApplyDiscountCodeProps,
  BookFirstAvailableSlotForRecurringMeetingsParams,
  BookFirstAvailableSlotParams,
  BookingCancelledEmailProps,
  BookingPageInitializerProps,
  CancelBookingViaClientUIProps,
  DeleteBookingViaUIProps,
  GetBookingDetailsViaRequestProps,
  NavigateAndFillRecurringMeetingCountProps,
  NavigateToApplyDiscountCodePageProps,
  NavigateToBookingDetailsPageProps,
  NavigateToBookingPageAndSubmitForm,
  RescheduleParams,
  SelectDateAndTimeProps,
  SelectDateProps,
  SelectTodaysDateProps,
  SwitchLanguageAtHostSideProps,
  VerifyAppliedValidDiscountCodeParams,
  VerifyBookingDetailsInCalendarView,
  VerifyBookingInCalendarViewProps,
  VerifyQuestionRequiredErrorProps,
  VerifySlotAvailabilityProps,
  ChangeLanguageAndVerifyInBookingFormProps,
  RescheduleSlotByTimeProps,
  VerifyReasonProps,
  SelectDateAndVerifyUnavailabilityProps,
  VerifyTaxDetailsInBookingDetailsProps,
  VerifyCancellationReasonProps,
  RescheduleViaAdminUIProps,
  VerifyPackageDetailsInBookingForm,
  VerifyRemainingSlotsOnEUIProps,
  VerifyAmountOverviewCardDetailsProps,
  AssertHelperDetailsAndTaxDetailsProps,
  GetBookingFormDetailsProps,
  CancelBookingViaAdminUIProps,
  VerifyUnavailabilityReasonProps,
  BookingDetailsTabs,
  OpenBookingFormProps,
  HostReminderEmailContentParams,
} from "@poms/types";
import { ROUTES } from "@routes";
import {
  BOOKING_SELECTORS,
  PAYMENT_SELECTORS,
  EUI_SELECTORS,
  MEETING_SELECTORS,
  QUESTIONS_SELECTORS,
  DISCOUNT_CODE_SELECTORS,
  SUBMISSIONS_TAB_SELECTORS,
  MESSAGE_SELECTORS,
  PAYMENTS_FILTERS_SELECTORS,
  COMMON_SELECTORS,
  ANTD_SELECTORS,
} from "@selectors";
import { PACKAGE_SELECTORS } from "@selectors/packages";
import { AVAILABILITY_TEXTS, BOOKING_TEXTS, PAYMENTS_TEXTS } from "@texts";
import {
  ClientDetails,
  CreateInstantBookingViaRequestProps,
  CreateValidBookingViaRequestProps,
  CustomBookingViaRequestPayload,
  EmailMessage,
  ParamFilters,
  Tab,
  TimeStamp,
  VerifyEmailsReceivedParams,
} from "@types";
import {
  meetingRoomURL,
  prependMinusSymbol,
  navigateToTargetMonth,
  buildBookingUrl,
  buildRescheduleUrl,
  getCurrentDateTime,
  formatTimeAsUtcOffset,
  getDateString,
  getCleanedEmailBody,
  appendQueryParams,
  getHostName,
  formatKolkataTimeWithOffset,
  buildTroubleshootUrl,
} from "@utils";
import BookingUtils from "@utils/booking";

import CommonUtils from "./commons/commands";
import FilterUtils from "./commons/filters";
import { PaymentsPage } from "./payments";
import SchedulingLinkPage from "./schedulingLink";

export default class BookingPage {
  page: Page;
  commonUtils: CommonUtils;
  t: TFunction;
  timezoneId: string;
  mailerUtils: MailerUtils;
  schedulingLinkPage: SchedulingLinkPage;
  bookingUtils: BookingUtils;
  bookingApis: BookingApis;
  schedulingLinkApis: SchedulingLinkApis;
  bookingFormApis: BookingFormApis;
  paymentsApis: PaymentsApis;
  callendarCell: Locator;
  paymentsPage: PaymentsPage;
  filterUtils: FilterUtils;

  constructor({
    page,
    commonUtils,
    timezoneId,
    mailerUtils,
  }: BookingPageInitializerProps) {
    this.page = page;
    this.commonUtils = commonUtils;
    this.t = getI18nInstance().t;
    this.timezoneId = timezoneId;
    this.mailerUtils = mailerUtils;
    this.schedulingLinkPage = new SchedulingLinkPage(page, commonUtils);
    this.bookingUtils = new BookingUtils(commonUtils);
    this.bookingApis = new BookingApis(commonUtils);
    this.schedulingLinkApis = new SchedulingLinkApis(commonUtils);
    this.bookingFormApis = new BookingFormApis(commonUtils);
    this.paymentsApis = new PaymentsApis(commonUtils);
    this.paymentsPage = new PaymentsPage(page, commonUtils);
    this.filterUtils = new FilterUtils(page, commonUtils);
  }

  calendarCell = (dateInStandardFormat: string) =>
    this.page
      .locator(BOOKING_SELECTORS.calendarCell(dateInStandardFormat))
      .locator(BOOKING_SELECTORS.calendarDayNumber);

  cancelBookingViaRequest = ({ accessToken }: { accessToken: string }) =>
    this.cancelBooking({ accessToken });

  deleteMeetingLinkAndAssociatedBookings = (name: string) =>
    Promise.all([
      this.cancelMultipleBookingsViaRequest({ name }),
      this.schedulingLinkPage.deleteMeetingViaRequest({ name }),
    ]);

  cancelMultipleBookingsViaRequest = async ({ name }: { name: string }) => {
    const accessToken = await this.getAccessTokens({ name });
    await Promise.all(
      accessToken.map(accessToken => this.cancelBooking({ accessToken }))
    );
  };

  cancelBooking = ({ accessToken }: { accessToken: string }) =>
    accessToken && this.bookingApis.cancel(accessToken);

  deleteBookingViaRequest = (bookingId: string) =>
    this.bookingApis.delete(bookingId);

  reschedule = async ({
    bookingSid,
    bookingDate,
    rescheduleReason,
    slotIndex,
  }: RescheduleParams) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildRescheduleUrl(bookingSid, bookingDate)
    );

    const rescheduledSlot = await this.selectDateAndTime({
      bookingDate,
      slotIndex: isNotNil(slotIndex)
        ? slotIndex
        : faker.number.int({ min: 2, max: 8 }),
    });

    rescheduleReason &&
      (await this.page
        .getByTestId(EUI_SELECTORS.reasonInput)
        .fill(rescheduleReason));

    await this.page.getByTestId(EUI_SELECTORS.submitBtn).click();
    await this.assertBookingContainer();

    return rescheduledSlot;
  };

  selectDateAndTime = async ({
    bookingDate,
    slotIndex = 0,
    customPageContext = this.page,
    shouldWaitForSlotsVisibility = true,
    shouldNavigateToMonth = true,
  }: SelectDateAndTimeProps) => {
    await this.selectDate({
      bookingDate,
      customPageContext,
      shouldWaitForSlotsVisibility,
      shouldNavigateToMonth,
    });

    return this.selectNthSlot(slotIndex, customPageContext);
  };

  selectNthSlot = async (slotIndex: number, customPageContext = this.page) => {
    const nthAvailableSlot = customPageContext
      .getByTestId(EUI_SELECTORS.availableSlotsList)
      // eslint-disable-next-line playwright/no-nth-methods
      .nth(slotIndex); // Gets the first available slot by default, with an option to choose nth slot.

    const nthAvailableSlotText = await nthAvailableSlot
      .getByTestId(EUI_SELECTORS.slotStartTime)
      .innerText();

    await nthAvailableSlot.click();
    await this.commonUtils.waitForPageLoad({ customPageContext });
    await expect(
      customPageContext.getByTestId(EUI_SELECTORS.scheduledMeetingDateTime)
    ).toBeVisible({ timeout: VISIBILITY_TIMEOUT });

    return nthAvailableSlotText as TimeStamp;
  };

  bookFirstAvailableSlotForTheMeeting = async ({
    slug,
    name,
    firstName,
    lastName,
    email,
    bookingDate,
    phone,
    slotIndex = 0,
    fillAdditionalDetails = noop,
    timezone = null,
    fillCardDetails = noop,
    shouldBookTheMeeting = true,
    skipPayment = false,
    customDuration,
  }: BookFirstAvailableSlotParams) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(slug, bookingDate)
    );

    customDuration && (await this.selectDurationAtClientSide(customDuration));

    if (timezone) {
      await this.commonUtils.selectTimezone(timezone, "timezone-selector");
    }

    const selectedSlot = await this.selectDateAndTime({
      bookingDate,
      slotIndex,
    });

    await this.page.waitForURL(new RegExp(ROUTES.public.booking.form), {
      timeout: VISIBILITY_TIMEOUT,
    });

    if (isNotNil(name)) {
      await this.page.getByTestId(EUI_SELECTORS.nameInput).fill(name);
    } else {
      await this.page
        .getByTestId(COMMON_SELECTORS.customInputField("first-name"))
        .fill(firstName);

      await this.page
        .getByTestId(COMMON_SELECTORS.customInputField("last-name"))
        .fill(lastName);
    }

    await this.page.getByTestId(EUI_SELECTORS.emailInput).fill(email);
    phone &&
      (await this.page.getByTestId(EUI_SELECTORS.phoneInput).fill(phone));

    await fillAdditionalDetails();

    await this.page.getByTestId(EUI_SELECTORS.submitBtn).click();
    await this.commonUtils.waitForPageLoad();

    await fillCardDetails();

    skipPayment &&
      (await this.page.getByTestId(EUI_SELECTORS.skipPaymentButton).click({
        timeout: VISIBILITY_TIMEOUT,
      }));

    shouldBookTheMeeting &&
      (await this.page.waitForURL(new RegExp(ROUTES.public.booking.finished), {
        timeout: VISIBILITY_TIMEOUT,
      }));

    return selectedSlot as TimeStamp;
  };

  bookFirstAvailableSlotForRecurringMeetings = async ({
    slug,
    bookingDate,
    recurringMeetingsInput,
    name,
    email,
    slotIndex = 0,
  }: BookFirstAvailableSlotForRecurringMeetingsParams) => {
    const selectedSlot = await this.navigateAndFillRecurringMeetingCount({
      slug,
      bookingDate,
      recurringMeetingsInput,
      slotIndex,
    });

    await this.page
      .getByRole("button", { name: this.t("buttons.continue") })
      .click();

    await this.page.waitForURL(new RegExp(ROUTES.public.booking.form));
    await this.commonUtils.fillAndSubmitClientDetails({ name, email });
    await this.page.waitForURL(new RegExp(ROUTES.public.booking.finished));

    await this.assertBookingContainer();

    return selectedSlot;
  };

  cancelFirstNBookings = async ({
    numberOfBookingsToCancel = 1,
    currentTotalBookings,
    tab = "upcoming",
    searchTerm,
  }: {
    numberOfBookingsToCancel?: number;
    currentTotalBookings: number;
    tab?: string;
    searchTerm: string;
  }) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      ROUTES.bookings.myBookings
    );

    await this.page
      .getByPlaceholder(this.t("bookings.placeholder.search"))
      .fill(searchTerm);

    await this.commonUtils.waitForUIComponentsToLoad();

    for (let i = 0; i < numberOfBookingsToCancel; i++) {
      await expect(
        this.page.getByRole("heading", {
          name: this.t("entity.bookingWithCount", {
            count: currentTotalBookings - i,
            entity: tab,
          }),
        })
      ).toBeVisible();

      await this.page
        .getByTestId(BOOKING_SELECTORS.bookingsTable)
        .locator(`[data-row-key="1"]`)
        .getByTestId(EUI_SELECTORS.bookingTime)
        .click();
      await this.page.getByTestId(BOOKING_SELECTORS.cancelMeetingLink).click();
      await this.page
        .getByTestId(BOOKING_SELECTORS.cancelMeetingSubmitButton)
        .click();
      await this.commonUtils.verifyToast();

      await expect(
        this.page.getByTestId(
          BOOKING_SELECTORS.bookingStatusHeader("cancelled")
        )
      ).toBeVisible();

      await this.commonUtils.closePane();
    }
  };

  cancelBookingViaAdminUI = async ({
    cancellationReason = faker.word.words(5),
    refundPercentage,
    hostName = getHostName(),
  }: CancelBookingViaAdminUIProps = {}) => {
    await this.assertBookingContainer();

    await this.page.getByTestId(BOOKING_SELECTORS.cancelMeetingLink).click();
    await this.commonUtils.waitForPageLoad();

    await this.page
      .getByTestId(BOOKING_SELECTORS.cancelMeetingInputField)
      .fill(cancellationReason);

    if (refundPercentage) {
      await this.page
        .getByTestId(COMMON_SELECTORS.checkboxInput("refund"))
        .click();

      await this.commonUtils.selectOptionFromDropdown({
        value: this.t("booking.show.cancelModal.custom"),
      });

      await this.page
        .getByTestId(
          QUESTIONS_SELECTORS.inputField(
            this.t("booking.show.refundModal.customPercentage")
          )
        )
        .click();

      await this.page.keyboard.type(String(refundPercentage));
    }

    await this.page
      .getByTestId(BOOKING_SELECTORS.cancelMeetingSubmitButton)
      .click();

    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.bookingStatusHeader("cancelled"))
    ).toContainText(this.t("booking.show.cancel.who", { name: hostName }), {
      timeout: 10_000,
    });

    await this.commonUtils.closePane();
  };

  cancelBookingViaClientUI = async ({
    cancellationReason,
    accessToken,
    baseURL = process.env.BASE_URL,
  }: CancelBookingViaClientUIProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      `${baseURL}/cancel/${accessToken}`
    );

    const cancelBookingButton = this.page.getByTestId(
      BOOKING_SELECTORS.cancelMeetingSubmitButton
    );

    await expect(cancelBookingButton).toBeVisible({
      timeout: 2 * VISIBILITY_TIMEOUT,
    });

    await this.page
      .getByTestId(BOOKING_SELECTORS.cancelMeetingField)
      .fill(cancellationReason);

    await cancelBookingButton.click();

    await expect(
      this.page.getByTestId(MEETING_SELECTORS.meetingCancelledText)
    ).toHaveText(this.t("booking.cancel.success"));
  };

  searchAndVerifyBookingsCount = async ({
    searchTerm,
    expectedCount = 1,
    tab = "upcoming",
  }: {
    searchTerm: string;
    expectedCount?: number;
    tab?: string;
  }) => {
    await this.commonUtils.waitForNeetoPageLoad();
    await this.page
      .getByPlaceholder(this.t("bookings.placeholder.search"))
      .fill(searchTerm);

    await this.commonUtils.waitForNeetoPageLoad();

    await this.commonUtils.switchTab({
      tabName: tab,
      tabItem: MEETING_SELECTORS.scheduledMeetingSideTab,
      customPageContext: this.page,
    });

    await expect(
      this.page.getByRole("heading", {
        name: this.t("entity.bookingWithCount", {
          count: expectedCount,
          entity: tab,
        }),
      })
    ).toBeVisible();
  };

  // Creates a booking by bypassing all validations from backend
  // and can be used to create custom bookings like past and incomplete bookings
  createCustomBookingViaRequest = async ({
    name,
    client,
    status = "confirmed",
    bookingDate,
    startTimeHour,
    duration = 30,
  }: CustomBookingViaRequestPayload) => {
    let attempt = 0;

    do {
      const finalStartTimeHour =
        startTimeHour ?? faker.number.int({ min: 9, max: 17 });

      const { startDateTime, endDateTime } =
        this.bookingUtils.bookingStartAndEndTime({
          bookingDate,
          startTimeHour: finalStartTimeHour,
          duration,
        });

      const booking = {
        ...client,
        starts_at: startDateTime,
        ends_at: endDateTime,
        status,
      };

      const bookingFormDetails = await this.getBookingFormDetails({
        clientName: client.name,
        clientEmail: client.email,
        meetingName: name,
      });

      const response = await this.bookingApis.createCustom({
        meeting_slug: joinHyphenCase(name),
        booking,
        neeto_form_response: bookingFormDetails,
      });

      if (response.status() === 200) {
        return response.json();
      }

      attempt += 1;
    } while (attempt <= 3);

    throw new Error("Failed to create a custom booking after 4 attempts.");
  };

  getBookingFormId = async (meetingName: string) => {
    const formResponse =
      await this.schedulingLinkApis.fetchBookingForm(meetingName);

    const {
      meeting: {
        form: { id: formId },
      },
    } = await formResponse.json();

    return formId;
  };

  getBookingFormDetails = async ({
    clientName,
    clientEmail,
    meetingName,
    isSMSReminderEnabled = false,
    retryCount = 0,
  }: GetBookingFormDetailsProps) => {
    const formId = await this.getBookingFormId(meetingName);
    const questionsResponse = await this.bookingFormApis.fetch(formId);
    const { questions: questionDetails } = await questionsResponse.json();

    const [{ id: nameQuestionId }, { id: emailQuestionId }] = questionDetails;

    const formResponses = [
      {
        question_id: nameQuestionId,
        value: { full_name: clientName },
        kind: "name",
      },
      {
        question_id: emailQuestionId,
        value: clientEmail,
        kind: "email",
      },
    ];

    if (isSMSReminderEnabled) {
      const smsReminderQuestion = findBy(
        { kind: "sms_reminder" },
        questionDetails
      ) as { id: string };

      if (!smsReminderQuestion && retryCount < 3) {
        // eslint-disable-next-line playwright/no-wait-for-timeout
        await this.page.waitForTimeout(2000); // Wait 2 seconds before retrying

        return this.getBookingFormDetails({
          clientName,
          clientEmail,
          meetingName,
          isSMSReminderEnabled,
          retryCount: retryCount + 1,
        });
      }

      if (smsReminderQuestion) {
        formResponses.push({
          question_id: smsReminderQuestion.id,
          value: process.env.TWILIO_INCOMING_PHONE_NUMBER,
          kind: "sms_reminder",
        });
      }
    }

    return { responses: formResponses };
  };

  createValidBookingViaRequest = async ({
    name,
    client,
    bookingDate,
    duration = 30,
    slotIndex = 0,
    slotTime,
    timezone = AVAILABILITY_TEXTS.asiaKolkata,
    retryCount = 0,
    preferredMeetingSpot = "jitsi",
    isSMSReminderEnabled = false,
  }: CreateValidBookingViaRequestProps & { retryCount?: number }) => {
    let startTimeInUTC: string;
    const bookingDateInStandardFormat =
      dayjs(bookingDate).format(STANDARD_DATE_FORMAT);

    if (slotTime) {
      startTimeInUTC = formatKolkataTimeWithOffset(
        `${bookingDateInStandardFormat} ${slotTime}`
      );
    } else {
      const { startTime } =
        await this.schedulingLinkPage.getSlotTimingsFromSlotIndexViaRequest({
          meetingName: name,
          bookingDate,
          slotIndex,
          timezone,
        });

      startTimeInUTC = formatTimeAsUtcOffset(
        `${bookingDateInStandardFormat} ${startTime}`
      );
    }

    const bookingFormDetails = await this.getBookingFormDetails({
      clientName: client.name,
      clientEmail: client.email,
      meetingName: name,
      isSMSReminderEnabled,
    });

    const bookingResponse = await this.bookingApis.createValid({
      meeting_slug: joinHyphenCase(name),
      booking: {
        duration,
        ...client,
        time_zone: timezone,
        starts_at: startTimeInUTC, // Convert the starting time to UTC with bookingDate
        preferred_meeting_spot: preferredMeetingSpot,
      },
      neeto_form_response: bookingFormDetails,
    });

    if (bookingResponse.status() !== 200) {
      if (retryCount < 3) {
        return this.createValidBookingViaRequest({
          name,
          client,
          bookingDate,
          duration,
          slotIndex,
          slotTime,
          preferredMeetingSpot,
          isSMSReminderEnabled,
          retryCount: retryCount + 1,
        });
      }

      throw new Error("Failed to create a valid booking after 4 attempts.");
    }

    return bookingResponse.json();
  };

  createInstantBookingViaRequest = async ({
    name,
    client,
    duration = 1,
    timezone = AVAILABILITY_TEXTS.asiaKolkata,
    retryCount = 0,
    preferredMeetingSpot = "jitsi",
    minutesAfter = 1,
    isSMSReminderEnabled = false,
  }: CreateInstantBookingViaRequestProps & { retryCount?: number }) => {
    const currentDateAndTime = getCurrentDateTime();
    const startTime = currentDateAndTime
      .subtract(5, "hours")
      .subtract(30, "minutes")
      .add(minutesAfter, "minute")
      .format(SLOT_TIME_FORMAT);

    const startTimeInUTC = formatTimeAsUtcOffset(
      `${currentDateAndTime.format(STANDARD_DATE_FORMAT)} ${startTime}`
    );

    const bookingFormDetails = await this.getBookingFormDetails({
      clientName: client.name,
      clientEmail: client.email,
      meetingName: name,
      isSMSReminderEnabled,
    });

    const bookingResponse = await this.bookingApis.createValid({
      meeting_slug: joinHyphenCase(name),
      booking: {
        duration,
        ...client,
        time_zone: timezone,
        starts_at: startTimeInUTC, // Convert the starting time to UTC with bookingDate
        preferred_meeting_spot: preferredMeetingSpot,
      },
      neeto_form_response: bookingFormDetails,
    });

    if (bookingResponse.status() !== 200) {
      if (retryCount < 3) {
        return this.createInstantBookingViaRequest({
          name,
          client,
          duration,
          preferredMeetingSpot,
          timezone,
          minutesAfter,
          retryCount: retryCount + 1,
        });
      }

      throw new Error("Failed to create an instant booking after 4 attempts.");
    }

    return bookingResponse.json();
  };

  getBookingDetailsViaRequest = ({
    name,
    tab = "upcoming",
  }: GetBookingDetailsViaRequestProps) => {
    const filters = [
      {
        conditions: [
          {
            type: "text",
            rule: "contains",
            node: "meeting.name",
            value: name,
            model: "Meeting",
            conditions_join_type: "or",
          },
        ],
      },
    ];

    return this.bookingApis.fetchAll(filters, tab);
  };

  verifyBookingDetails = (details: string[], isVisible = true) =>
    Promise.all(
      details.map(detail =>
        expect(this.page.getByRole("cell", { name: detail }))[
          isVisible ? "toBeVisible" : "toBeHidden"
        ]({ timeout: 10_000 })
      )
    );

  verifyBookingResponses = async (names: string[]) => {
    await expect(this.page.getByTestId(COMMON_SELECTORS.pageLoader)).toBeHidden(
      { timeout: 10000 }
    );

    await Promise.all(
      names.map(name =>
        expect(
          this.page
            .getByTestId(EUI_SELECTORS.bookingResponseContainer)
            .getByText(new RegExp(name, "i"), { exact: true })
        ).toBeVisible()
      )
    );
  };

  verifyClientDetails = async ({ name, email }: ClientDetails) => {
    await expect(
      this.page
        .getByTestId(EUI_SELECTORS.bookingResponseContainer)
        .filter({ hasText: name })
    ).toBeVisible();

    await expect(
      this.page.getByTestId(EUI_SELECTORS.bookingStatusDescription)
    ).toContainText(this.t("booking.show.confirmed.description", { email }));
  };

  verifyBookingCancelledEmail = async ({
    email,
    cancellationReason,
    meetingName,
  }: BookingCancelledEmailProps) => {
    const emailContent = await this.mailerUtils.findMessage(
      { to: email, subject: BOOKING_TEXTS.meetingCancelled(meetingName) },
      { timeout: MESSAGE_DELIVERY_TIMEOUT }
    );
    expect(getCleanedEmailBody(emailContent)).toContain(cancellationReason);

    return emailContent;
  };

  verifyDisabledMeetingLink = async (meetingName: string) => {
    const bookingPromise = this.page.waitForEvent("popup");
    await this.page
      .getByTestId(MEETING_SELECTORS.meetingLink(meetingName))
      .click();
    const bookingPage = await bookingPromise;
    await bookingPage.waitForLoadState();
    await expect(
      bookingPage.getByTestId(COMMON_SELECTORS.noDataTitle)
    ).toHaveText(this.t("live.booking.disabled.header"), {
      timeout: VISIBILITY_TIMEOUT,
    });

    await bookingPage.close();
  };

  navigateToApplyDiscountCodePage = async ({
    amount,
    currency,
    name,
    clientName,
    clientEmail,
    bookingDate,
    slotIndex = 0,
  }: NavigateToApplyDiscountCodePageProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(hyphenate(name), bookingDate)
    );

    const bookedSlot = await this.selectDateAndTime({
      bookingDate,
      slotIndex,
    });

    await this.commonUtils.fillAndSubmitClientDetails({
      name: clientName,
      email: clientEmail,
    });

    await this.page.waitForURL(new RegExp(`${hyphenate(name)}/book/payment`));
    await this.commonUtils.waitForPageLoad();

    await expect(
      this.page.getByTestId(EUI_SELECTORS.skipPaymentButton)
    ).toBeVisible({ timeout: VISIBILITY_TIMEOUT });

    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.paymentTitle)
    ).toHaveText(
      this.t("live.payment.helperText", {
        amount: currencyUtils.currencyFormat.withSymbol(
          Number(amount),
          currency,
          { minimumFractionDigits: 2 }
        ),
      })
    );

    return bookedSlot;
  };

  applyDiscountCode = async ({
    currency,
    name,
    clientName,
    clientEmail,
    bookingDate,
    discountCode,
    oldAmount,
    newAmount,
    slotIndex = 0,
  }: ApplyDiscountCodeProps) => {
    const bookedSlot = await this.navigateToApplyDiscountCodePage({
      name,
      clientName,
      clientEmail,
      bookingDate,
      slotIndex,
      amount: oldAmount,
      currency,
    });

    await this.fillAndSubmitDiscountCode(discountCode);
    await this.verifyAppliedValidDiscountCode({
      oldAmount,
      newAmount,
      currency,
      ...discountCode,
    });

    return bookedSlot;
  };

  verifyAppliedValidDiscountCode = async (
    {
      oldAmount,
      newAmount,
      currency,
      code,
    }: VerifyAppliedValidDiscountCodeParams,
    shouldVerifyAmountOverviewCardDetails = true
  ) => {
    const discountCodeSuccessCard = this.page.getByTestId(
      BOOKING_SELECTORS.discountCodeSuccessCard
    );

    const discountedAmount = Number(oldAmount) - Number(newAmount);
    const percentageValue = (
      (discountedAmount / Number(oldAmount)) *
      100
    ).toFixed(2);

    await Promise.all([
      expect(
        this.page.getByTestId(BOOKING_SELECTORS.paymentHelperText)
      ).toHaveText(
        this.t("live.payment.helperText", {
          amount: currencyUtils.currencyFormat.withSymbol(newAmount, currency, {
            minimumFractionDigits: 2,
          }),
        })
      ),
      expect(
        discountCodeSuccessCard.getByTestId(BOOKING_SELECTORS.appliedCode)
      ).toHaveText(this.t("live.payment.appliedCode", { code })),
      expect(
        discountCodeSuccessCard.getByTestId(BOOKING_SELECTORS.discountedAmount)
      ).toHaveText(
        this.t("live.payment.discountedAmount", {
          amount: currencyUtils.currencyFormat.withSymbol(
            discountedAmount,
            currency,
            { minimumFractionDigits: 2 }
          ),
          percentageValue: currencyUtils.currencyFormat.withAmount(
            Number(percentageValue),
            { minimumFractionDigits: 0 }
          ),
        })
      ),
    ]);

    shouldVerifyAmountOverviewCardDetails &&
      (await this.verifyAmountOverviewCardDetails({
        meetingFee: oldAmount,
        discountPercentage: Number(percentageValue),
        currency,
      }));
  };

  submitAndConfirmPayment = async () => {
    await this.paymentsPage.fillCardDetails();
    await this.confirmSuccessPageForPaidBooking();
  };

  confirmSuccessPageForPaidBooking = async () => {
    await this.paymentsPage.verifyPaymentStatusAtClientSide(
      this.t("live.payment.status", { status: "successful" })
    );

    await this.assertBookingContainer();
  };

  confirmCashPayment = async (bookingId?: string) => {
    isNotNil(bookingId) && (await this.navigateToBookingDetails(bookingId));

    await this.page
      .getByTestId(BOOKING_SELECTORS.confirmCashPaymentButton)
      .click();

    await this.commonUtils.clickSaveChangesButton(
      this.page.getByTestId(COMMON_SELECTORS.modalForever)
    );

    await this.commonUtils.verifyToast();
    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.paymentStatus)
    ).toHaveText(this.t("common.paid"));
  };

  assertBookingContainer = (customPageContext = this.page) =>
    expect(
      customPageContext.getByTestId(EUI_SELECTORS.bookingResponseContainer)
    ).toBeVisible({ timeout: VISIBILITY_TIMEOUT });

  fillAndSubmitDiscountCode = async ({ code }: { code: string }) => {
    const discountCodeButton = this.page.getByTestId(
      BOOKING_SELECTORS.discountCodeApplyButton
    );

    await this.page
      .getByTestId(DISCOUNT_CODE_SELECTORS.discountCodeInputField)
      .clear();

    await this.page
      .getByTestId(DISCOUNT_CODE_SELECTORS.discountCodeInputField)
      .pressSequentially(code, { delay: 10 });

    await expect(discountCodeButton).toBeEnabled();
    await this.commonUtils.bringElementIntoView(discountCodeButton);
    await discountCodeButton.click();

    await expect(
      this.page
        .getByTestId(BOOKING_SELECTORS.discountCodeApplyButton)
        .locator(COMMON_SELECTORS.buttonSpinner)
    ).toBeHidden({ timeout: 10_000 });
  };

  navigateToBookingDetailsPage = async ({
    name,
    date,
    tabName = this.t("bookings.tabs.upcoming"),
    expectedCount = 1,
  }: NavigateToBookingDetailsPageProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      ROUTES.bookings.allBookings
    );

    await this.commonUtils.switchTab({
      tabName,
      tabItem: MEETING_SELECTORS.scheduledMeetingSideTab,
    });

    await this.searchAndVerifyBookingsCount({
      searchTerm: name,
      tab: tabName,
      expectedCount,
    });

    await this.page
      .getByRole("row", { name })
      .getByRole("cell", { name: date })
      .getByRole("link")
      .click();

    await this.commonUtils.waitForPageLoad();
    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.bookingMeetingName)
    ).toHaveText(name, { timeout: VISIBILITY_TIMEOUT });
  };

  getAccessToken = async ({ name }: { name: string }) => {
    const response = await this.getBookingDetailsViaRequest({ name });
    const responseBody = await response.json();

    return responseBody.bookings[0]?.sid;
  };

  getBookingDetailsUsingSid = async (accessToken: string) => {
    const response = await this.bookingApis.fetch(accessToken);
    const responseBody = await response.json();

    return responseBody?.booking;
  };

  verifyRoomTypeAndURL = (
    bookingSid: string,
    roomURL: string = "jitsi.neetocal.com"
  ) =>
    this.commonUtils
      .poll(async () => {
        const bookingDetails = await this.getBookingDetailsUsingSid(bookingSid);

        return bookingDetails.room_url;
      })
      .toContain(roomURL);

  getPaymentIdentifiers = async (name: string) => {
    // both payment identifier and transaction identifier are same
    const accessTokens = await this.getAccessTokens({ name });
    const bookingDetails = (await Promise.all(
      accessTokens.map(this.getBookingDetailsUsingSid)
    )) as { payment: { identifier: string } }[];

    return bookingDetails.map(detail => detail?.payment?.identifier);
  };

  getAccessTokens = async ({ name }: { name: string }) => {
    const response = await this.getBookingDetailsViaRequest({ name });
    const responseBody = await response.json();

    return responseBody.bookings.map(booking => booking.sid);
  };

  selectDate = async ({
    bookingDate,
    customPageContext = this.page,
    shouldWaitForSlotsVisibility = true,
    shouldNavigateToMonth = true,
  }: SelectDateProps) => {
    await this.commonUtils.waitForPageLoad({ customPageContext });

    shouldWaitForSlotsVisibility &&
      (await this.commonUtils.waitForSlotsVisibility(customPageContext));

    if (shouldNavigateToMonth) {
      const newMonth = dayjs(bookingDate).format(FULL_MONTH_NAME_FORMAT);
      await navigateToTargetMonth({
        newMonth,
        page: customPageContext,
        isPreviousMonth: false,
      });
    }

    await customPageContext
      .locator(BOOKING_SELECTORS.calendarCell(bookingDate))
      .click();

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

  verifyQuestionRequiredError = async ({
    slug,
    bookingDate,
    name,
    email,
    errorSelector,
    min = "an",
    entity = this.t("neetoForm.questions.common.questionFields.field.option"),
    slotIndex = 0,
  }: VerifyQuestionRequiredErrorProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(slug, bookingDate)
    );

    await this.selectDateAndTime({ bookingDate, slotIndex });
    await this.commonUtils.fillAndSubmitClientDetails({ name, email });
    await expect(this.page.getByTestId(errorSelector)).toHaveText(
      this.t("neetoForm.error.selectMin", { min, entity })
    );
  };

  navigateToBookingPageAndSubmitForm = async ({
    meetingName,
    bookingDate,
    name,
    email,
    slotIndex = 0,
  }: NavigateToBookingPageAndSubmitForm) => {
    await this.openBookingForm({
      slug: hyphenate(meetingName),
      bookingDateInStandardFormat: bookingDate,
    });

    await this.selectDateAndTime({ bookingDate, slotIndex });
    await this.commonUtils.fillAndSubmitClientDetails({ name, email });
  };

  openBookingForm = async ({
    slug,
    bookingDateInStandardFormat,
    isTroubleshooting = false,
  }: OpenBookingFormProps) => {
    const url = isTroubleshooting
      ? buildTroubleshootUrl(slug, bookingDateInStandardFormat)
      : buildBookingUrl(slug, bookingDateInStandardFormat);

    await this.commonUtils.navigateAndWaitForPageLoad(url);

    await expect(this.page.getByTestId(EUI_SELECTORS.monthLabel)).toBeVisible({
      timeout: 20_000,
    });

    await this.commonUtils.waitForUIComponentsToLoad();
  };

  verifyFullDayUnavailability = async (
    slug: string,
    bookingDateInStandardFormat: string
  ) => {
    await this.openBookingForm({
      slug,
      bookingDateInStandardFormat,
      isTroubleshooting: true,
    });

    await this.assertFullDayUnavailabilityMessage(bookingDateInStandardFormat);
  };

  assertFullDayUnavailabilityMessage = (bookingDateInStandardFormat: string) =>
    expect(
      this.page.getByTestId(EUI_SELECTORS.emptyStateMessage)
    ).toContainText(
      this.t("live.typography.fullyUnavailable", {
        date: dayjs(bookingDateInStandardFormat).format(
          FULL_DAY_MONTH_DAY_FORMAT
        ),
      })
    );

  assertUnavailabilityOfNthSlot = (slotIndex = 0) =>
    expect(
      this.page
        .getByTestId(BOOKING_SELECTORS.startTimeSlotContainer)
        // eslint-disable-next-line playwright/no-nth-methods
        .nth(slotIndex) // To get the nth slot container
        .getByTestId(BOOKING_SELECTORS.slotStatus)
    ).toHaveText(this.t("live.troubleshoot.unavailable"), {
      timeout: 15_000,
    });

  verifyUnavailabilityForMonth = async (
    slug: string,
    bookingDateInStandardFormat: string
  ) => {
    await this.openBookingForm({ slug, bookingDateInStandardFormat });

    await expect(
      this.page.getByTestId(EUI_SELECTORS.viewNextMonthButton)
    ).toBeVisible();

    await Promise.any([
      // eslint-disable-next-line playwright/missing-playwright-await
      expect(
        this.page.getByTestId(EUI_SELECTORS.noSlotsAvailableAtTheMomentMessage)
      ).toHaveText(this.t("live.typography.noSlotsAvailableAtTheMoment")),
      // eslint-disable-next-line playwright/missing-playwright-await
      expect(
        this.page.getByTestId(EUI_SELECTORS.noSlotsAvailableText)
      ).toHaveText(
        this.t("live.typography.noSlots", {
          month: dayjs(bookingDateInStandardFormat).format(
            FULL_MONTH_NAME_FORMAT
          ),
        })
      ),
    ]);
  };

  selectTodaysDate = async ({
    placeholder,
    customPageContext = this.page,
  }: SelectTodaysDateProps) => {
    await customPageContext.getByPlaceholder(placeholder).click();
    await customPageContext
      .getByText(PAYMENTS_TEXTS.transactionExistTexts[0])
      .click();
  };

  navigateAndFillRecurringMeetingCount = async ({
    slug,
    bookingDate,
    recurringMeetingsInput,
    slotIndex = 0,
  }: NavigateAndFillRecurringMeetingCountProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(slug, bookingDate)
    );

    const selectedSlot = await this.selectDateAndTime({
      bookingDate,
      slotIndex,
    });

    await this.page.waitForURL(
      new RegExp(ROUTES.public.booking.recurringMeeting)
    );

    await this.page.getByTestId(BOOKING_SELECTORS.yesRadioLabel).click();
    await this.page
      .getByTestId(EUI_SELECTORS.preBookFrequencyInput)
      .fill(recurringMeetingsInput);

    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.reserveBookingSlot)
    ).toHaveCount(1);

    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.preBookingSlots)
    ).toHaveCount(Number(recurringMeetingsInput));

    return selectedSlot;
  };

  getMeetingHost = () =>
    this.page
      .getByTestId(EUI_SELECTORS.bookingHostNameContainer)
      .getByTestId(EUI_SELECTORS.bookingHostName)
      .innerText();

  verifySlotAvailability = async ({
    customPageContext = this.page,
    slug,
    bookingDate,
    isAvailable,
    slotStartTime,
  }: VerifySlotAvailabilityProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(slug, bookingDate),
      customPageContext
    );

    await this.selectDate({ bookingDate, customPageContext });

    const expectation = isAvailable ? "toBeVisible" : "toBeHidden";
    await expect(
      customPageContext
        .getByTestId(EUI_SELECTORS.availableSlotsList)
        .getByRole("button", { name: slotStartTime })
    )[expectation]();
  };

  switchLanguageAtHostSide = async ({
    currentLanguage,
    newLanguage,
    page = this.page,
  }: SwitchLanguageAtHostSideProps) => {
    const dropdownIcon = page.getByTestId(
      COMMON_SELECTORS.customDropdownIcon(currentLanguage)
    );

    const dropdownContainer = page.getByTestId(
      COMMON_SELECTORS.customDropdownContainer(currentLanguage)
    );

    await dropdownIcon.click();
    await expect(dropdownContainer).toBeVisible();
    await dropdownContainer.getByRole("button", { name: newLanguage }).click();

    await expect(dropdownContainer).toBeHidden();
    await expect(
      page.getByTestId(COMMON_SELECTORS.customDropdownIcon(newLanguage))
    ).toBeVisible();
  };

  changeLanguageAndVerifyAtClientSide = async ({
    currentLanguage = "English",
    questionDetails,
    page = this.page,
  }: ChangeLanguageAndVerifyInBookingFormProps) => {
    for (const { question, placeholder, language } of questionDetails) {
      currentLanguage !== language &&
        (await this.commonUtils.selectOptionFromDropdown({
          value: language,
          page,
        }));

      await Promise.all([
        expect(
          page.getByTestId(QUESTIONS_SELECTORS.fieldInputLabel(question))
        ).toBeVisible(),
        expect(
          page.getByTestId(QUESTIONS_SELECTORS.inputField(question))
        ).toHaveAttribute("placeholder", placeholder),
      ]);
    }
  };

  navigateAndSelectDate = async (preFilledLink: string, date: Dayjs) => {
    const standardDateFormat = dayjs(date).format(STANDARD_DATE_FORMAT);

    await this.commonUtils.navigateAndWaitForPageLoad(
      buildBookingUrl(preFilledLink, standardDateFormat)
    );

    await this.selectDateAndTime({ bookingDate: standardDateFormat });
  };

  switchToAssociatedBookingsTab = async (
    totalReservedSlots: number,
    isPane = false
  ) => {
    await this.page
      .getByTestId(
        BOOKING_SELECTORS.tabItem(this.t("booking.show.tabs.associated"))
      )
      .click();

    await Promise.all([
      expect(this.page.getByTestId(COMMON_SELECTORS.uiSpinner)).toHaveCount(0, {
        timeout: VISIBILITY_TIMEOUT,
      }),

      expect(
        this.page.getByTestId(BOOKING_SELECTORS.prebookingStatusDescription)
      ).toHaveCount(0, { timeout: VISIBILITY_TIMEOUT }),
    ]);

    const tagContainer = isPane
      ? this.page
          .getByTestId(COMMON_SELECTORS.paneBody)
          .getByTestId(TAGS_SELECTORS.tagContainer)
      : this.page.getByTestId(TAGS_SELECTORS.tagContainer);

    await expect(tagContainer).toHaveCount(totalReservedSlots, {
      timeout: 10_000,
    });
  };

  verifyAssociatedBookings = async (
    reservedSlots: string[],
    isPane = false
  ) => {
    await this.switchToAssociatedBookingsTab(reservedSlots.length, isPane);

    const associatedBookingCards = await this.page
      .getByTestId(BOOKING_SELECTORS.associatedBookingCard)
      .all();

    for (let index = 0; index < associatedBookingCards.length; index++) {
      const slotCard = associatedBookingCards[index];
      await Promise.all([
        expect(slotCard.getByRole("link")).toHaveText(reservedSlots[index]),
        expect(slotCard.getByTestId(TAGS_SELECTORS.tagContainer)).toHaveText(
          this.t("booking.show.associated.status.confirmed"),
          { ignoreCase: true }
        ),
      ]);
    }
  };

  verifyAssociatedBookingsCountViaRequest = (
    parentBookingSid: string,
    totalReservedSlots: number
  ) =>
    this.commonUtils
      .poll(() => this.getAssociatedBookings(parentBookingSid))
      .toHaveLength(totalReservedSlots);

  copyBookingURLAndVerify = async ({
    expectedURL,
    meetingName,
    date,
  }: {
    expectedURL: string;
    meetingName: string;
    date: Dayjs;
  }) => {
    await this.page
      .getByRole("row", { name: meetingName })
      .getByRole("cell", { name: getDateString(date) })
      .getByTestId(COMMON_SELECTORS.dropdownIcon)
      .click();

    await this.page.getByTestId(BOOKING_SELECTORS.copyBookingUrlButton).click();
    await this.commonUtils.verifyToast({
      message: BOOKING_TEXTS.copiedToastMessage,
    });

    const copiedBookingURL = await this.commonUtils.getClipboardContent();

    expect(copiedBookingURL).toStrictEqual(expectedURL);
  };

  deleteBookingViaUI = async ({
    meetingName,
    date,
    clientName,
  }: DeleteBookingViaUIProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      ROUTES.bookings.allBookings
    );

    await this.searchAndVerifyBookingsCount({
      searchTerm: meetingName,
    });

    await this.page
      .getByRole("row", { name: meetingName })
      .getByRole("cell", { name: date })
      .getByTestId(COMMON_SELECTORS.dropdownIcon)
      .click();

    await this.page
      .getByTestId(COMMON_SELECTORS.dropdownContainer)
      .getByTestId(BOOKING_SELECTORS.deleteBookingButton)
      .click();

    await this.proceedBookingDeletionFromModal(clientName);
    await this.commonUtils.waitForUIComponentsToLoad();

    await expect(
      this.page.getByText(this.t("emptyState.bookings.upcoming.title"))
    ).toBeVisible({ timeout: VISIBILITY_TIMEOUT });
  };

  proceedBookingDeletionFromModal = async (clientName?: string) => {
    const deleteBookingModalMessage = isNotNil(clientName)
      ? this.t("alert.bookings.cancelAndDelete.message", { clientName })
      : this.t("alert.bookings.cancelAndDelete.groupBooking.message");

    await Promise.all([
      expect(this.page.getByTestId(COMMON_SELECTORS.alertTitle)).toHaveText(
        this.t("alert.bookings.cancelAndDelete.title")
      ),
      expect(
        this.page.getByTestId(COMMON_SELECTORS.alertModalMessage)
      ).toHaveText(deleteBookingModalMessage),
    ]);

    await this.page
      .getByTestId(COMMON_SELECTORS.alertModalSubmitButton)
      .click();

    await this.commonUtils.verifyToast();
  };

  changeSlots = async (
    newDate: string,
    slotIndex: number = Number(faker.number.int({ min: 3, max: 12 }))
  ) => {
    const newMonth = dayjs(newDate).format(FULL_MONTH_NAME_FORMAT);

    await expect(
      this.page.getByText(
        this.t("live.preBook.list.checkingAvailability").slice(0, 25)
      )
    ).toHaveCount(0);

    const changeSlotButton = this.page.getByTestId(
      RegExp(BOOKING_SELECTORS.changeSlotButton)
    );

    let numberOfUnavailableSlots = await changeSlotButton.count();
    let newSlotIndex = slotIndex;

    while (numberOfUnavailableSlots--) {
      // eslint-disable-next-line playwright/no-nth-methods
      await changeSlotButton.first().click(); // Disabling no-nth-methods to get the first Change slot button
      await navigateToTargetMonth({ newMonth, page: this.page });

      await this.selectDateAndTime({
        bookingDate: newDate,
        slotIndex: newSlotIndex++,
      });
    }
  };

  verifyRefundAmountAndStatus = async ({
    refundedAmount,
    currency,
  }: {
    refundedAmount: number;
    currency: string;
  }) => {
    await Promise.all([
      expect(
        this.page
          .getByTestId(COMMON_SELECTORS.paneBody)
          .getByTestId(BOOKING_SELECTORS.refundAmount)
          .filter({
            hasText: currencyUtils.currencyFormat.withSymbol(
              refundedAmount,
              currency,
              { minimumFractionDigits: 2 }
            ),
          })
      ).toBeVisible(),
      expect(
        this.page
          .getByTestId(COMMON_SELECTORS.paneBody)
          .getByTestId(BOOKING_SELECTORS.refundStatus)
          .filter({ hasText: this.t("common.refunded") })
      ).toBeVisible(),
    ]);

    await this.commonUtils.closePane();
  };

  verifyBookingLimitReachedAtClientSide = async () => {
    const bookingPagePromise = this.page.waitForEvent("popup");
    await this.page.getByTestId(MEETING_SELECTORS.arrowIcon).click();
    const bookingPage = await bookingPagePromise;
    await bookingPage.waitForLoadState();
    await Promise.all(
      ["header", "helpText"].map(text =>
        expect(
          bookingPage
            .getByTestId(SUBMISSIONS_TAB_SELECTORS.noDataContainer)
            .getByText(this.t(`live.booking.bookingLimitReached.${text}`))
        ).toBeVisible({ timeout: VISIBILITY_TIMEOUT })
      )
    );
  };

  verifyTimeline = async (
    timelineExpectations: {
      status: string;
      date: Dayjs;
    }[]
  ) => {
    await this.page.getByTestId(BOOKING_SELECTORS.tabItem("timeline")).click();

    await Promise.all(
      timelineExpectations
        .map(({ status, date }, index) => {
          const formattedDate = date.format(MONTH_YEAR_FORMAT);
          const timelineCard = this.page
            .getByTestId(MESSAGE_SELECTORS.timelineCard)
            // eslint-disable-next-line playwright/no-nth-methods
            .nth(index); // Disabled no-nth-methods to get the timeline cards in relative order

          return [
            expect(timelineCard.getByText(status)).toBeVisible(),

            expect(timelineCard.getByText(formattedDate)).toBeVisible(),
          ];
        })
        .flat()
    );
  };

  verifyFiltersByPaymentTime = async ({
    meetingName,
    clientName,
    clientEmail,
    tab = "upcoming",
  }: {
    meetingName: string;
    clientName: string;
    clientEmail: string;
    tab?: Tab;
  }) => {
    const standardCurrentDate = getCurrentDateTime();
    const futureDate = standardCurrentDate.add(1, "day");
    const filteredCurrentDate = standardCurrentDate.format(FILTER_DATE_FORMAT);
    const filteredFutureDate = futureDate.format(FILTER_DATE_FORMAT);
    await this.commonUtils.switchTab({
      tabName: tab,
      tabItem: MEETING_SELECTORS.scheduledMeetingSideTab,
    });

    for (const dateFilter of MEETING_DATE_FILTERS.values()) {
      await this.filterUtils.filterByDate({
        label: PAYMENTS_FILTERS_SELECTORS.paymentTime,
        value: dateFilter,
        date: futureDate,
        dateFilter: PAYMENTS_FILTERS_SELECTORS.paymentTimeInputField,
      });

      dateFilter === DATE_AND_TIME_FILTERS.before
        ? await this.verifyBookingDetails([
            clientName,
            clientEmail,
            meetingName,
          ])
        : await expect(
            this.page.getByTestId(COMMON_SELECTORS.noDataTitle)
          ).toHaveText(this.t(`emptyState.bookings.${tab}.title`));
    }

    await this.filterUtils.filterByCustomDate({
      startDate: filteredCurrentDate,
      endDate: filteredFutureDate,
      label: PAYMENTS_FILTERS_SELECTORS.paymentTime,
    });

    await this.verifyBookingDetails([clientEmail, clientName, meetingName]);
    await this.filterUtils.clearFiltersFromActionBlock();
  };

  verifyBookingDetailsInCalendarView = async ({
    bookingDate,
    meetingName,
    clientName,
    clientEmail,
    currentUserName,
    tab = "upcoming",
  }: VerifyBookingDetailsInCalendarView) => {
    const bookingDateStandardFormat = bookingDate.format(STANDARD_DATE_FORMAT);
    await this.commonUtils.switchTab({
      tabName: tab,
      tabItem: MEETING_SELECTORS.scheduledMeetingSideTab,
      customPageContext: this.page,
    });

    await this.changeMonthInCalendarView(bookingDate);

    await this.searchAndVerifyBookingsCount({
      searchTerm: meetingName,
      tab,
    });

    await this.page
      .locator(BOOKING_SELECTORS.calendarCell(bookingDateStandardFormat))
      .getByText(meetingName)
      .click();

    await this.page.getByTestId(BOOKING_SELECTORS.tabItem("details")).click();

    currentUserName &&
      (await expect(
        this.page
          .getByTestId(EUI_SELECTORS.bookingHostNameContainer)
          .getByTestId(EUI_SELECTORS.bookingHostName)
      ).toHaveText(currentUserName));

    await this.verifyBookingResponses([clientName, clientEmail]);
    await this.commonUtils.closePane();
  };

  fetchAll = async (filters?: ParamFilters, tab?: Tab) => {
    const bookingsResponse = await this.bookingApis.fetchAll(filters, tab);

    return bookingsResponse.json();
  };

  fetchTotalNumberOfBookings = async (bookingType: Tab) => {
    const bookings = await this.fetchAll(undefined, bookingType);
    const { pagy_total_count: bookingCounts } = bookings;

    return Number(bookingCounts);
  };

  verifyBookingUnavailabilityInCalendarView = async (
    meetingName: string,
    bookingType: Tab
  ) => {
    await this.commonUtils.switchTab({
      tabName: bookingType,
      tabItem: MEETING_SELECTORS.scheduledMeetingSideTab,
      customPageContext: this.page,
    });

    (await this.fetchTotalNumberOfBookings(bookingType)) > 0 &&
      (await this.page
        .getByPlaceholder(this.t("bookings.placeholder.search"))
        .fill(meetingName));

    await this.commonUtils.waitForPageLoad();

    await expect(
      this.page.getByTestId(EUI_SELECTORS.noSlotsAvailableText)
    ).toHaveText(this.t(`emptyState.bookings.${bookingType}.title`));
  };

  changeMonthInCalendarView = async (bookingDate: Dayjs) => {
    const currentMonth = bookingDate.format("MMM YYYY");
    const datePicker = this.page.getByTestId(
      BOOKING_SELECTORS.calendarMonthPicker
    );

    const calendarMonth = await datePicker.innerText();

    if (currentMonth !== calendarMonth) {
      await datePicker.click();
      await this.page.locator(ANTD_SELECTORS.antPickerYearBtn).click();

      await this.page
        .locator(ANTD_SELECTORS.yearPanel)
        .getByTitle(bookingDate.format("YYYY"))
        .click();

      await this.page.getByTitle(bookingDate.format("YYYY-MM")).click();
      await expect(datePicker).toHaveText(currentMonth);
    }
  };

  verifyUnavailabilityAndAvailabilityInCalendarView = async ({
    bookingDetails,
    currentUserName,
    clientName,
    clientEmail,
    bookingDate,
    unavailabilityTab = "cancelled",
    availabilityTab = "upcoming",
  }: VerifyBookingInCalendarViewProps) => {
    await this.verifyBookingUnavailabilityInCalendarView(
      bookingDetails.name,
      unavailabilityTab
    );

    await this.verifyBookingDetailsInCalendarView({
      meetingName: bookingDetails.name,
      currentUserName,
      clientEmail,
      clientName,
      bookingDate,
      tab: availabilityTab,
    });
  };

  rescheduleSlotByTime = async ({
    bookingDate,
    customPageContext = this.page,
    startTime,
    rescheduleReason = faker.word.words(5),
    bookingId,
  }: RescheduleSlotByTimeProps) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      buildRescheduleUrl(bookingId, bookingDate),
      customPageContext
    );

    await this.selectDate({ bookingDate, customPageContext });
    await customPageContext
      .getByTestId(EUI_SELECTORS.slotsList)
      .getByTestId(EUI_SELECTORS.startTimeSlotButton)
      .filter({ hasText: startTime })
      .click();

    await customPageContext
      .getByTestId(EUI_SELECTORS.reasonInput)
      .fill(rescheduleReason);

    await customPageContext.getByTestId(EUI_SELECTORS.submitBtn).click();
  };

  byPass3DAuth = async (action: "Complete" | "Fail") => {
    const threeDAuthFrame = this.page
      .frameLocator(PAYMENT_SELECTORS.threeDS2)
      .locator(PAYMENT_SELECTORS.threeDIframe);

    await expect(threeDAuthFrame).toBeVisible({ timeout: VISIBILITY_TIMEOUT });

    await expect(async () => {
      await threeDAuthFrame
        .contentFrame()
        .getByRole("button", { name: action })
        .click();

      await expect(threeDAuthFrame).toBeHidden();
    }).toPass();
  };

  toggleTroubleshooting = async (customPageContext = this.page) => {
    await expect(
      customPageContext.getByTestId(BOOKING_SELECTORS.meetingNameHeaderTitle)
    ).toBeVisible({ timeout: 20_000 });

    const troubleshootingButton = customPageContext.getByTestId(
      BOOKING_SELECTORS.troubleshootingButton()
    );

    (await troubleshootingButton.isVisible()) &&
      (await troubleshootingButton.click());

    await this.commonUtils.waitForPageLoad({ customPageContext });
  };

  verifyUnavailabilityReason = async ({
    slotIndex,
    bookedSlot,
    reason,
    alternateReason = "",
  }: (
    | { slotIndex: number; bookedSlot?: never }
    | { slotIndex?: never; bookedSlot: string }
  ) & {
    reason: string;
    alternateReason?: string;
  }) => {
    await this.commonUtils.reloadAndWaitForPageLoad();

    await this.toggleTroubleshooting();

    await this.verifyUnavailabilityReasonInTroubleshoot({
      slotIndex,
      bookedSlot,
      reason,
      alternateReason,
    });
  };

  verifyUnavailabilityReasonInTroubleshoot = ({
    slotIndex,
    bookedSlot,
    reason,
    alternateReason = "",
  }: Partial<VerifyUnavailabilityReasonProps>) => {
    const slotStatus = this.page.getByTestId(
      BOOKING_SELECTORS.startTimeSlotContainer
    );

    const slotContainer =
      slotIndex === 0 || slotIndex
        ? slotStatus
            // eslint-disable-next-line playwright/no-nth-methods
            .nth(slotIndex) // Using nth method to get the nth slot container
        : slotStatus.filter({ hasText: new RegExp(`^${bookedSlot}`) });

    return this.verifyReason({
      slotContainer,
      reason,
      alternateReason,
    });
  };

  verifyReason = async ({
    slotContainer,
    reason,
    alternateReason = "",
    customPageContext = this.page,
  }: VerifyReasonProps) => {
    await expect(
      slotContainer.getByText(this.t("live.troubleshoot.unavailable"))
    ).toBeVisible();

    await this.commonUtils.verifyTooltip({
      triggerElement: slotContainer.getByTestId(
        COMMON_SELECTORS.helpPopoverButton
      ),
      content: RegExp([reason, alternateReason].join("|")),
      customPageContext,
    });
  };

  selectDateAndVerifyUnavailability = async ({
    meetingSlug,
    bookingDate,
    reason,
    slotIndex,
  }: SelectDateAndVerifyUnavailabilityProps) => {
    await this.openBookingForm({
      slug: meetingSlug,
      bookingDateInStandardFormat: bookingDate,
    });

    await this.verifyUnavailabilityReason({ slotIndex, reason });
  };

  verifyUnavailabilityOfBooking = () =>
    expect(this.page.getByTestId(COMMON_SELECTORS.noDataTitle)).toHaveText(
      this.t("emptyState.bookings.upcoming.title")
    );

  updateTransactions = async (meetingName: string) => {
    const transactionIdentifiers = (
      await this.getPaymentIdentifiers(meetingName)
    ).filter(Boolean);

    await Promise.all(
      transactionIdentifiers.map(transactionIdentifier =>
        expect
          .poll(
            async () => {
              const response = await this.paymentsApis.updateTransactions(
                transactionIdentifier
              );

              return response.status();
            },
            { timeout: 2 * 60 * 1000 }
          )
          .toBe(200)
      )
    );

    return transactionIdentifiers;
  };

  fillOTPAndProceed = async (email: string, expectedEmailCount = 1) => {
    const otpInputField = this.page.getByTestId(COMMON_SELECTORS.otpInputField);

    const otp = await this.mailerUtils.findOtpFromEmail({
      email,
      expectedEmailCount,
      receivedAfter: new Date(new Date().valueOf() - 60 * 60 * 1000),
      timeout: MESSAGE_DELIVERY_TIMEOUT,
    });

    await otpInputField.fill(otp);
    await this.commonUtils.clickOnButton(COMMON_SELECTORS.submitButton);
  };

  selectDurationAtClientSide = async (
    duration: number,
    customPageContext: Page = this.page
  ) => {
    const durationButton = this.getDurationButton(duration, customPageContext);

    await expect(async () => {
      await durationButton.click();
      await expect(durationButton).toHaveClass(COMMON_REGEXES.primary);
    }).toPass({ timeout: 10_000 });
  };

  getDurationButton = (duration: number, customPageContext: Page = this.page) =>
    customPageContext
      .getByTestId(BOOKING_SELECTORS.meetingDurationContainer)
      .getByRole("button", {
        name: this.t("meetings.duration.minutes", {
          entity: duration,
        }),
      });

  setMeetingOutcome = async (
    meetingName: string,
    outcome: string = BOOKING_TEXTS.noShow
  ) => {
    const paneHeader = this.page.getByTestId(COMMON_SELECTORS.paneHeader);
    const meetingOutcomeDropdown = this.page.getByTestId(
      BOOKING_SELECTORS.meetingOutcomeDropdown
    );

    await expect(paneHeader).toContainText(meetingName);
    await this.commonUtils.waitForPageLoad();

    await meetingOutcomeDropdown.click();
    await this.page
      .getByTestId(RegExp(BOOKING_SELECTORS.statusDropdownItem))
      .filter({ hasText: outcome })
      .click();

    await this.commonUtils.waitForPageLoad();
    await this.assertMeetingOutcome(outcome);
  };

  assertMeetingOutcome = (
    outcome: string = this.t("booking.show.meetingOutcome.placeholder")
  ) =>
    expect(
      this.page.getByTestId(BOOKING_SELECTORS.meetingOutcomeDropdown)
    ).toContainText(outcome);

  verifyTheApprovalRequiredCallout = () =>
    Promise.all([
      expect(
        this.page.getByTestId(EUI_SELECTORS.bookingStatusTitle)
      ).toHaveText(
        this.t("live.booking.statuses.title", {
          status: this.t("live.booking.statuses.awaiting_approval"),
        })
      ),
      expect(
        this.page.getByTestId(EUI_SELECTORS.bookingStatusDescription)
      ).toHaveText(this.t("live.booking.statuses.awaitingApprovalDescription")),
    ]);

  rejectTheBooking = async (meetingName: string, rejectionReason?: string) => {
    await this.assertBookingContainer();

    await expect(
      this.page.getByTestId(COMMON_SELECTORS.paneHeader)
    ).toContainText(meetingName);

    const bookingRejectButton = this.page.getByTestId(
      BOOKING_SELECTORS.bookingRejectButton
    );
    await bookingRejectButton.click();

    await expect(
      this.page.getByTestId(COMMON_SELECTORS.modalHeader)
    ).toHaveText(this.t("booking.show.rejectModal.title"));

    rejectionReason &&
      (await this.page
        .getByTestId(BOOKING_SELECTORS.cancelMeetingModalInput)
        .fill(rejectionReason));

    await this.page
      .getByTestId(BOOKING_SELECTORS.rejectMeetingSubmitButton)
      .click();

    await this.commonUtils.verifyToast();
    await expect(bookingRejectButton).toBeHidden();
  };

  acceptTheBooking = async () => {
    const bookingAcceptButton = this.page.getByTestId(
      BOOKING_SELECTORS.bookingAcceptButton
    );
    await bookingAcceptButton.click();

    await this.commonUtils.verifyToast();
    await expect(bookingAcceptButton).toBeHidden();
  };

  waitUntilSchedulingLinkIsGenerated = async (
    customPageContext = this.page
  ) => {
    const joinCallLink = customPageContext.getByTestId(
      EUI_SELECTORS.joinCallLink
    );

    await expect(async () => {
      await this.commonUtils.navigateAndWaitForPageLoad(
        customPageContext.url(),
        customPageContext
      );

      await this.assertBookingContainer(customPageContext);
      await expect(joinCallLink).toBeVisible();
    }).toPass({ timeout: 7 * VISIBILITY_TIMEOUT });

    return joinCallLink.getAttribute("href");
  };

  verifyTaxDetailsInBookingDetails = ({
    meetingFee,
    totalAmount,
    meetingCurrency,
    taxNames,
  }: VerifyTaxDetailsInBookingDetailsProps) =>
    Promise.all([
      expect(this.page.getByTestId(BOOKING_SELECTORS.paymentAmount)).toHaveText(
        currencyUtils.currencyFormat.withSymbol(totalAmount, meetingCurrency, {
          minimumFractionDigits: 2,
        })
      ),

      Promise.all(
        taxNames.map(taxName =>
          expect(
            this.page.getByTestId(BOOKING_SELECTORS.bookingTaxAmount)
          ).toContainText(taxName)
        )
      ),
      expect
        .soft(this.page.getByTestId(BOOKING_SELECTORS.bookingTaxAmount))
        .toContainText(
          currencyUtils.currencyFormat.withSymbol(
            totalAmount - meetingFee,
            meetingCurrency.toUpperCase(),
            { minimumFractionDigits: 2 }
          )
        ),
    ]);

  addInternalNotesViaUI = async (internalNote: string) => {
    const paneBody = this.page.getByTestId(COMMON_SELECTORS.paneBody);
    const contentField = paneBody.getByTestId(
      NEETO_EDITOR_SELECTORS.contentField
    );

    await contentField.fill(internalNote);

    const saveChangesButton = paneBody.getByTestId(
      COMMON_SELECTORS.saveChangesButton
    );

    await saveChangesButton.click();

    await expect(
      saveChangesButton.getByTestId(COMMON_SELECTORS.uiSpinner)
    ).toBeHidden({ timeout: 30_000 });

    await expect(contentField).toHaveText(internalNote);
  };

  getAssociatedBookings = async (bookingSid: string) => {
    const bookingResponse = await this.bookingApis.fetchAssociated(bookingSid);

    const { associated_bookings: associatedBookings } =
      await bookingResponse.json();

    return associatedBookings;
  };

  addAnotherSlotInRecurringMeeting = async (
    weekday: string,
    slotTime: TimeStamp,
    slotInputIndex = 0
  ) => {
    await this.page.getByTestId(EUI_SELECTORS.addAnotherSlotButton).click();

    await this.commonUtils.selectOptionFromDropdown({
      label: EUI_SELECTORS.selectDayLabel(slotInputIndex),
      value: weekday,
    });

    await this.commonUtils.selectOptionFromDropdown({
      label: EUI_SELECTORS.selectTimeLabel(slotInputIndex),
      value: slotTime,
    });
  };

  verifyCancellationReason = async ({
    bookingSid,
    cancelledBy,
    cancellationReason,
  }: VerifyCancellationReasonProps) => {
    await expect(async () => {
      await this.navigateToBookingDetails(bookingSid);

      await Promise.all([
        expect(
          this.page.getByTestId(
            BOOKING_SELECTORS.bookingStatusHeader("cancelled")
          )
        ).toContainText(
          this.t("booking.show.cancel.who", { name: cancelledBy }),
          { timeout: 10_000 }
        ),
        expect(
          this.page.getByTestId(BOOKING_SELECTORS.bookingCancelReason)
        ).toContainText(cancellationReason, { ignoreCase: true }),
      ]);
    }).toPass({ timeout: 5 * 60 * 1000 });
  };

  rescheduleMeeting = async ({
    bookingDate,
    customPageContext = this.page,
    slotIndex = 1,
    rescheduleReason = faker.word.words(5),
  }: RescheduleSlotByTimeProps) => {
    await this.selectDateAndTime({
      bookingDate,
      slotIndex,
      customPageContext,
    });

    await customPageContext
      .getByTestId(EUI_SELECTORS.reasonInput)
      .fill(rescheduleReason);

    await customPageContext.getByTestId(EUI_SELECTORS.submitBtn).click();
  };

  verifyRemainingSlotsOnEUI = async ({
    meetingName,
    bookingDate,
    slot,
    remainingSlots,
  }: VerifyRemainingSlotsOnEUIProps) => {
    const standardBookingDateFormat = bookingDate.format(STANDARD_DATE_FORMAT);
    await this.openBookingForm({
      slug: hyphenate(meetingName),
      bookingDateInStandardFormat: standardBookingDateFormat,
    });

    await this.selectDate({ bookingDate: standardBookingDateFormat });
    await this.assertRemainingSlots(slot, remainingSlots);
    await this.toggleTroubleshooting();
    await this.commonUtils.waitForSlotsVisibility();

    await expect(
      this.page
        .getByTestId(BOOKING_SELECTORS.startTimeSlotContainer)
        .filter({ hasText: slot })
    ).toContainText(
      this.t("live.numberOfSlotsLeft", { count: remainingSlots })
    );
  };

  assertRemainingSlots = (
    slot: TimeStamp,
    remainingSlots: number,
    customPageContext: Page = this.page
  ) =>
    expect(
      customPageContext
        .getByTestId(EUI_SELECTORS.startTimeSlotButton)
        .filter({ hasText: slot })
    ).toContainText(
      this.t("live.numberOfSlotsLeft", { count: remainingSlots })
    );

  requestToRescheduleViaUI = async () => {
    await this.commonUtils.performActionFromMoreDropdown(
      this.page.getByTestId(COMMON_SELECTORS.paneHeader),
      this.t("booking.show.requestToReschedule.title")
    );

    await Promise.all([
      expect(this.page.getByTestId(COMMON_SELECTORS.modalHeader)).toHaveText(
        this.t("booking.show.requestToReschedule.modal.title")
      ),
      expect(this.page.getByTestId(COMMON_SELECTORS.modalBody)).toContainText(
        this.t("booking.show.requestToReschedule.modal.message")
      ),
    ]);

    await this.commonUtils.saveChanges();
  };

  resendConfirmation = async () => {
    await this.commonUtils.performActionFromMoreDropdown(
      this.page.getByTestId(COMMON_SELECTORS.paneHeader),
      this.t("booking.show.resendConfirmation.title")
    );

    await Promise.all([
      expect(this.page.getByTestId(COMMON_SELECTORS.modalHeader)).toHaveText(
        this.t("booking.show.resendConfirmation.modal.title")
      ),
      expect(this.page.getByTestId(COMMON_SELECTORS.modalBody)).toHaveText(
        this.t("booking.show.resendConfirmation.modal.description")
      ),
    ]);

    await this.page
      .getByTestId(BOOKING_SELECTORS.resendConfirmationSubmitButton)
      .click();

    await this.commonUtils.verifyToast();
  };

  changeHostOfBooking = async (newHostEmail: string) => {
    await this.assertBookingContainer();

    await this.commonUtils.performActionFromMoreDropdown(
      this.page.getByTestId(COMMON_SELECTORS.paneHeader),
      this.t("booking.show.changeHost.title")
    );

    await Promise.all(
      ["title", "description"].map(key =>
        expect(
          this.page.getByTestId(COMMON_SELECTORS.modalHeader)
        ).toContainText(this.t(`booking.show.changeHost.modal.${key}`))
      )
    );

    await this.commonUtils.selectOptionFromDropdown({
      label: this.t("common.host"),
      value: newHostEmail,
    });

    await this.page
      .getByTestId(BOOKING_SELECTORS.resendConfirmationSubmitButton)
      .click();

    await this.commonUtils.verifyToast({
      timeout: VISIBILITY_TIMEOUT,
    });

    await expect(
      this.page
        .getByTestId(EUI_SELECTORS.bookingHostNameContainer)
        .getByTestId(EUI_SELECTORS.bookingHostName)
    ).toHaveText(newHostEmail);
  };

  rescheduleViaAdminUI = async ({
    rescheduleReason = faker.word.words(5),
    bookingId,
    newDateInStandardFormat,
    newSlotIndex = faker.number.int({ min: 2, max: 8 }),
  }: RescheduleViaAdminUIProps) => {
    const reschedulePromise = this.page.waitForEvent("popup");
    await this.page
      .getByTestId(BOOKING_SELECTORS.rescheduleMeetingLink)
      .click();

    const reschedulePage = await reschedulePromise;
    await reschedulePage.waitForLoadState();

    await this.commonUtils.navigateAndWaitForPageLoad(
      buildRescheduleUrl(bookingId, newDateInStandardFormat),
      reschedulePage
    );

    await this.selectDateAndTime({
      bookingDate: newDateInStandardFormat,
      slotIndex: newSlotIndex,
      customPageContext: reschedulePage,
    });

    await reschedulePage
      .getByTestId(EUI_SELECTORS.reasonInput)
      .fill(rescheduleReason);

    await reschedulePage.getByTestId(EUI_SELECTORS.submitBtn).click();

    await this.assertBookingContainer(reschedulePage);

    await reschedulePage.close();
  };

  verifyPackageDetailsInBookingForm = async ({
    meetingName,
    bookingDate,
    packageName,
    shouldBeVisible = true,
  }: VerifyPackageDetailsInBookingForm) => {
    const assertion = shouldBeVisible ? "toBeVisible" : "toBeHidden";

    await this.navigateAndSelectDate(hyphenate(meetingName), bookingDate);
    await expect(
      this.page.getByTestId(EUI_SELECTORS.meetingNameHeader)
    ).toHaveText(meetingName, { timeout: 20_000 });

    shouldBeVisible &&
      (await expect(
        this.page.getByTestId(PACKAGE_SELECTORS.buyPackageHeading)
      ).toHaveText(this.t("live.booking.buyPackage")));

    await expect(
      this.page.getByTestId(PACKAGE_SELECTORS.packageItem(packageName))
    )[assertion]();
  };

  verifyEmailsReceived = async ({
    emails,
    subject,
    timeout = MESSAGE_DELIVERY_TIMEOUT,
  }: VerifyEmailsReceivedParams): Promise<EmailMessage[]> => {
    const messages = await Promise.all(
      emails.map(email =>
        this.mailerUtils.findMessage({ to: email, subject }, { timeout })
      )
    );

    messages.forEach(message => expect(message.subject).toContain(subject));

    return messages as unknown as EmailMessage[];
  };

  addTip = async (tipValue: 18 | 22 = 18) => {
    await expect(
      this.page.getByTestId(EUI_SELECTORS.addTipContainer)
    ).toBeVisible({ timeout: 10_000 });

    await this.page
      .getByTestId(EUI_SELECTORS.defaultTipValueButton)
      .filter({ hasText: String(tipValue) })
      .click();

    await expect(this.page.getByTestId(EUI_SELECTORS.tipAmount)).toBeVisible();
  };

  verifyAmountOverviewCardDetails = async ({
    meetingFee,
    discountPercentage = 0,
    tipPercentage = 0,
    taxPercentage = 0,
    currency = "USD",
    shouldApplyTaxOnTip = false,
  }: VerifyAmountOverviewCardDetailsProps) => {
    const discountAmount = (meetingFee * discountPercentage) / 100;
    const meetingFeeAfterDiscount = meetingFee - discountAmount;
    const tipAmount = (meetingFeeAfterDiscount * tipPercentage) / 100;
    const taxOnAmount = (meetingFeeAfterDiscount * taxPercentage) / 100;
    const taxOnTip = shouldApplyTaxOnTip
      ? (tipAmount * taxPercentage) / 100
      : 0;

    const totalTaxAmount = taxOnAmount + taxOnTip;

    const totalPayableAmount =
      meetingFeeAfterDiscount + tipAmount + totalTaxAmount;

    await expect
      .soft(this.page.getByTestId(EUI_SELECTORS.meetingFee))
      .toHaveText(
        currencyUtils.currencyFormat.withSymbol(meetingFee, currency, {
          minimumFractionDigits: 2,
        })
      );

    isNotEmpty(discountAmount) &&
      (await expect
        .soft(this.page.getByTestId(EUI_SELECTORS.discountAmount))
        .toHaveText(
          prependMinusSymbol(
            currencyUtils.currencyFormat.withSymbol(discountAmount, currency, {
              minimumFractionDigits: 2,
            })
          )
        ));

    tipAmount !== 0 &&
      (await expect
        .soft(this.page.getByTestId(EUI_SELECTORS.tipAmount))
        .toHaveText(
          currencyUtils.currencyFormat.withSymbol(tipAmount, currency, {
            minimumFractionDigits: 2,
          })
        ));

    taxPercentage !== 0 &&
      (await expect
        .soft(this.page.getByTestId(EUI_SELECTORS.taxAmount))
        .toHaveText(
          currencyUtils.currencyFormat.withSymbol(totalTaxAmount, currency, {
            minimumFractionDigits: 2,
          })
        ));

    await expect
      .soft(this.page.getByTestId(EUI_SELECTORS.totalPayableAmount))
      .toHaveText(
        currencyUtils.currencyFormat.withSymbol(totalPayableAmount, currency, {
          minimumFractionDigits: 2,
        })
      );

    if (shouldApplyTaxOnTip) {
      await this.commonUtils.verifyHelpPopover({
        triggerElement: this.page
          .getByTestId(EUI_SELECTORS.taxesLabelContainer)
          .getByTestId(COMMON_SELECTORS.helpPopoverButton),
        content: this.t("live.payment.taxOnAmountHelpText", {
          taxAmount: currencyUtils.currencyFormat.withSymbol(
            taxOnAmount,
            currency,
            { minimumFractionDigits: 2 }
          ),
        }),
      });

      await this.commonUtils.verifyHelpPopover({
        triggerElement: this.page
          .getByTestId(EUI_SELECTORS.taxesLabelContainer)
          .getByTestId(COMMON_SELECTORS.helpPopoverButton),
        content: this.t("live.payment.taxOnTipHelpText", {
          taxAmount: currencyUtils.currencyFormat.withSymbol(
            taxOnTip,
            currency,
            { minimumFractionDigits: 2 }
          ),
        }),
      });
    }

    await expect
      .soft(this.page.getByTestId(BOOKING_SELECTORS.paymentHelperText))
      .toHaveText(
        this.t("live.payment.helperText", {
          amount: currencyUtils.currencyFormat.withSymbol(
            totalPayableAmount,
            currency,
            { minimumFractionDigits: 2 }
          ),
        })
      );

    return totalPayableAmount;
  };

  assertHelperDetailsAndTaxDetails = async ({
    amount,
    currency,
    taxDetails,
  }: AssertHelperDetailsAndTaxDetailsProps) => {
    await expect
      .soft(this.page.getByTestId(MEETING_SELECTORS.meetingFeeText))
      .toContainText(taxDetails.name);

    await expect(
      this.page.getByTestId(BOOKING_SELECTORS.paymentHelperText)
    ).toHaveText(
      this.t("live.payment.helperText", {
        amount: currencyUtils.currencyFormat.withSymbol(
          amount + (amount * taxDetails.taxValue) / 100,
          currency,
          { minimumFractionDigits: 2 }
        ),
      })
    );
  };

  verifyHostReminderEmailContent = ({
    hostReminderEmail,
    meetingName,
    bookingSid,
    clientName,
    clientEmail,
    bookingDate,
    baseURL,
  }: HostReminderEmailContentParams) => {
    expect(hostReminderEmail.subject).toContain(
      BOOKING_TEXTS.reminderSubject(meetingName)
    );

    const expectedBodyContent = [
      bookingDate.format(FULL_DAY_MONTH_ORDINAL_FORMAT),
      clientName,
      clientEmail.toLowerCase(),
      meetingName,
      meetingRoomURL(bookingSid, baseURL),
      bookingSid,
      BOOKING_TEXTS.reminderMessage,
    ];

    expectedBodyContent.forEach(content => {
      expect(hostReminderEmail.text.body).toContain(content);
    });
  };

  navigateToBookingDetails = async (
    bookingSid: string,
    customPageContext = this.page,
    tab: BookingDetailsTabs = "details"
  ) => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      appendQueryParams(ROUTES.bookings.details(bookingSid), { tab }),
      customPageContext
    );

    tab === "details" && (await this.assertBookingContainer(customPageContext));
  };
}
