import { verifyBookingPriceChangeSection } from '@/manager/modules/booking/booking-assertions';
import { getBookingCreateData } from '@/manager/modules/booking/booking-factories';
import { test } from '@/manager/modules/booking/booking-fixtures';
import { toBookingPriceChangeDetails } from '@/manager/modules/booking/booking-mappers';
import { BookingPriceChange } from '@/manager/modules/booking/booking-types';
import { bookingPriceChangeHappyTestCases } from '@/manager/modules/booking/test-cases/booking-price-change-happy-test-cases';
import { getCustomerDisplayName } from '@/manager/modules/customer/customer-utils';
import {
  getDefaultUnitTypeCreateWeeklyBookingPlans,
  getUnitTypeCreateData,
} from '@/manager/modules/unit-type/unit-type-factories';
import { getUnitCreateData } from '@/manager/modules/unit/unit-factories';
import { getFutureDate } from '@/shared/utils/date-utils';
import { formatId } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';

test.describe.configure({ mode: 'default', timeout: 120_000 });

for (const tc of bookingPriceChangeHappyTestCases) {
  test(tc.description, async ({ setupBookingCreateData, createBooking, bookingListPage, bookingDetailsPage }) => {
    const booking = await createBooking(await setupBookingCreateData(tc.setup, tc.options));
    const expected = toBookingPriceChangeDetails(booking, tc.priceChange);
    const { currency, timezone } = booking.location;

    await bookingListPage.goto();

    await test.step('select the booking in the list', async () => {
      await bookingListPage.searchTextField.fill(formatId(booking.id));
      await expect(bookingListPage.dataTable.getRows()).toHaveCount(1);
      await bookingListPage.selectBookings(0);
    });

    await test.step('apply the price change', async () => {
      const priceChangeDialog = await bookingListPage.openBookingPriceChangeDialog();
      await priceChangeDialog.edit(tc.priceChange, currency);
    });

    await test.step('verify the price adjustment on the details page', async () => {
      await bookingDetailsPage.goto(booking.id);
      await verifyBookingPriceChangeSection(bookingDetailsPage, expected, currency, timezone);
    });
  });
}

test('changes the price of multiple bookings at once', async ({
  setupBookingCreateData,
  createBooking,
  createUnitType,
  createUnit,
  bookingListPage,
  bookingDetailsPage,
}) => {
  const priceChange: BookingPriceChange = {
    option: 'increase',
    value: 10,
    valueType: 'percentage',
    changeAt: getFutureDate(0, 'day'),
    adjustUnit: true,
    adjustRecurringProducts: false,
  };

  // Both bookings belong to the same customer so the list can be narrowed down to exactly these two rows.
  const firstData = await setupBookingCreateData();
  const unitType = await createUnitType(
    getUnitTypeCreateData({ bookingPlans: getDefaultUnitTypeCreateWeeklyBookingPlans() })
  );
  const unit = await createUnit(getUnitCreateData(unitType));
  const secondData = getBookingCreateData({ customer: firstData.customer, unitType, unit });
  const bookings = [await createBooking(firstData), await createBooking(secondData)];
  const { currency, timezone } = firstData.location;

  await bookingListPage.goto();

  await test.step('select both bookings in the list', async () => {
    await bookingListPage.searchTextField.fill(getCustomerDisplayName(firstData.customer));
    await expect(bookingListPage.dataTable.getRows()).toHaveCount(2);
    await bookingListPage.selectBookings([0, 1]);
  });

  await test.step('apply one price change to both bookings', async () => {
    const priceChangeDialog = await bookingListPage.openBookingPriceChangeDialog();
    await priceChangeDialog.edit(priceChange, currency);
  });

  for (const booking of bookings) {
    await test.step(`verify the price adjustment on booking ${booking.id}`, async () => {
      await bookingDetailsPage.goto(booking.id);
      await verifyBookingPriceChangeSection(
        bookingDetailsPage,
        toBookingPriceChangeDetails(booking, priceChange),
        currency,
        timezone
      );
    });
  }
});

test('rejects a price change without all required fields', async ({
  setupBookingCreateData,
  createBooking,
  bookingListPage,
}) => {
  const booking = await createBooking(await setupBookingCreateData());

  await bookingListPage.goto();

  await test.step('select the booking in the list', async () => {
    await bookingListPage.searchTextField.fill(formatId(booking.id));
    await bookingListPage.selectBookings(0);
  });

  const priceChangeDialog = await test.step('submit the price change form', async () => {
    const dialog = await bookingListPage.openBookingPriceChangeDialog();
    await dialog.submit();
    return dialog;
  });

  await test.step('verify errors on form', async () => {
    await expect(priceChangeDialog.errors).toHaveCountGreaterThan(0);
  });
});

test('rejects a price change with an out-of-range value', async ({
  setupBookingCreateData,
  createBooking,
  bookingListPage,
}) => {
  // The magnitude caps differ per direction and value type: increases allow at most 1000 %, decreases at
  // most 100 % (a full waiver), and fixed amounts at most 5000.
  const invalidPriceChanges: { priceChange: BookingPriceChange; error: string }[] = [
    {
      priceChange: {
        option: 'increase',
        value: 1001,
        valueType: 'percentage',
        changeAt: getFutureDate(0, 'day'),
        adjustUnit: true,
        adjustRecurringProducts: false,
      },
      error: 'must have a maximum value of 1000',
    },
    {
      priceChange: {
        option: 'decrease',
        value: 101,
        valueType: 'percentage',
        changeAt: getFutureDate(0, 'day'),
        adjustUnit: true,
        adjustRecurringProducts: false,
      },
      error: 'must have a maximum value of 100',
    },
    {
      priceChange: {
        option: 'increase',
        value: 5001,
        valueType: 'amount',
        changeAt: getFutureDate(0, 'day'),
        adjustUnit: true,
        adjustRecurringProducts: false,
      },
      error: 'must have a maximum value of 5000',
    },
  ];

  const booking = await createBooking(await setupBookingCreateData());
  const currency = booking.location.currency;

  await bookingListPage.goto();

  await test.step('select the booking in the list', async () => {
    await bookingListPage.searchTextField.fill(formatId(booking.id));
    await bookingListPage.selectBookings(0);
  });

  const priceChangeDialog = await test.step('open the price change dialog', () =>
    bookingListPage.openBookingPriceChangeDialog());

  for (const { priceChange, error } of invalidPriceChanges) {
    await test.step(`verify ${priceChange.option} of ${priceChange.value} (${priceChange.valueType}) is rejected`, async () => {
      await priceChangeDialog.fill(priceChange, currency);
      await priceChangeDialog.submit();
      await expect(priceChangeDialog.errors).toContainText([error]);
    });
  }
});

test('rejects a price change targeting items the booking does not have', async ({
  setupBookingCreateData,
  createBooking,
  bookingListPage,
}) => {
  // Recurring products only, on a booking that has none: the server schedules nothing and reports it.
  const priceChange: BookingPriceChange = {
    option: 'increase',
    value: 10,
    valueType: 'percentage',
    changeAt: getFutureDate(0, 'day'),
    adjustUnit: false,
    adjustRecurringProducts: true,
  };

  const booking = await createBooking(await setupBookingCreateData());

  await bookingListPage.goto();

  await test.step('select the booking in the list', async () => {
    await bookingListPage.searchTextField.fill(formatId(booking.id));
    await bookingListPage.selectBookings(0);
  });

  const priceChangeDialog = await test.step('apply the price change', async () => {
    const dialog = await bookingListPage.openBookingPriceChangeDialog();
    await dialog.fill(priceChange, booking.location.currency);
    await dialog.submit();
    await dialog.confirmDialog.submit();
    return dialog;
  });

  await test.step('verify the error snackbar', async () => {
    await expect(priceChangeDialog.noBookingsAffectedSnackbar).toBeVisible();
  });
});

test('disables the price adjustment action for a scheduled booking', async ({
  setupBookingCreateData,
  createBooking,
  bookingListPage,
}) => {
  const booking = await createBooking(await setupBookingCreateData({}, { moveInDaysInTheFuture: 5 }));

  await bookingListPage.goto();

  await test.step('select the scheduled booking in the list', async () => {
    await bookingListPage.searchTextField.fill(formatId(booking.id));
    await bookingListPage.selectBookings(0);
  });

  await test.step('verify the price adjustment action is disabled', async () => {
    await expect(bookingListPage.priceChangeActionButton).toBeDisabled();
  });
});

test('disables the price adjustment action for a booking with a scheduled price change', async ({
  setupBookingCreateData,
  createBooking,
  createBookingPriceChange,
  bookingListPage,
}) => {
  const priceChange: BookingPriceChange = {
    option: 'increase',
    value: 10,
    valueType: 'percentage',
    changeAt: getFutureDate(0, 'day'),
    adjustUnit: true,
    adjustRecurringProducts: false,
  };

  const booking = await createBooking(await setupBookingCreateData());
  await createBookingPriceChange([booking.id], priceChange);

  await bookingListPage.goto();

  await test.step('select the booking in the list', async () => {
    await bookingListPage.searchTextField.fill(formatId(booking.id));
    await bookingListPage.selectBookings(0);
  });

  await test.step('verify the price adjustment action is disabled', async () => {
    await expect(bookingListPage.priceChangeActionButton).toBeDisabled();
  });
});
