import {
  verifyBookingInvoice,
  verifyBookingInvoiceCount,
  verifyBookingPayment,
  verifyBookingPaymentCount,
} from '@/manager/modules/booking/booking-assertions';
import { getBookingCreateData } from '@/manager/modules/booking/booking-factories';
import { test } from '@/manager/modules/booking/booking-fixtures';
import { toBookingCreateTotalsSummary } from '@/manager/modules/booking/booking-mappers';
import { bookingAdjustMoveInDateHappyTestCases } from '@/manager/modules/booking/test-cases/booking-adjust-move-in-date-happy-test-cases';
import { getCustomerCreateData } from '@/manager/modules/customer/customer-factories';
import { getUnitCreateData } from '@/manager/modules/unit/unit-factories';
import {
  getDefaultUnitTypeCreateWeeklyBookingPlans,
  getUnitTypeCreateData,
} from '@/manager/modules/unit-type/unit-type-factories';
import { expectLocatorToBeEmpty, expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { getDefaultVisaCardPayment } from '@/shared/modules/payment-method/payment-method-factories';
import { getFutureDate } from '@/shared/utils/date-utils';
import { pollOrSkip } from '@/shared/utils/poll-utils';
import { formatCurrency, formatDate, formatDateRange, formatPaymentMethod } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';
import dayjs from 'dayjs';

// Activation cases wait on async first-period invoicing (up to a 60s reload-poll) on top of the booking
// setup and adjustment itself, so the default per-test timeout is too tight — allow headroom.
test.describe.configure({ mode: 'default', timeout: 120_000 });

for (const tc of bookingAdjustMoveInDateHappyTestCases) {
  test(tc.description, async ({ setupBookingCreateData, createBooking, bookingDetailsPage }) => {
    const data = await setupBookingCreateData({}, tc.options);
    const booking = await createBooking(data);
    const { currency, timezone, invoiceDaysUntilDue } = booking.location;
    const newMoveInDate = getFutureDate(tc.newMoveInDaysInTheFuture, 'day');

    // Mirrors the server's scheduled check: the booking stays scheduled only while the new move-in,
    // anchored at the location-local 02:00 subscription start, is still ahead of now. A past date always
    // activates; today activates once 02:00 local time has passed — relevant for runs in the 00:00-02:00
    // window, where a move to "today" still counts as scheduled.
    const activates = !dayjs(newMoveInDate).tz(timezone).startOf('day').add(2, 'hour').isAfter(dayjs());

    // On activation, billing periods anchor on the new move-in date and invoicing starts with the
    // period that contains today. Every activating case here backdates by a whole number of weekly plan
    // periods (0 or -7 days), so that ongoing period runs from today over one plan period, with the
    // next invoice due at its end.
    const plan = booking.unitType.plan;
    const today = getFutureDate(0, 'day');
    const currentPeriodEnd = getFutureDate(plan.amount, plan.period === 'monthly' ? 'month' : 'week');
    const totalInclVat = formatCurrency(toBookingCreateTotalsSummary(data).totalInclVat, currency);
    const formattedNewMoveInDate = formatDate(newMoveInDate, timezone);
    const formattedToday = formatDate(today, timezone);
    const formattedCurrentPeriodEnd = formatDate(currentPeriodEnd, timezone);
    const dueDate = formatDate(getFutureDate(invoiceDaysUntilDue, 'day'), timezone);

    await bookingDetailsPage.goto(booking.id);

    await test.step('adjust the move-in date', async () => {
      const adjustMoveInDateDialog = await bookingDetailsPage.openBookingAdjustMoveInDateDialog();
      await adjustMoveInDateDialog.edit(newMoveInDate);
    });

    await test.step('verify the booking values on the details page', async () => {
      await expectSingleLocatorToHaveText(bookingDetailsPage.baseStatus, activates ? 'active' : 'scheduled');
      await expectSingleLocatorToHaveText(bookingDetailsPage.generalCard.moveIn, formattedNewMoveInDate);
    });

    await test.step('verify the billing values on the details page', async () => {
      const billingCard = bookingDetailsPage.billingCard;

      if (activates) {
        await expectSingleLocatorToHaveText(
          billingCard.currentPeriod,
          formatDateRange(today, currentPeriodEnd, timezone)
        );
        await expectSingleLocatorToHaveText(billingCard.nextInvoice, formattedCurrentPeriodEnd);

        // Activation kicks off real invoicing. These cases book the default
        // charge-automatically-after-first-invoice option, so the first invoice is issued open and
        // manual with an unpaid payment row — an auto-charge booking would get it charged instead.
        // Issuance runs through the async backend job queue, which backs up on the test env —
        // reload-poll for the invoice, degrading to a skip on the scheduled lane.
        await pollOrSkip(
          async () => {
            await bookingDetailsPage.reload();
            await expect(bookingDetailsPage.invoicesCard.dataTable.getRows()).toHaveCount(1);
          },
          { timeout: 60_000, reason: 'first invoice not issued within 60s — test-env queue backlog (KIN-4692)' }
        );
        await verifyBookingInvoice(bookingDetailsPage, {
          status: 'Open',
          paymentOption: 'Manual',
          unit: booking.unit.name,
          amount: totalInclVat,
          amountDue: totalInclVat,
          issueDate: formattedToday,
          dueDate,
        });
        await verifyBookingPaymentCount(bookingDetailsPage, 1);
        await verifyBookingPayment(bookingDetailsPage, {
          amount: totalInclVat,
          status: 'Not paid yet',
          date: formattedToday,
        });
      } else {
        // The rescheduled booking has no running period, and the first invoice is expected on the new
        // move-in date. The invoice count is only zero because these cases book no upfront-charged
        // deposit — a scheduled booking created with one would already carry its deposit invoice.
        await expectLocatorToBeEmpty(billingCard.currentPeriod);
        await expectSingleLocatorToHaveText(billingCard.nextInvoice, formattedNewMoveInDate);
        await verifyBookingInvoiceCount(bookingDetailsPage, 0);
      }
    });
  });
}

test('auto-charges the first invoice when the adjusted move-in date activates the booking', async ({
  createCustomer,
  createUnitType,
  createUnit,
  bookingCreateDialog,
  trackBookingForTeardown,
}) => {
  const [customer, unitType] = await Promise.all([
    createCustomer(getCustomerCreateData()),
    createUnitType(getUnitTypeCreateData({ bookingPlans: getDefaultUnitTypeCreateWeeklyBookingPlans() })),
  ]);
  const unit = await createUnit(getUnitCreateData(unitType));
  // An auto-charge booking needs its payment method attached through the create dialog's Stripe
  // elements — the API create payload only carries the payment type — so this booking is created via
  // the UI. A card method is used because a card auto-charge settles synchronously.
  const data = getBookingCreateData(
    { customer, unitType, unit },
    { paymentOption: 'charge_automatically', paymentMethod: getDefaultVisaCardPayment(), moveInDaysInTheFuture: 14 }
  );
  const { currency, timezone, invoiceDaysUntilDue } = data.location;
  // -7 days is exactly one weekly plan period, so the ongoing period (anchored on the new move-in
  // date) runs from today over one plan period.
  const newMoveInDate = getFutureDate(-7, 'day');
  const today = getFutureDate(0, 'day');
  const currentPeriodEnd = getFutureDate(
    data.unitType.plan.amount,
    data.unitType.plan.period === 'monthly' ? 'month' : 'week'
  );
  const totalInclVat = formatCurrency(toBookingCreateTotalsSummary(data).totalInclVat, currency);
  // the payments grid renders "Card •••• 4242" (title first), unlike the billing card's reversed order
  const paymentMethod = data.payment.method ? formatPaymentMethod(data.payment.method) : undefined;
  const formattedNewMoveInDate = formatDate(newMoveInDate, timezone);
  const formattedToday = formatDate(today, timezone);
  const formattedCurrentPeriodEnd = formatDate(currentPeriodEnd, timezone);
  const dueDate = formatDate(getFutureDate(invoiceDaysUntilDue, 'day'), timezone);

  await bookingCreateDialog.goto();

  const detailsPage = await test.step('create booking', () => bookingCreateDialog.create(data));

  trackBookingForTeardown(detailsPage.getBookingId());

  await test.step('adjust the move-in date', async () => {
    const adjustMoveInDateDialog = await detailsPage.openBookingAdjustMoveInDateDialog();
    await adjustMoveInDateDialog.edit(newMoveInDate);
  });

  await test.step('verify the booking values on the details page', async () => {
    await expectSingleLocatorToHaveText(detailsPage.baseStatus, 'active');
    await expectSingleLocatorToHaveText(detailsPage.generalCard.moveIn, formattedNewMoveInDate);
  });

  await test.step('verify the billing values on the details page', async () => {
    await expectSingleLocatorToHaveText(
      detailsPage.billingCard.currentPeriod,
      formatDateRange(today, currentPeriodEnd, timezone)
    );
    await expectSingleLocatorToHaveText(detailsPage.billingCard.nextInvoice, formattedCurrentPeriodEnd);

    // The attached card is charged as soon as activation issues the first invoice, so it is paid with
    // nothing left due and its payment has succeeded. Issuance runs through the async backend job
    // queue, which backs up on the test env — reload-poll for the invoice, degrading to a skip on
    // the scheduled lane.
    await pollOrSkip(
      async () => {
        await detailsPage.reload();
        await expect(detailsPage.invoicesCard.dataTable.getRows()).toHaveCount(1);
      },
      { timeout: 60_000, reason: 'first invoice not issued within 60s — test-env queue backlog (KIN-4692)' }
    );
    await verifyBookingInvoice(detailsPage, {
      status: 'Paid',
      paymentOption: 'Auto charge',
      unit: data.unit.name,
      amount: totalInclVat,
      amountDue: formatCurrency(0, currency),
      issueDate: formattedToday,
      dueDate,
    });
    await verifyBookingPaymentCount(detailsPage, 1);
    // The charged method's details are recorded after the charge result, and the payments card does
    // not refresh them on its own — they only render on a fresh load. Poll with reloads: the sole
    // realistic failure mode is Stripe settlement details landing late, so the scheduled lane
    // degrades to a skip instead of failing on a backed-up queue, while local/PR runs still fail.
    await pollOrSkip(
      async () => {
        await detailsPage.reload();
        await verifyBookingPayment(detailsPage, {
          amount: totalInclVat,
          status: 'Succeeded',
          paymentMethod,
          date: formattedToday,
        });
      },
      { timeout: 60_000, reason: 'charged payment method details not recorded in time (KIN-4692)' }
    );
  });
});

test('hides the adjust move-in date action for an active booking', async ({
  setupBookingCreateData,
  createBooking,
  bookingDetailsPage,
}) => {
  const booking = await createBooking(await setupBookingCreateData());

  await bookingDetailsPage.goto(booking.id);

  await test.step('open the booking action menu', async () => {
    await bookingDetailsPage.actionMenuButton.click();
    // anchor on an action that is always offered for an active booking, so the absence check below
    // cannot pass early against a menu that is still rendering its items
    await expect(bookingDetailsPage.actionMenuCancelBookingItem).toBeVisible();
  });

  await test.step('verify the adjust move-in date action is not offered', async () => {
    await expect(bookingDetailsPage.actionMenuAdjustMoveInDateItem).toHaveCount(0);
  });
});
