import { verifyBookingBasketContent } from '@/portal/modules/booking/booking-assertions';
import { BookingCreateData } from '@/portal/modules/booking/booking-types';
import {
  bookingCheckoutTestCases,
  bookingCompletionTestCases,
  declinedPaymentBookingCreateTestCases,
  neighboringUnitBookingCreateTestCases,
} from '@/portal/modules/booking/test-cases/booking-create-test-cases';
import { test } from '@/portal/modules/booking/booking-fixtures';
import { BookingBasketView } from '@/portal/modules/booking/views/BookingBasketView';
import { BookingCustomerView } from '@/portal/modules/booking/views/BookingCustomerView';
import { BookingLocationView } from '@/portal/modules/booking/views/BookingLocationView';
import { BookingPaymentView } from '@/portal/modules/booking/views/BookingPaymentView';
import { BookingUnitTypeView } from '@/portal/modules/booking/views/BookingUnitTypeView';
import { Location } from '@/shared/types/location-types';
import { expect } from '@playwright/test';

// This whole file runs sequentially: mode: 'default' opts it out of the global fullyParallel setting, so its
// tests run one at a time in declaration order even when multiple workers are available — bookings can't be
// driven through Stripe concurrently. The 120s timeout covers the slow Stripe steps. Both settings apply to
// every test in the file, including the describes below.
test.describe.configure({ mode: 'default', timeout: 120_000 });

async function selectLocation(
  bookingLocationView: BookingLocationView,
  location: Location
): Promise<BookingUnitTypeView> {
  return test.step('select location', async () => {
    const view = await bookingLocationView.select(location.id);

    if (location.showCookieConsentBanner) {
      await view.acceptCookieConsent({ allowMarketing: true });
    }

    return view;
  });
}

async function selectUnitTypeAndOptions(
  bookingLocationView: BookingLocationView,
  booking: BookingCreateData
): Promise<BookingBasketView> {
  const unitTypeView = await selectLocation(bookingLocationView, booking.location);

  const optionsView = await test.step('select unit type', async () => {
    return unitTypeView.select(
      booking.unitType.name,
      booking.location.categories.length > 1 ? booking.unitType.category : undefined
    );
  });

  return test.step('select booking options', async () => {
    await optionsView.select(booking.moveInDate, booking.unitType.billing, booking.insurance?.name);
    return optionsView.confirm();
  });
}

async function checkoutToCustomer(
  basketView: BookingBasketView,
  booking: BookingCreateData,
  isMobile: boolean
): Promise<BookingCustomerView> {
  await test.step('verify basket summary', async () => {
    if (booking.discount) {
      await basketView.add(booking.discount.code);
    }

    await verifyBookingBasketContent(
      basketView.basketContent,
      isMobile,
      'private',
      booking.moveInDate,
      booking.location,
      booking.unitType,
      booking.insurance,
      booking.discount
    );
  });

  const customerView = await test.step('confirm basket', async () => {
    const view = await basketView.confirm();
    view.isMobile = isMobile;
    return view;
  });

  await test.step('fill customer data', async () => {
    await customerView.fill(booking.customer);

    if (isMobile) {
      await customerView.openDrawer();
    }

    await verifyBookingBasketContent(
      customerView.getBasketContent(),
      isMobile,
      booking.customer.type,
      booking.moveInDate,
      booking.location,
      booking.unitType,
      booking.insurance,
      booking.discount
    );
  });

  return customerView;
}

async function confirmCustomerAndFillPayment(
  customerView: BookingCustomerView,
  booking: BookingCreateData
): Promise<BookingPaymentView> {
  return test.step('confirm customer data and fill payment info', async () => {
    const paymentView = await customerView.confirm();
    await paymentView.fill(booking.paymentMethod);
    return paymentView;
  });
}

// Basket totals are payment-independent, so these cases stop before payment (no Stripe). They still run
// sequentially under the file-level mode: 'default' above; parallelising them would need a separate file or a
// mode: 'parallel' describe.
for (const tc of bookingCheckoutTestCases) {
  test(tc.description, tc.runOnMobile ? { tag: '@mobile' } : {}, async ({ bookingLocationView, isMobile }) => {
    const basketView = await selectUnitTypeAndOptions(bookingLocationView, tc.data);
    await checkoutToCustomer(basketView, tc.data, isMobile);
  });
}

// payment completion — drives the full Stripe funnel through to the success view
for (const tc of bookingCompletionTestCases) {
  test(tc.description, tc.runOnMobile ? { tag: '@mobile' } : {}, async ({ bookingLocationView, isMobile }) => {
    const basketView = await selectUnitTypeAndOptions(bookingLocationView, tc.data);
    const customerView = await checkoutToCustomer(basketView, tc.data, isMobile);
    const paymentView = await confirmCustomerAndFillPayment(customerView, tc.data);

    // currently restricting sepa payment tests for desktop (see: comments of https://kinn.atlassian.net/browse/KIN-2835)
    if (!isMobile && tc.data.paymentMethod.kind === 'sepa') {
      return;
    }

    const successView = await test.step('confirm payment', () => paymentView.confirm());

    await test.step('verify success view', async () => {
      const customerName = tc.data.customer.companyName ?? `${tc.data.customer.firstName} ${tc.data.customer.lastName}`;

      await expect(successView.welcomeMessage).toBeVisible();
      await expect(successView.thanksMessage).toContainText(customerName);
      await expect(successView.emailMessage).toContainText(tc.data.customer.email);
      await expect(successView.repeatButton).toBeVisible();
    });
  });
}

// declined payment — submits a declined card and expects the error surface
for (const tc of declinedPaymentBookingCreateTestCases) {
  test(tc.description, tc.runOnMobile ? { tag: '@mobile' } : {}, async ({ bookingLocationView, isMobile }) => {
    const basketView = await selectUnitTypeAndOptions(bookingLocationView, tc.data);
    const customerView = await checkoutToCustomer(basketView, tc.data, isMobile);
    const paymentView = await confirmCustomerAndFillPayment(customerView, tc.data);

    await test.step('attempt to confirm and verify payment error', async () => {
      await paymentView.submit();
      await expect(paymentView.stripeFieldError.or(paymentView.stripeAlertError)).toBeVisible();
    });
  });
}

// neighboring units — when the chosen size is unavailable, a neighboring unit is suggested
for (const tc of neighboringUnitBookingCreateTestCases.happy) {
  test(tc.description, tc.runOnMobile ? { tag: '@mobile' } : {}, async ({ bookingLocationView, isMobile }) => {
    const { booking, notAvailable } = tc.data;

    const unitTypeView = await selectLocation(bookingLocationView, booking.location);

    const oldOptionsView = await test.step('select unavailable unit type', async () => {
      return unitTypeView.select(
        notAvailable.unitType,
        booking.location.categories.length > 1 ? notAvailable.category : undefined
      );
    });

    const sizeNotAvailableView = await test.step('verify selected size is not available', async () => {
      return oldOptionsView.failSelect(booking.moveInDate);
    });

    const basketView = await test.step('select neighboring unit and booking options', async () => {
      const optionsView = await sizeNotAvailableView.select(booking.unitType.name);
      await optionsView.selectWithoutMoveInDate(booking.unitType.billing, booking.insurance?.name);
      return optionsView.confirm();
    });

    const customerView = await checkoutToCustomer(basketView, booking, isMobile);

    if (!tc.completesPayment) {
      return;
    }

    const paymentView = await confirmCustomerAndFillPayment(customerView, booking);
    const successView = await test.step('confirm payment', () => paymentView.confirm());

    await test.step('verify success view', async () => {
      const customerName = booking.customer.companyName ?? `${booking.customer.firstName} ${booking.customer.lastName}`;

      await expect(successView.welcomeMessage).toBeVisible();
      await expect(successView.thanksMessage).toContainText(customerName);
      await expect(successView.emailMessage).toContainText(booking.customer.email);
      await expect(successView.repeatButton).toBeVisible();
    });
  });
}

for (const tc of neighboringUnitBookingCreateTestCases.unhappy) {
  test(tc.description, async ({ bookingLocationView }) => {
    const { booking, notAvailable } = tc.data;

    const unitTypeView = await selectLocation(bookingLocationView, booking.location);

    const optionsView = await test.step('select unavailable unit type', async () => {
      return unitTypeView.select(
        notAvailable.unitType,
        booking.location.categories.length > 1 ? notAvailable.category : undefined
      );
    });

    const sizeNotAvailableView = await test.step('attempt to select move-in date', async () => {
      return optionsView.failSelect(booking.moveInDate);
    });

    await test.step('verify back-to-size-selection button is visible', async () => {
      await expect(sizeNotAvailableView.backToSizeSelectionButton).toBeVisible();
    });
  });
}
