import { BookingItemsDetails, BookingPriceChangeDetails } from '@/manager/modules/booking/booking-types';
import { getBookingDepositInvoiceNumber } from '@/manager/modules/booking/booking-utils';
import { BookingAdjustmentAdjustedItemsTable } from '@/manager/modules/booking/components/BookingAdjustmentAdjustedItemsTable';
import { BookingAdjustmentCurrentItemsTable } from '@/manager/modules/booking/components/BookingAdjustmentCurrentItemsTable';
import { BookingDetailsItemsSection } from '@/manager/modules/booking/components/BookingDetailsItemsSection';
import { BookingDetailsPage } from '@/manager/modules/booking/views/BookingDetailsPage';
import {
  bookingPaymentTableColumnTestIds,
  invoiceTableColumnTestIds,
} from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { formatCurrency, formatDate } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';

export async function verifyBookingDetailsItemsSection(
  section: BookingDetailsItemsSection,
  data: BookingItemsDetails,
  currency: string
): Promise<void> {
  await expectSingleLocatorToHaveText(section.unitTypeLabel, data.unitType.name);
  await expectSingleLocatorToHaveText(section.unitLabel, data.unitType.unitName);
  await expectSingleLocatorToHaveText(
    section.unitTypePriceExclVat,
    formatCurrency(data.unitType.priceExclVat, currency)
  );

  if (data.insurance) {
    await expectSingleLocatorToHaveText(section.insuranceLabel, data.insurance.name);
    await expectSingleLocatorToHaveText(
      section.insurancePriceExclVat,
      formatCurrency(data.insurance.priceExclVat, currency)
    );
  } else {
    await expect(section.insuranceLabel).toBeHidden();
    await expect(section.insurancePriceExclVat).toBeHidden();
  }

  // The SPA renders recurring product rows in the order they were added to the booking (the API
  // orders by the booking_has_recurring_products pivot id — see orderByPivot('id')). That insertion
  // order matches data.recurringProducts, so iterate it as-is; sorting by product id is wrong because
  // parallel test setup assigns product ids in a non-deterministic order. The per-piece price line is
  // only shown for multi-piece items, so it is tracked with its own running index.
  const recurringProducts = data.recurringProducts;
  let perPieceIndex = 0;

  for (const [index, product] of recurringProducts.entries()) {
    // The label drops the "{quantity} x" prefix when only a single piece is booked.
    const label = product.quantity > 1 ? `${product.quantity} x ${product.name}` : product.name;
    await expectSingleLocatorToHaveText(section.recurringProductLabel.nth(index), label);
    await expectSingleLocatorToHaveText(
      section.recurringProductPriceExclVat.nth(index),
      formatCurrency(product.priceExclVat, currency)
    );

    if (product.quantity > 1) {
      await expectSingleLocatorToHaveText(
        section.recurringProductPricePerPiece.nth(perPieceIndex),
        `${formatCurrency(product.perPieceExclVat, currency)} per piece`
      );
      perPieceIndex++;
    }
  }

  await expectSingleLocatorToHaveText(section.totalExclVatValue, formatCurrency(data.totals.exclVat, currency));
  await expectSingleLocatorToHaveText(section.vatValue, formatCurrency(data.totals.vat, currency));
  await expectSingleLocatorToHaveText(section.totalInclVatValue, formatCurrency(data.totals.inclVat, currency));
}

export async function verifyBookingAdjustmentItemsTable(
  table: BookingAdjustmentCurrentItemsTable | BookingAdjustmentAdjustedItemsTable,
  data: BookingItemsDetails,
  currency: string
): Promise<void> {
  await expect(table.unitRow).toContainText(data.unitType.name);
  await expect(table.unitRow).toContainText(data.unitType.unitName);
  await expectSingleLocatorToHaveText(table.unitPriceValue, formatCurrency(data.unitType.priceExclVat, currency));

  if (data.insurance) {
    await expect(table.insuranceRow).toContainText(data.insurance.name);
    await expectSingleLocatorToHaveText(
      table.insurancePriceValue,
      formatCurrency(data.insurance.priceExclVat, currency)
    );
  } else {
    await expect(table.insuranceRow).toBeHidden();
  }

  await expect(table.recurringProductRows).toHaveCount(data.recurringProducts.length);

  for (const product of data.recurringProducts) {
    await expect(table.recurringProductRows.filter({ hasText: product.name })).toContainText(
      formatCurrency(product.priceExclVat, currency)
    );
  }
}

// Invoices are issued by an asynchronous backend job after the booking is created, and the invoices
// card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a
// settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the issued
// invoices render. The window is deliberately bounded: if issuance doesn't land within it (e.g. the
// billing job lagging under load), that is a real backend-latency signal — don't paper over it by
// bumping the timeout.
export async function verifyBookingInvoiceCount(detailsPage: BookingDetailsPage, expected: number): Promise<void> {
  await expect(detailsPage.invoicesCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 });
}

// Payments are recorded by the same asynchronous backend job that issues the invoices, and the payments
// card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a
// settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the payments
// render. The window is deliberately bounded: if the payment doesn't land within it (e.g. the billing
// job lagging under load), that is a real backend-latency signal — don't paper over it by bumping the timeout.
export async function verifyBookingPaymentCount(detailsPage: BookingDetailsPage, expected: number): Promise<void> {
  await expect(detailsPage.paymentsCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 });
}

// The whole price adjustment scene in the billing card: chip, new subscription amount, targets, and the
// snapped apply-from date. The SPA polls the booking while has_pending_changes is set, so the section
// appears without a reload once the backend has persisted the change.
export async function verifyBookingPriceChangeSection(
  detailsPage: BookingDetailsPage,
  expected: BookingPriceChangeDetails,
  currency: string,
  timezone: string
): Promise<void> {
  const section = detailsPage.billingCard.priceChangeSection;

  await expectSingleLocatorToHaveText(section.chip, expected.chip);
  await expectSingleLocatorToHaveText(
    section.newAmount,
    `New amount excl. VAT: ${formatCurrency(expected.newAmountExclVat, currency)}`
  );
  await expectSingleLocatorToHaveText(section.appliedOn, expected.appliedOn);
  await expectSingleLocatorToHaveText(
    section.appliedFrom,
    `applied from ${formatDate(expected.appliedFrom, timezone)}`
  );
}

// Asserts one invoice row in full. Pass `numberColumnText` to locate the row in the number column — an
// invoice number for a regular invoice, or the "Deposit" chip label for the deposit invoice (whose number
// cell also holds the chip, so it can't be matched by number) — needed when a booking has several invoices.
// Omit it for a booking with a single invoice, where it falls back to the first row. Covers every column
// the row carries besides the number itself. `unit` is optional — omit it for an invoice not tied to a
// unit (e.g. the upfront-deposit invoice), where the unit cell asserts empty.
export async function verifyBookingInvoice(
  detailsPage: BookingDetailsPage,
  expected: {
    status: string;
    paymentOption: string;
    unit?: string;
    amount: string;
    amountDue: string;
    issueDate: string;
    dueDate: string;
  },
  numberColumnText?: string
): Promise<void> {
  const invoices = detailsPage.invoicesCard.dataTable;
  const cell = (columnId: string) =>
    numberColumnText
      ? invoices.getRowColumnBy([{ columnId: invoiceTableColumnTestIds.id, text: numberColumnText }], columnId)
      : invoices.getRowColumnByIndex(0, columnId);

  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.status), expected.status);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.paymentOption), expected.paymentOption);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.unit), expected.unit);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amount), expected.amount);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amountDue), expected.amountDue);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.issueDate), expected.issueDate);
  await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.dueDate), expected.dueDate);
}

// A booking has one payment row per invoice in the payments grid, keyed by invoice number. Pass an
// invoiceNumber to scope the check to that invoice's payment — needed when a booking has several invoices
// (e.g. an upfront deposit's invoice alongside the first-period one), each with its own payment; omit it
// for a booking with a single payment. Asserts the row's gross amount, status, payment method, and date.
// Omit paymentMethod for an unpaid payment (e.g. a manual invoice's "Not paid yet" row): no method has
// been charged yet, so the cell is empty and the assertion checks it renders blank.
export async function verifyBookingPayment(
  detailsPage: BookingDetailsPage,
  expected: { amount: string; status: string; paymentMethod?: string; date: string },
  invoiceNumber?: string
): Promise<void> {
  const payments = detailsPage.paymentsCard.dataTable;
  const cell = (columnId: string) =>
    invoiceNumber
      ? payments.getRowColumnBy(
          [{ columnId: bookingPaymentTableColumnTestIds.invoiceId, text: invoiceNumber }],
          columnId
        )
      : payments.getRowColumnByIndex(0, columnId);

  await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.amountInclVat), expected.amount);
  await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.status), expected.status);
  await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.paymentMethod), expected.paymentMethod);
  await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.date), expected.date);
}

// The deposit field renders its amount as a link to the deposit invoice, but the link is only hydrated
// on a fresh load, not right after the booking is created. So grep the invoice's number from the grid,
// reload to mount the link, then assert the field reads "{depositAmount} ({number})" with the number
// rendered as the invoice link.
export async function verifyBookingDepositInvoiceLink(
  detailsPage: BookingDetailsPage,
  depositAmount: string
): Promise<void> {
  const invoiceNumber = await getBookingDepositInvoiceNumber(detailsPage.invoicesCard.dataTable);

  await detailsPage.reload();

  await expectSingleLocatorToHaveText(detailsPage.generalCard.depositLink, invoiceNumber);
  await expectSingleLocatorToHaveText(detailsPage.generalCard.deposit, `${depositAmount} (${invoiceNumber})`);
}
