import { getCustomerCreateData } from '@/manager/modules/customer/customer-factories';
import { getCustomerDisplayName } from '@/manager/modules/customer/customer-utils';
import { getInvoiceCreateData } from '@/manager/modules/invoice/invoice-factories';
import { test } from '@/manager/modules/invoice/invoice-fixtures';
import { toInvoiceCreateTotalsSummary } from '@/manager/modules/invoice/invoice-mappers';
import { invoiceCreateHappyTestCases } from '@/manager/modules/invoice/test-cases/invoice-create-test-cases';
import { getProductOneTimeCreateData } from '@/manager/modules/product/product-factories';
import {
  expectDataTableChipsColumnToHaveText,
  expectDataTableTextColumnToHaveText,
} from '@/manager/modules/ui/data-table/data-table-assertions';
import {
  invoicePaymentTableColumnTestIds,
  invoiceTableColumnTestIds,
} from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
import { verifyTotalsSummarySection } from '@/manager/modules/ui/totals-summary/totals-summary-assertions';
import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { locations } from '@/shared/data/seed-locations';
import { getFutureDate } from '@/shared/utils/date-utils';
import { formatAddress, formatCurrency, formatDate, formatPaymentMethod, formatPhone } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';
import { pollOrSkip } from '@/shared/utils/poll-utils';

// Each case stands up its customer + products via API and the create flow finalizes the invoice in Stripe
// (card cases also add a payment method), so the default per-test timeout is too tight — allow headroom.
test.describe.configure({ mode: 'default', timeout: 120_000 });

for (const tc of invoiceCreateHappyTestCases) {
  test(tc.description, async ({ setupInvoiceCreateData, invoiceCreateDialog }) => {
    const data = await setupInvoiceCreateData(tc.setup, tc.options);

    const currency = data.location.currency;
    const locationName = data.location.name;
    const status = tc.expectedStatus ?? (data.payment.option === 'charge_automatically' ? 'paid' : 'open');
    const issueDate = formatDate(new Date(), data.location.timezone);
    const dueDate = formatDate(
      getFutureDate(data.payment.manual?.daysUntilDue ?? data.location.invoiceDaysUntilDue, 'day')
    );
    const customerName = getCustomerDisplayName(data.customer);
    const customerPhone = formatPhone(data.customer.phone);
    const customerBillingAddress = formatAddress(
      data.customer.street,
      data.customer.postalCode,
      data.customer.city,
      data.customer.country.name
    );
    const totalsSummary = toInvoiceCreateTotalsSummary(data);
    const totalInclVat = formatCurrency(totalsSummary.totalInclVat, currency);
    const amountDue = status === 'paid' ? formatCurrency(0, currency) : totalInclVat;
    const paymentOption = data.payment.option === 'charge_automatically' ? 'Auto charge' : 'Manual';
    const paymentStatus = status === 'paid' ? 'Succeeded' : status === 'pending payment' ? 'Pending' : 'Not paid yet';
    // The charged method is only attributed once the charge succeeds, so a pending-payment row (like a manual
    // not-paid-yet one) shows none — only expect a method on a settled/paid invoice.
    const paymentMethod =
      status === 'paid' && data.payment.method ? formatPaymentMethod(data.payment.method) : undefined;

    await invoiceCreateDialog.goto();

    await test.step('fill invoice create form', () => invoiceCreateDialog.fill(data));

    await test.step('verify totals summary in dialog', () =>
      verifyTotalsSummarySection(invoiceCreateDialog.productsFieldSet.totalsSummarySection, totalsSummary, currency));

    await test.step('submit invoice create form', () => invoiceCreateDialog.submit());

    const detailsPage = await test.step('confirm invoice create', () => invoiceCreateDialog.confirm());

    await test.step('verify new invoice on details page', async () => {
      await expect(detailsPage.title).not.toBeEmpty();

      // A direct-debit (sepa/bacs) auto-charge is created pending and settles to paid asynchronously, and the
      // details page doesn't refetch on its own — reload-poll the status together with the amounts that settle
      // alongside it: a paid invoice's amount due only drops to zero once its charge is applied, a beat after
      // the status flips. Card (synchronous) and manual pass on the first iteration.
      await pollOrSkip(
        async () => {
          await detailsPage.reload();
          await expectSingleLocatorToHaveText(detailsPage.baseStatus, status);
          await expectSingleLocatorToHaveText(detailsPage.generalCard.total, totalInclVat);
          await expectSingleLocatorToHaveText(detailsPage.generalCard.amountDue, amountDue);
        },
        {
          timeout: 90_000,
          reason: 'invoice status/amounts did not settle within 90s — test-env queue backlog (KIN-4692)',
        }
      );

      await expectSingleLocatorToHaveText(detailsPage.generalCard.location, locationName);
      await expectSingleLocatorToHaveText(detailsPage.generalCard.issueDate, issueDate);

      if (data.payment.manual) {
        await expectSingleLocatorToHaveText(detailsPage.generalCard.dueDate, dueDate);
      } else {
        // TODO: visibility-only fallback because the due date is sometimes not immediately shown for non-manual payments
        await expect(detailsPage.generalCard.dueDate).toBeVisible();
      }

      await expectSingleLocatorToHaveText(detailsPage.generalCard.period, undefined);
      await expectSingleLocatorToHaveText(detailsPage.generalCard.paymentOption, paymentOption);
      await expect(detailsPage.generalCard.paymentLink).not.toBeEmpty();
      await expectSingleLocatorToHaveText(detailsPage.customerCard.name, customerName);
      await expectSingleLocatorToHaveText(detailsPage.customerCard.type, data.customer.type);

      if (data.customer.type === 'business') {
        // The VAT number field is only rendered when the customer has a VAT number defined.
        if (data.customer.vatNumber) {
          await expectSingleLocatorToHaveText(detailsPage.customerCard.vatNumber, data.customer.vatNumber);
        } else {
          await expect(detailsPage.customerCard.vatNumber).toBeHidden();
        }

        // The VAT type field is only rendered when the customer has a VAT type defined.
        if (data.customer.vatType) {
          await expectSingleLocatorToHaveText(detailsPage.customerCard.vatType, data.customer.vatType.name);
        } else {
          await expect(detailsPage.customerCard.vatType).toBeHidden();
        }
      } else {
        await expect(detailsPage.customerCard.vatNumber).toBeHidden();
        await expect(detailsPage.customerCard.vatType).toBeHidden();
      }

      await expectSingleLocatorToHaveText(detailsPage.customerCard.email, data.customer.email);
      await expectSingleLocatorToHaveText(detailsPage.customerCard.phoneNumber, customerPhone);
      await expectSingleLocatorToHaveText(detailsPage.customerCard.billingAddress, customerBillingAddress);
    });

    const invoiceId = await detailsPage.getInvoiceId();

    await test.step('verify invoice payments', async () => {
      const payments = detailsPage.paymentsCard.dataTable;

      // Every invoice lands a single payment row, keyed to the invoice's own number, for the full total; the
      // status, date and (for auto-charge) the charged method ride along. Unlike the booking payments card, this
      // card fetches once on mount and never polls, so an auto-charge payment recorded asynchronously after that
      // fetch only surfaces on reload — reload-poll until the row lands. The charged method is populated a beat
      // after the row first appears, so gate the poll on it too; it's empty for a manual invoice's "not paid yet"
      // placeholder and for a pending-payment charge (the method is only attributed once the charge succeeds),
      // which pass on the first iteration.
      await pollOrSkip(
        async () => {
          await detailsPage.reload();
          await expect(payments.getRows()).toHaveCount(1);
          await expectDataTableTextColumnToHaveText(
            payments,
            invoicePaymentTableColumnTestIds.paymentMethod,
            paymentMethod
          );
        },
        { timeout: 60_000, reason: 'invoice payment row did not land within 60s — test-env queue backlog (KIN-4692)' }
      );

      await expectDataTableTextColumnToHaveText(payments, invoicePaymentTableColumnTestIds.amountInclVat, totalInclVat);
      await expectDataTableChipsColumnToHaveText(payments, invoicePaymentTableColumnTestIds.status, paymentStatus);
      await expectDataTableTextColumnToHaveText(payments, invoicePaymentTableColumnTestIds.invoiceId, invoiceId);
      await expectDataTableTextColumnToHaveText(payments, invoicePaymentTableColumnTestIds.date, issueDate);
    });

    await test.step('verify new invoice on list page', async () => {
      const listPage = await detailsPage.returnToInvoiceListPage();

      await listPage.searchTextField.fill(invoiceId);

      await expect(listPage.dataTable.getRows()).toHaveCount(1);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.id, invoiceId);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.location, locationName);
      await expectDataTableChipsColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.status, status);
      await expectDataTableTextColumnToHaveText(
        listPage.dataTable,
        invoiceTableColumnTestIds.paymentOption,
        paymentOption
      );

      if (data.payment.manual) {
        await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.dueDate, dueDate);
      } else {
        // TODO: visibility-only fallback because the due date is sometimes not immediately shown for non-manual payments
        await expect(listPage.dataTable.getColumn(invoiceTableColumnTestIds.dueDate)).toBeVisible();
      }

      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.customer, customerName);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.unit, undefined);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.amount, totalInclVat);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.amountDue, amountDue);
      await expectDataTableTextColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.issueDate, issueDate);
    });
  });
}

test('creates an invoice for a customer having multiple locations', async ({
  createCustomer,
  createProductOneTime,
  invoiceCreateDialog,
}) => {
  const location = locations.viennaSouth;
  const [customer, product] = await Promise.all([
    createCustomer(getCustomerCreateData({ locations: [location, locations.viennaNorth] })),
    createProductOneTime(getProductOneTimeCreateData({ location })),
  ]);
  const data = getInvoiceCreateData({ customer, products: [product] }, { location });

  await invoiceCreateDialog.goto();

  const detailsPage = await test.step('create invoice', () => invoiceCreateDialog.create(data));

  await test.step('verify new invoice on details page', async () => {
    await expectSingleLocatorToHaveText(detailsPage.customerCard.name, getCustomerDisplayName(data.customer));
    await expectSingleLocatorToHaveText(detailsPage.generalCard.location, location.name);
  });
});

test('rejects an invoice without all required fields', async ({ setupInvoiceCreateData, invoiceCreateDialog }) => {
  const data = await setupInvoiceCreateData(
    { customerType: 'private', numberOfProducts: 0 },
    { paymentOption: 'send_invoice' }
  );

  await invoiceCreateDialog.goto();

  await test.step('submit invalid invoice create form', async () => {
    await invoiceCreateDialog.fill(data);
    await invoiceCreateDialog.submit();
  });

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