import { getCustomerCreateData } from '@/manager/modules/customer/customer-factories';
import { getCustomerDisplayName } from '@/manager/modules/customer/customer-utils';
import { getInvoiceCreateData, getInvoiceMarkAsPaidData } from '@/manager/modules/invoice/invoice-factories';
import { test } from '@/manager/modules/invoice/invoice-fixtures';
import {
  toInvoiceCreateTotalsSummary,
  toInvoiceMarkAsPaidPaymentMethodLabel,
} from '@/manager/modules/invoice/invoice-mappers';
import { getProductOneTimeCreateData } from '@/manager/modules/product/product-factories';
import {
  invoicePaymentTableColumnTestIds,
  invoiceTableColumnTestIds,
} from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { locations } from '@/shared/data/seed-locations';
import { formatCurrency, formatDate } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';
import { pollOrSkip } from '@/shared/utils/poll-utils';
import { faker } from '@faker-js/faker';

// Bulk mark-as-paid only responds with 204 and queues a job per invoice that performs the actual status
// flip, so the post-mark list assertions reload-poll — give the per-test budget headroom over the 30s default.
test.describe.configure({ mode: 'default', timeout: 120_000 });

// The created invoices are recovered on the list page by searching the name of the customer they all share.
// Plain faker surnames collide with residue from earlier runs (teardown is disabled), so a unique suffix is
// appended.
function getSharedLastName(): string {
  return faker.person.lastName() + faker.string.alphanumeric(6);
}

test('marks a single selected invoice as paid', async ({
  createCustomer,
  createProductOneTime,
  createInvoice,
  invoiceListPage,
  invoiceDetailsPage,
}) => {
  const location = locations.viennaSouth;
  const [customer, product] = await Promise.all([
    createCustomer(getCustomerCreateData({ lastName: getSharedLastName(), locations: [location] })),
    createProductOneTime(getProductOneTimeCreateData({ location })),
  ]);
  const data = getInvoiceCreateData({ customer, products: [product] }, { location });
  const invoice = await createInvoice(data);

  const markAsPaidData = getInvoiceMarkAsPaidData();
  const currency = data.location.currency;
  const total = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, currency);
  const amountDue = formatCurrency(0, currency);
  // The payment date is a calendar date picked in the dialog, so format it in the runner's own zone (the
  // basis the date picker selects the day on) rather than the location's — the two can disagree across midnight.
  const paymentDate = formatDate(markAsPaidData.paymentDate);
  const paymentMethod = toInvoiceMarkAsPaidPaymentMethodLabel(markAsPaidData);

  const payments = invoiceDetailsPage.paymentsCard.dataTable;

  await test.step('wait for the invoice charge to be recorded', async () => {
    // An open invoice's charge is recorded asynchronously a few seconds after creation, and the payments
    // card only fetches on mount — reload-poll until it lands so the mark-as-paid flow has a charge to cancel.
    await pollOrSkip(
      async () => {
        await invoiceDetailsPage.goto(invoice.id);
        await expect(payments.getRows()).toHaveCount(1);
      },
      { timeout: 60_000, reason: 'invoice charge was not recorded within 60s — test-env queue backlog (KIN-4692)' }
    );
  });

  await invoiceListPage.goto();

  await test.step('select the invoice in the list', async () => {
    await invoiceListPage.searchTextField.fill(invoice.number);
    await expect(invoiceListPage.dataTable.getRows()).toHaveCount(1);
    await invoiceListPage.selectInvoices(0);
  });

  await test.step('mark the selected invoice as paid', async () => {
    const bulkMarkAsPaidDialog = await invoiceListPage.openInvoiceBulkMarkAsPaidDialog();
    await bulkMarkAsPaidDialog.mark(markAsPaidData);
  });

  await test.step('verify the marked invoice on the list page', async () => {
    await pollOrSkip(
      async () => {
        await invoiceListPage.reload();
        await invoiceListPage.searchTextField.fill(invoice.number);
        await expectSingleLocatorToHaveText(
          invoiceListPage.dataTable.getRowColumnByIndex(0, invoiceTableColumnTestIds.status),
          'paid'
        );
        await expectSingleLocatorToHaveText(
          invoiceListPage.dataTable.getRowColumnByIndex(0, invoiceTableColumnTestIds.amountDue),
          amountDue
        );
      },
      {
        timeout: 60_000,
        reason: 'bulk mark-as-paid job did not run within 60s — test-env queue backlog (KIN-4692)',
      }
    );
  });

  await test.step('verify the marked invoice on the details page', async () => {
    const chargeCell = (status: string, columnId: string) =>
      payments.getRowColumnBy([{ columnId: invoicePaymentTableColumnTestIds.status, text: status }], columnId);

    // Marking an open invoice paid out of band cancels its original charge and records a new succeeded one
    // for the same total, dated and attributed to the dialog's payment selection. The cancellation lands
    // asynchronously a few seconds after the new charge, and the payments card only fetches on mount, so
    // reload-poll until the invoice is settled and both charges have landed.
    await pollOrSkip(
      async () => {
        await invoiceDetailsPage.goto(invoice.id);

        await expectSingleLocatorToHaveText(invoiceDetailsPage.baseStatus, 'paid');
        await expectSingleLocatorToHaveText(invoiceDetailsPage.generalCard.amountDue, amountDue);

        await expect(payments.getRows()).toHaveCount(2);
        await expectSingleLocatorToHaveText(
          chargeCell('Cancelled', invoicePaymentTableColumnTestIds.amountInclVat),
          total
        );
        await expectSingleLocatorToHaveText(
          chargeCell('Succeeded', invoicePaymentTableColumnTestIds.amountInclVat),
          total
        );
        await expectSingleLocatorToHaveText(
          chargeCell('Succeeded', invoicePaymentTableColumnTestIds.date),
          paymentDate
        );
        await expectSingleLocatorToHaveText(
          chargeCell('Succeeded', invoicePaymentTableColumnTestIds.paymentMethod),
          paymentMethod
        );
      },
      {
        timeout: 60_000,
        reason: 'mark-as-paid charges did not settle within 60s — test-env queue backlog (KIN-4692)',
      }
    );
  });
});

test('marks multiple selected invoices as paid at once', async ({
  createCustomer,
  createProductOneTime,
  createInvoice,
  invoiceListPage,
}) => {
  const location = locations.viennaSouth;
  const [customer, product] = await Promise.all([
    createCustomer(getCustomerCreateData({ lastName: getSharedLastName(), locations: [location] })),
    createProductOneTime(getProductOneTimeCreateData({ location })),
  ]);
  const data = getInvoiceCreateData({ customer, products: [product] }, { location });
  const invoiceCount = 3;
  await Promise.all(Array.from({ length: invoiceCount }, () => createInvoice(data)));

  const customerName = getCustomerDisplayName(customer);
  const markAsPaidData = getInvoiceMarkAsPaidData();
  const currency = data.location.currency;
  const total = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, currency);
  const amountDue = formatCurrency(0, currency);

  const rowCell = (invoiceNumber: string, columnId: string) =>
    invoiceListPage.dataTable.getRowColumnBy(
      [{ columnId: invoiceTableColumnTestIds.id, text: invoiceNumber }],
      columnId
    );

  await invoiceListPage.goto();

  // The first two rows get marked; the last row stays unselected as the control that the bulk action
  // only touches the selection. The row-to-invoice mapping is read off the number cells since the list
  // order of same-day invoices is not guaranteed to follow creation order.
  const rowNumbers = await test.step('select the invoices in the list', async () => {
    await invoiceListPage.searchTextField.fill(customerName);
    await expect(invoiceListPage.dataTable.getRows()).toHaveCount(invoiceCount);

    const numbers = await Promise.all(
      Array.from({ length: invoiceCount }, async (_, index) =>
        (await invoiceListPage.dataTable.getRowColumnByIndex(index, invoiceTableColumnTestIds.id).innerText()).trim()
      )
    );
    await invoiceListPage.selectInvoices([0, 1]);
    return numbers;
  });
  const markedNumbers = rowNumbers.slice(0, 2);
  const controlNumber = rowNumbers[2];

  await test.step('mark the selected invoices as paid', async () => {
    const bulkMarkAsPaidDialog = await invoiceListPage.openInvoiceBulkMarkAsPaidDialog();
    await bulkMarkAsPaidDialog.mark(markAsPaidData);
  });

  await test.step('verify the marked and unselected invoices on the list page', async () => {
    await pollOrSkip(
      async () => {
        await invoiceListPage.reload();
        await invoiceListPage.searchTextField.fill(customerName);

        for (const invoiceNumber of markedNumbers) {
          await expectSingleLocatorToHaveText(rowCell(invoiceNumber, invoiceTableColumnTestIds.status), 'paid');
          await expectSingleLocatorToHaveText(rowCell(invoiceNumber, invoiceTableColumnTestIds.amountDue), amountDue);
        }

        await expectSingleLocatorToHaveText(rowCell(controlNumber, invoiceTableColumnTestIds.status), 'open');
        await expectSingleLocatorToHaveText(rowCell(controlNumber, invoiceTableColumnTestIds.amountDue), total);
      },
      {
        timeout: 60_000,
        reason: 'bulk mark-as-paid jobs did not run within 60s — test-env queue backlog (KIN-4692)',
      }
    );
  });
});

test('disables the bulk mark as paid action for a selection with a non-markable invoice', async ({
  createCustomer,
  createProductOneTime,
  createInvoice,
  markInvoiceAsPaid,
  invoiceListPage,
}) => {
  const location = locations.viennaSouth;
  const [customer, product] = await Promise.all([
    createCustomer(getCustomerCreateData({ lastName: getSharedLastName(), locations: [location] })),
    createProductOneTime(getProductOneTimeCreateData({ location })),
  ]);
  const data = getInvoiceCreateData({ customer, products: [product] }, { location });
  const customerName = getCustomerDisplayName(customer);

  // Only open and past-due invoices can be marked as paid, and a single invalid invoice disables the bulk
  // action for the whole selection — so one of the two gets paid up front (synchronously) via the API.
  const [paidInvoice] = await Promise.all([createInvoice(data), createInvoice(data)]);
  await markInvoiceAsPaid(paidInvoice.id);

  await invoiceListPage.goto();

  await test.step('select the invoices in the list', async () => {
    await invoiceListPage.searchTextField.fill(customerName);
    await expect(invoiceListPage.dataTable.getRows()).toHaveCount(2);
    await invoiceListPage.selectInvoices([0, 1]);
  });

  await test.step('verify the bulk mark as paid action is disabled', async () => {
    await expect(invoiceListPage.bulkMarkAsPaidActionButton).toBeDisabled();
  });
});
