import { dynamicArray, hyphenate } from "@bigbinary/neeto-cist";
import { faker } from "@faker-js/faker";
import {
  EMPTY_STORAGE_STATE,
  OrganizationPage,
  SINGULAR,
} from "@neetoplaywright";
import { BrowserContext, expect, Page } from "@playwright/test";
import dayjs, { Dayjs } from "dayjs";
import { isNotNil } from "ramda";

import { HolidaysApis } from "@apis";
import {
  FULL_MONTH_NAME_FORMAT,
  MONTH_YEAR_FORMAT,
  PRODUCT,
  STANDARD_DATE_FORMAT,
  VISIBILITY_TIMEOUT,
} from "@constants/common";
import { HELP_URL, ROUTES } from "@constants/routes";
import test from "@fixtures";
import {
  AvailabilityPage,
  BookingPage,
  CommonUtils,
  CustomDomainsPage,
  HolidaysPage,
  OnboardingPage,
  SchedulingLinkPage,
} from "@poms";
import TableUtils from "@poms/commons/tables";
import { LeadDaysType } from "@poms/types";
import {
  ADMIN_PANEL_CARD_SELECTORS,
  HOLIDAYS_SELECTORS,
  BOOKING_SELECTORS,
  COMMON_SELECTORS,
  EUI_SELECTORS,
} from "@selectors";
import { Credentials, TimeStamp } from "@types";
import {
  generateStagingDataAndBaseURL,
  navigateToTargetMonth,
  buildBookingUrl,
  getFirstMondayAfterMonths,
  getBookingDateInFormats,
  getCurrentDateTime,
  calendarDaysToHitBusinessDays,
  getNearestUpcomingSlotOfToday,
  createTimeSlots,
  moveDateToWorkingDay,
} from "@utils";

test.describe("Holidays in Admin panel", () => {
  test.describe.configure({ mode: "default" });

  let meetingName: string,
    holidayName: string,
    holidayId: string,
    meetingId: string,
    availabilityName: string;

  let credentials: Credentials,
    page: Page,
    baseURL: string,
    context: BrowserContext,
    schedulingLinkPage: SchedulingLinkPage,
    availabilityPage: AvailabilityPage,
    bookingPage: BookingPage,
    holidaysPage: HolidaysPage,
    commonUtils: CommonUtils,
    holidaysApis: HolidaysApis,
    tableUtils: TableUtils,
    customDomainsPage: CustomDomainsPage;

  const nextMonthMonday = getFirstMondayAfterMonths(1);
  const { nextWeekDate: nextWeekMonday } = getBookingDateInFormats(1);

  const nextMonthMondayStandardFormat =
    dayjs(nextMonthMonday).format(STANDARD_DATE_FORMAT);

  test.beforeAll(async ({ browser }) => {
    ({ credentials, baseURL } = generateStagingDataAndBaseURL());

    await test.step("1: Sign up the Admin user", async () => {
      context = await browser.newContext({
        ...EMPTY_STORAGE_STATE,
        baseURL,
      });

      page = await context.newPage();
      const request = context.request;

      commonUtils = new CommonUtils(page, request);
      const signupPage = new OrganizationPage(page, commonUtils);
      await signupPage.signUp({ credentials, appName: PRODUCT });
    });

    await test.step("2: Onboard the Admin user", async () => {
      const onboardingPage = new OnboardingPage(page, browser);

      await onboardingPage.onboardAndSetup({
        skipOnboarding: true,
        baseURL,
        ...credentials,
        shouldOnboardViaRequest: true,
      });

      await page.waitForLoadState();

      schedulingLinkPage = new SchedulingLinkPage(page, commonUtils);
      availabilityPage = new AvailabilityPage(page, commonUtils);
      bookingPage = new BookingPage({ page, commonUtils });
      commonUtils = new CommonUtils(page, context.request);
      holidaysPage = new HolidaysPage(page, commonUtils);
      holidaysApis = new HolidaysApis(commonUtils);
      tableUtils = new TableUtils(page, commonUtils);
      customDomainsPage = new CustomDomainsPage(page, browser);
      customDomainsPage.subdomain = credentials.subdomainName;
    });
  });

  test.beforeEach(async () => {
    meetingName = faker.lorem.words(3);
    holidayName = faker.word.words(2);

    meetingId = await schedulingLinkPage.createSchedulingLinkViaRequest({
      name: meetingName,
    });
  });

  test.afterEach(async ({}, testInfo) => {
    await schedulingLinkPage.deleteMeetingViaRequest({ name: meetingName });
    isNotNil(availabilityName) &&
      (await availabilityPage.deleteAvailabilityViaRequest(availabilityName));

    testInfo.tags.includes("@deleteHoliday") &&
      (await holidaysApis.delete(holidayId));
  });

  test.afterAll(async () => {
    await customDomainsPage.disconnectViaRequest();
    await context.close();
  });

  test(
    "should verify page elements",
    { tag: ["@deleteHoliday"] },
    async ({ t }) => {
      const newHolidayName = faker.word.words(2);

      const searchBarInput = page.getByTestId(COMMON_SELECTORS.inputField);
      const targetRow = page.getByRole("row", { name: holidayName });
      const nextMonthTuesday = nextMonthMonday.add(1, "day");
      const nextMonthTuesdayStandardFormat =
        nextMonthTuesday.format(STANDARD_DATE_FORMAT);

      await test.step("3: Verify the settings card details and navigation", async () => {
        await commonUtils.navigateAndWaitForPageLoad(
          ROUTES.adminPanel.general.index
        );

        const holidaysSettingsCard = page.getByTestId(
          ADMIN_PANEL_CARD_SELECTORS.general.holidays
        );

        await Promise.all([
          expect(
            holidaysSettingsCard.getByTestId(
              COMMON_SELECTORS.settingsItemHeading
            )
          ).toHaveText(t("adminPanel.holidays.title")),
          expect(
            holidaysSettingsCard.getByTestId(
              COMMON_SELECTORS.settingsItemDescription
            )
          ).toHaveText(t("adminPanel.holidays.description")),
        ]);

        await holidaysSettingsCard.click();
        await commonUtils.waitForPageLoad();

        await expect(page.getByTestId(COMMON_SELECTORS.heading)).toContainText(
          t("adminPanel.holidays.title")
        );
      });

      await test.step("4: Verify the help popover", () =>
        commonUtils.verifyHelpPopover({
          triggerElement: page
            .getByTestId(COMMON_SELECTORS.heading)
            .getByTestId(COMMON_SELECTORS.helpPopoverButton),
          title: t("adminPanel.holidays.title"),
          content: t("adminPanel.holidays.description"),
          helpURL: HELP_URL.holidays,
        }));

      await test.step("5: Add new holiday via UI", () =>
        holidaysPage.addHoliday(holidayName, nextMonthMonday));

      await test.step("6: Verify full day unavailability for holiday", () =>
        bookingPage.verifyFullDayUnavailability(
          hyphenate(meetingName),
          nextMonthMondayStandardFormat
        ));

      await test.step("7: Verify the empty state text", async () => {
        await commonUtils.navigateAndWaitForPageLoad(
          ROUTES.adminPanel.general.holidays
        );

        await searchBarInput.fill(faker.word.words(3));
        await commonUtils.waitForPageLoad();

        await Promise.all([
          expect(page.getByTestId(COMMON_SELECTORS.noDataTitle)).toContainText(
            t("emptyState.holidays.title")
          ),
          expect(
            page.getByTestId(COMMON_SELECTORS.noDataDescription)
          ).toContainText(t("emptyState.holidays.description")),
          expect(
            page.getByTestId(HOLIDAYS_SELECTORS.availabilityOverridesButton)
          ).toHaveAttribute(
            "href",
            ROUTES.host.self.meetings.availabilities.index
          ),
        ]);

        await searchBarInput.clear();
        await searchBarInput.blur();

        await expect(
          page.getByTestId(HOLIDAYS_SELECTORS.holidaysCount)
        ).toHaveText(t("adminPanel.holidays.withCount", SINGULAR), {
          timeout: VISIBILITY_TIMEOUT,
        });
      });

      await test.step("8: Verify the freeze column feature", () =>
        tableUtils.verifyFreezeColumnAction());

      await test.step("9: Verify the search bar", async () => {
        await searchBarInput.fill(holidayName);
        await commonUtils.waitForPageLoad();

        await expect(
          page.getByTestId(HOLIDAYS_SELECTORS.holidaysCount)
        ).toHaveText(t("adminPanel.holidays.withCount", SINGULAR), {
          timeout: 10_000,
        });

        await expect(
          targetRow.getByRole("cell", {
            name: nextMonthMonday.format(MONTH_YEAR_FORMAT),
          })
        ).toBeVisible();

        await searchBarInput.clear();
      });

      await test.step("10: Edit the holiday", () =>
        commonUtils.performActionFromMoreDropdown(
          targetRow,
          t("operation.edit")
        ));

      await test.step("11: Change the name and date of holiday", () =>
        holidaysPage.fillHolidayDetails({
          holidayName: newHolidayName,
          holidayDate: nextMonthTuesday,
          shouldRepeatEveryYear: true,
          previousHolidayDate: nextMonthMonday,
        }));

      await test.step("12: Verify change in holiday reflects in booking form", () =>
        bookingPage.verifyFullDayUnavailability(
          hyphenate(meetingName),
          nextMonthTuesdayStandardFormat
        ));

      await test.step("13: Verify slots availability of previous holiday", async () => {
        await page
          .getByTestId(BOOKING_SELECTORS.troubleshootingButton("done"))
          .click();

        await bookingPage.selectDate({
          bookingDate: nextMonthMondayStandardFormat,
        });

        const availableSlots = await commonUtils.getTotalAvailableSlots();
        expect(availableSlots.length).toBeGreaterThan(10);
      });
    }
  );

  test(
    "should verify disabling the holiday reflects in booking form",
    { tag: ["@deleteHoliday"] },
    async () => {
      holidayId = await test.step("3: Create holiday via request", () =>
        holidaysApis.create(holidayName, nextMonthMonday));

      await test.step("4: Verify unavailability of the host due to holiday", () =>
        bookingPage.verifyFullDayUnavailability(
          hyphenate(meetingName),
          nextMonthMondayStandardFormat
        ));

      await test.step("5: Disable the holiday", async () => {
        await commonUtils.navigateAndWaitForPageLoad(
          ROUTES.adminPanel.general.holidays
        );

        await page
          .getByRole("row", { name: holidayName })
          .getByTestId(COMMON_SELECTORS.neetoUiSwitch)
          .click();
        await commonUtils.verifyToast();
      });

      await test.step("6: Verify the slots availability", async () => {
        await commonUtils.navigateAndWaitForPageLoad(
          buildBookingUrl(hyphenate(meetingName), nextMonthMondayStandardFormat)
        );

        await bookingPage.selectDate({
          bookingDate: nextMonthMondayStandardFormat,
        });

        const availableSlots = await commonUtils.getTotalAvailableSlots();
        expect(availableSlots.length).toBeGreaterThan(10);
      });
    }
  );

  test("should verify repeat every year feature", async ({ t }) => {
    const nextYearMonday = nextMonthMonday.add(1, "year");

    await test.step("3: Create holiday that repeats every year via request", async () => {
      const response = await holidaysApis.create(
        holidayName,
        nextYearMonday,
        true
      );
      holidayId = response[0];
    });

    await test.step("4: Verify unavailability of the slots due to holiday in the current year", () =>
      bookingPage.verifyFullDayUnavailability(
        hyphenate(meetingName),
        nextMonthMondayStandardFormat
      ));

    await test.step("5: Disable repeat every year", async () => {
      await commonUtils.navigateAndWaitForPageLoad(
        ROUTES.adminPanel.general.holidays
      );

      await commonUtils.performActionFromMoreDropdown(
        page.getByRole("cell", { name: holidayName }),
        t("operation.edit")
      );

      await commonUtils.waitForPageLoad();
      await expect(page.getByTestId(COMMON_SELECTORS.paneHeader)).toContainText(
        t("adminPanel.holidays.edit")
      );

      await page
        .getByTestId(HOLIDAYS_SELECTORS.repeatEveryYearSwitchLabel)
        .click();

      await commonUtils.saveChanges();
    });

    await test.step("6: Verify the slots availability", async () => {
      await commonUtils.navigateAndWaitForPageLoad(
        buildBookingUrl(hyphenate(meetingName), nextMonthMondayStandardFormat)
      );

      await bookingPage.selectDate({
        bookingDate: nextMonthMondayStandardFormat,
      });

      const availableSlots = await commonUtils.getTotalAvailableSlots();
      expect(availableSlots.length).toBeGreaterThan(10);
    });
  });

  test("should verify deleting a holiday reflects in booking form", async ({
    t,
  }) => {
    holidayId = await test.step("3: Create holiday via request", () =>
      holidaysApis.create(holidayName, nextMonthMonday));

    await test.step("4: Verify unavailability of the host due to holiday", () =>
      bookingPage.verifyFullDayUnavailability(
        hyphenate(meetingName),
        nextMonthMondayStandardFormat
      ));

    await test.step("5: Verify deleting the holiday reflects in booking form", async () => {
      await commonUtils.navigateAndWaitForPageLoad(
        ROUTES.adminPanel.general.holidays
      );

      await expect(
        page.getByRole("row", { name: holidayName }).getByRole("cell", {
          name: nextMonthMonday.format(MONTH_YEAR_FORMAT),
        })
      ).toBeVisible({ timeout: 10_000 });

      await commonUtils.performActionFromMoreDropdown(
        page.getByRole("cell", { name: holidayName }),
        t("operation.delete")
      );

      await Promise.all([
        expect(page.getByTestId(COMMON_SELECTORS.alertTitle)).toHaveText(
          t("alert.delete.title", { entity: t("entity.holiday") })
        ),
        expect(page.getByTestId(COMMON_SELECTORS.alertModalMessage)).toHaveText(
          t("alert.delete.message", {
            entity: t("entity.holiday"),
            title: holidayName,
          })
        ),
      ]);

      await commonUtils.clickOnButton(COMMON_SELECTORS.alertModalSubmitButton);

      await commonUtils.verifyToast();
    });

    await test.step("6: Verify the slots availability", async () => {
      await commonUtils.navigateAndWaitForPageLoad(
        buildBookingUrl(hyphenate(meetingName), nextMonthMondayStandardFormat)
      );

      await bookingPage.selectDate({
        bookingDate: nextMonthMondayStandardFormat,
      });

      const availableSlots = await commonUtils.getTotalAvailableSlots();
      expect(availableSlots.length).toBeGreaterThan(10);
    });
  });

  test(
    "should verify holidays not reflected in other workspaces",
    { tag: ["@deleteHoliday"] },
    async ({
      page: defaultPage,
      commonUtils: defaultPlaywrightUtilities,
      commonUtils: defaultCommonUtils,
    }) => {
      const holidayName = faker.word.words(2);
      const nextYearMonday = nextMonthMonday.add(1, "year");

      holidayId = await test.step("3: Create holiday via request", () =>
        holidaysApis.create(holidayName, nextYearMonday));

      await test.step("4: Verify the created holiday in the isolated context", async () => {
        await commonUtils.navigateAndWaitForPageLoad(
          ROUTES.adminPanel.general.holidays
        );

        await expect(page.getByRole("row", { name: holidayName })).toBeVisible({
          timeout: 20_000,
        });
      });

      await test.step("5: Verify the created holiday not reflected in the global user context", async () => {
        await defaultCommonUtils.navigateAndWaitForPageLoad(
          ROUTES.adminPanel.general.holidays
        );

        await defaultPage
          .getByTestId(COMMON_SELECTORS.inputField)
          .fill(holidayName);
        await defaultPlaywrightUtilities.waitForPageLoad();

        await expect(
          defaultPage.getByTestId(COMMON_SELECTORS.noDataPrimaryButton)
        ).toBeVisible({ timeout: 15_000 });
      });
    }
  );

  ["business", "calendar"].forEach(leadDaysType => {
    test(
      `should verify lead time in days for ${leadDaysType} days with holidays`,
      { tag: ["@deleteHoliday"] },
      async ({ t }) => {
        availabilityName = faker.word.words(2);
        const startTime = "09:00 AM" as TimeStamp;
        const endTime = "06:00 PM" as TimeStamp;
        let upcomingSlot: TimeStamp, nextDay: Dayjs;

        const leadTimeDays = 7;
        const customAvailability = dynamicArray(5, index => ({
          wday: index + 1,
          startTime,
          endTime,
        }));

        await test.step("3: Create availability", () =>
          availabilityPage.createAvailabilityViaRequest({
            name: availabilityName,
            customAvailability,
          }));

        await test.step("4: Apply availability", () =>
          availabilityPage.applyAvailabilityViaRequest({
            availabilityName,
            meetingName,
            hostEmail: credentials.email,
          }));

        holidayId =
          await test.step("5: Create holiday via request for the next week monday", () =>
            holidaysApis.create(holidayName, nextWeekMonday));

        await test.step("6: Add custom lead time in days", () =>
          schedulingLinkPage.setLeadTime(
            meetingId,
            leadTimeDays,
            leadDaysType as LeadDaysType
          ));

        await test.step("7: Verify unavailability of slots due to lead days", async () => {
          let totalUnavailabilityDays =
            leadDaysType === "business"
              ? calendarDaysToHitBusinessDays(leadTimeDays) + 1 // +1 for Monday holiday
              : leadTimeDays;

          upcomingSlot = getNearestUpcomingSlotOfToday(startTime, endTime);

          isNotNil(upcomingSlot) && (totalUnavailabilityDays -= 1); // Subtracting the current day because lead time starts from current time

          nextDay = getCurrentDateTime().add(1, "day");

          await bookingPage.openBookingForm({
            slug: hyphenate(meetingName),
            bookingDateInStandardFormat: nextDay.format(STANDARD_DATE_FORMAT),
            isTroubleshooting: true,
          });

          while (totalUnavailabilityDays--) {
            const nextDayStandardFormat = nextDay.format(STANDARD_DATE_FORMAT);

            const calendarCell = page.locator(
              BOOKING_SELECTORS.calendarCell(nextDayStandardFormat)
            );

            await navigateToTargetMonth({
              newMonth: nextDay.format(FULL_MONTH_NAME_FORMAT),
              page,
              isPreviousMonth: false,
            });

            await commonUtils.waitForUIComponentsToLoad();
            await calendarCell.click();

            await Promise.any([
              bookingPage.assertFullDayUnavailabilityMessage(
                nextDayStandardFormat
              ),
              bookingPage.assertUnavailabilityOfNthSlot(),
            ]);

            nextDay = nextDay.add(1, "day");
          }
        });

        await test.step("8: Verify the slots availability", async () => {
          if (nextDay.isSame(nextWeekMonday, "day")) {
            // Because slots won't be available on holiday
            upcomingSlot = startTime;
            nextDay = nextDay.add(1, "day");
          }

          nextDay = moveDateToWorkingDay(nextDay);
          await bookingPage.toggleTroubleshooting();

          await bookingPage.selectDate({
            bookingDate: nextDay.format(STANDARD_DATE_FORMAT),
            shouldNavigateToMonth: true,
            shouldWaitForSlotsVisibility: true,
          });

          const expectedAvailableSlots = createTimeSlots({
            startingTime: isNotNil(upcomingSlot) ? upcomingSlot : startTime,
            endingTime: endTime,
          });

          const availableSlots = page
            .getByTestId(BOOKING_SELECTORS.startTimeSlotContainer)
            .filter({
              has: page.getByText(t("live.troubleshoot.available"), {
                exact: true,
              }),
            })
            .getByTestId(EUI_SELECTORS.slotStartTime);

          await expect.soft(availableSlots).toHaveText(expectedAvailableSlots, {
            timeout: VISIBILITY_TIMEOUT,
          });
        });
      }
    );
  });
});
