import { findBy, keysToSnakeCase } from "@bigbinary/neeto-cist";
import { COMMON_SELECTORS } from "@neetoplaywright";
import { expect, Page } from "@playwright/test";
import { TFunction } from "i18next";
import { getI18nInstance } from "playwright-i18next-fixture";

import { AvailabilityApis } from "@apis";
import { ROUTES } from "@constants/routes";
import {
  AddBufferTimeProps,
  ApplyAvailabilityParams,
  AvailabilityDetails,
  CreateAvailabilityViaRequestProps,
} from "@poms/types";
import {
  AllDaysType,
  AVAILABILITY_SELECTORS,
  HOST_SECTION_SELECTORS,
} from "@selectors";
import { ALL_DAYS, FULL_AVAILABILITY, WEEKENDS, WORKING_DAYS } from "@texts";

import CommonUtils from "./commons/commands";
import SchedulingLinkPage from "./schedulingLink";
import { TeamMembersPage } from "./teamMembers";

export default class AvailabilityPage {
  page: Page;
  t: TFunction;
  schedulingLinkPage: SchedulingLinkPage;
  teamMembersPage: TeamMembersPage;
  availabilityApis: AvailabilityApis;

  constructor(
    page: Page,
    public commonUtils: CommonUtils
  ) {
    this.page = page;
    this.commonUtils = commonUtils;
    this.t = getI18nInstance().t;
    this.teamMembersPage = new TeamMembersPage(page, commonUtils);
    this.schedulingLinkPage = new SchedulingLinkPage(page, commonUtils);
    this.availabilityApis = new AvailabilityApis(commonUtils);
  }

  createAvailabilityViaRequest = async ({
    name,
    isDefault = true,
    customAvailability,
    retryCount = 4,
  }: CreateAvailabilityViaRequestProps) => {
    const response = await this.availabilityApis.create({
      name,
      periodsAttributes: customAvailability || FULL_AVAILABILITY,
      default: isDefault,
    });

    if (retryCount-- && response.status() !== 200) {
      return this.createAvailabilityViaRequest({
        name,
        isDefault,
        customAvailability,
        retryCount,
      });
    }

    const { availability_sid: availabilityId } = await response.json();

    return availabilityId;
  };

  getAvailabilityId = async (name: string) => {
    const response = await this.availabilityApis.fetch();
    const responseBody = await response.json();
    const { id } = findBy<unknown, { id: string }>(
      { name },
      responseBody.availabilities
    );

    return id;
  };

  getAvailabilitySid = async (name: string) => {
    const response = await this.availabilityApis.fetch();
    const responseBody = await response.json();
    const availability = findBy<unknown, { sid: string }>(
      { name },
      responseBody.availabilities
    );

    return availability?.sid;
  };

  deleteAvailabilityViaRequest = async (name: string) => {
    const availabilitySid = await this.getAvailabilitySid(name);

    availabilitySid && (await this.availabilityApis.delete(availabilitySid));
  };

  getDefaultAvailabilityId = async (baseURL = "") => {
    const response = await this.availabilityApis.fetchDefault(baseURL);
    const {
      availability: { id: availabilityId },
    } = await response.json();

    return availabilityId;
  };

  navigateToAvailabilitiesPage = async () => {
    await this.commonUtils.navigateAndWaitForPageLoad(
      ROUTES.host.self.meetings.availabilities.index
    );

    await expect(this.page.getByTestId(COMMON_SELECTORS.heading)).toHaveText(
      this.t("availabilities.title")
    );
  };

  openNewAvailabilityPane = async () => {
    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.newAvailabilityButton)
      .click();

    await expect(this.page.getByTestId(COMMON_SELECTORS.paneHeader)).toHaveText(
      this.t("availability.newAvailability")
    );
  };

  addAvailabilityViaUI = async ({ availabilityName }) => {
    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.nameInputField)
      .fill(availabilityName);

    await Promise.all([
      this.page
        .getByTestId(AVAILABILITY_SELECTORS.availabilityPaneSubmitButton)
        .click(),
      this.commonUtils.verifyToast(),
    ]);
  };

  addBufferTime = async ({
    startTime,
    endTime,
    customPageContext = this.page,
  }: AddBufferTimeProps) => {
    await customPageContext
      .getByTestId(AVAILABILITY_SELECTORS.bufferTimeDropdown)
      .click();

    await customPageContext
      .getByTestId(AVAILABILITY_SELECTORS.bufferTimeEditLink)
      .click();

    startTime &&
      (await customPageContext
        .getByTestId(AVAILABILITY_SELECTORS.beforeBufferTime)
        .fill(startTime));

    endTime &&
      (await customPageContext
        .getByTestId(AVAILABILITY_SELECTORS.afterBufferTime)
        .fill(endTime));

    await Promise.all([
      customPageContext
        .getByTestId(AVAILABILITY_SELECTORS.saveBufferButton)
        .click(),

      this.commonUtils.verifyToast({ customPageContext }),
    ]);
  };

  verifyNextMonthSubheader = async ({ nextMonth }: { nextMonth: string }) => {
    await this.page.getByTestId(AVAILABILITY_SELECTORS.viewNextButton).click();
    await expect(
      this.page.getByTestId(COMMON_SELECTORS.subheaderText)
    ).toContainText(nextMonth);
  };

  overrideEndTime = async ({ endTime }: { endTime: string }) => {
    await this.page.getByTestId(AVAILABILITY_SELECTORS.endTime).fill(endTime);
    await this.page.keyboard.press("Enter");

    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.dateOverrideSubmitButton)
      .click();

    await this.commonUtils.verifyToast({
      message: this.t("added_override_where_hours_differ"),
    });
  };

  enterStartTime = async ({ startTime }: { startTime: string }) => {
    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.startTime)
      .fill(startTime);
    await this.page.keyboard.press("Enter");
  };

  applyAvailability = async ({
    availabilityName,
    meetingName,
  }: ApplyAvailabilityParams) => {
    const availabilityCard = this.page
      .getByTestId(AVAILABILITY_SELECTORS.card)
      .filter({ hasText: availabilityName });

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

    await availabilityCard.getByTestId(COMMON_SELECTORS.dropdownIcon).click();
    await expect(applyAvailabilityButton).toHaveText(
      this.t("availability.apply")
    );

    await this.commonUtils.waitForPageLoad();
    await applyAvailabilityButton.click();
    await expect(
      this.page.getByTestId(AVAILABILITY_SELECTORS.applyAvailabilityHeader)
    ).toHaveText(this.t("availability.apply"));

    await expect(
      this.page.getByTestId(AVAILABILITY_SELECTORS.applyAvailabilityDescription)
    ).toHaveText(this.t("availability.selected", { entity: availabilityName }));

    await this.commonUtils.selectOptionFromDropdown({
      value: meetingName,
    });

    await Promise.all([
      this.page.getByTestId(COMMON_SELECTORS.saveChangesButton).click(),
      this.commonUtils.verifyToast(),
    ]);

    await expect(
      availabilityCard.getByTestId(
        AVAILABILITY_SELECTORS.availabilityUsedInMeeting
      )
    ).toHaveText(this.t("availability.usedInMeetings", { count: 1 }));
  };

  saveAvailability = () =>
    Promise.all([
      this.page
        .getByTestId(AVAILABILITY_SELECTORS.weeklyHoursSaveChangesButton)
        .click(),
      this.commonUtils.verifyToast(),
    ]);

  toggleAvailabilities = (days: string[]) =>
    Promise.all(
      days.map(day =>
        this.page
          .getByTestId(AVAILABILITY_SELECTORS.workingDayContainer)
          .filter({ hasText: day.toLowerCase() })
          .getByTestId(COMMON_SELECTORS.neetoUiSwitch)
          .dispatchEvent("click")
      )
    );

  checkButtonVisibility = (name: string) =>
    expect(
      this.page
        .getByTestId(COMMON_SELECTORS.dropdownContainer)
        .getByRole("menuitem", { name: this.t(name) })
        // 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
    ).toBeVisible();

  verifyAvailabilitySection = async (
    newAvailabilityName?: string,
    isPane: boolean = true
  ) => {
    const newAvailabilityPane = isPane
      ? this.page.getByTestId(HOST_SECTION_SELECTORS.newAvailabilityPane)
      : this.page;

    const workingDayContainer = newAvailabilityPane.getByTestId(
      AVAILABILITY_SELECTORS.workingDayContainer
    );

    await Promise.all(
      WORKING_DAYS.map(day =>
        expect(
          workingDayContainer
            .filter({ hasText: day })
            .getByTestId(
              AVAILABILITY_SELECTORS.weeklyHoursCheckbox(
                day.toLowerCase() as AllDaysType
              )
            )
        ).toBeChecked()
      )
    );

    await Promise.all(
      WEEKENDS.map(day =>
        expect(
          workingDayContainer
            .filter({ hasText: day })
            .getByTestId(
              AVAILABILITY_SELECTORS.weeklyHoursCheckbox(
                day.toLowerCase() as AllDaysType
              )
            )
        ).not.toBeChecked()
      )
    );

    await Promise.all(
      WEEKENDS.map(day =>
        workingDayContainer
          .filter({ hasText: day })
          .getByTestId(COMMON_SELECTORS.neetoUiSwitch)
          .dispatchEvent("click")
      )
    );

    newAvailabilityName &&
      (await newAvailabilityPane
        .getByTestId(AVAILABILITY_SELECTORS.nameInputField)
        .fill(newAvailabilityName));

    await Promise.all([
      newAvailabilityPane
        .getByTestId(AVAILABILITY_SELECTORS.weeklyHoursSaveChangesButton)
        .click(),
      this.commonUtils.verifyToast(),
    ]);
  };

  applyAvailabilityViaRequest = async ({
    availabilityName,
    meetingName,
    hostEmail,
  }) => {
    const [hostId, { id: meetingId }, availabilityId] = await Promise.all([
      this.teamMembersPage.getMemberIdViaRequest(hostEmail),
      this.schedulingLinkPage.getMeetingIds(meetingName),
      this.getAvailabilityId(availabilityName),
    ]);

    await this.availabilityApis.update({
      availabilityId,
      body: keysToSnakeCase({
        meetingMembersAvailabilitiesAttributes: [
          { meetingId, memberId: hostId },
        ],
      }),
    });
  };

  updateAvailabilityViaRequest = async (
    availabilityDetails: AvailabilityDetails
  ) => {
    const availabilityId = await this.getAvailabilityId(
      availabilityDetails.name
    );

    await this.availabilityApis.update({
      availabilityId,
      body: keysToSnakeCase({
        name: availabilityDetails.name,
        periodsAttributes: availabilityDetails.customAvailability,
      }),
    });
  };

  destroyDayAvailability = async (availabilityName: string, days: number[]) => {
    const availabilityId = await this.getAvailabilityId(availabilityName);

    const availabilityDetails = await this.availabilityApis
      .fetchDetails(availabilityId)
      .then(response => response.json());

    await Promise.all(
      days
        .map(day =>
          availabilityDetails.periods[day].map(dayAvailability =>
            this.availabilityApis.destroyDayAvailability(
              dayAvailability.periodable_id
            )
          )
        )
        .flat()
    );
  };

  getWeekdayContainer = (dayNumber: number) =>
    this.page
      .getByTestId(HOST_SECTION_SELECTORS.newAvailabilityPane)
      .getByTestId(AVAILABILITY_SELECTORS.workingDayContainer)
      .filter({ hasText: ALL_DAYS[dayNumber] });

  addNewAvailabilityFromHostSection = async (
    customAvailabilityName: string,
    timezoneName: string
  ) => {
    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.addNewAvailability)
      .click();

    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.nameInputField)
      .fill(customAvailabilityName);

    await this.page
      .getByTestId(AVAILABILITY_SELECTORS.createAvailabilitySelectButton)
      .click();

    await this.page.getByTestId(timezoneName).click();

    await Promise.all([
      this.page
        .getByTestId(AVAILABILITY_SELECTORS.weeklyHoursSaveChangesButton)
        .click(),
      this.commonUtils.verifyToast(),
    ]);
  };

  verifyAvailabilityDaysOrder = async (weekDay: AllDaysType) => {
    const weekDays = this.commonUtils.WEEK_DAYS;
    const dayAvailabilityContainersDays = await this.page
      .getByTestId(/-day-text$/)
      .all();

    await Promise.all(
      dayAvailabilityContainersDays.map((day, index) => {
        const currentDayIndex = (weekDays.indexOf(weekDay) + index) % 7;
        const expectedDay = weekDays[currentDayIndex];

        return expect(day).toHaveText(expectedDay);
      })
    );
  };
}
