import { test } from '@/manager/modules/invoice/invoice-fixtures';
import { toInvoiceCreateTotalsSummary } from '@/manager/modules/invoice/invoice-mappers';
import { invoiceChargeCustomerHappyTestCases } from '@/manager/modules/invoice/test-cases/invoice-charge-customer-test-cases';
import {
  expectDataTableChipsColumnToHaveText,
  expectDataTableTextColumnToHaveText,
} from '@/manager/modules/ui/data-table/data-table-assertions';
import { invoicePaymentTableColumnTestIds } from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { stripeCards } from '@/shared/data/stripe-card-data';
import { getDefaultCardPayment } from '@/shared/modules/payment-method/payment-method-factories';
import { formatCurrency, formatPaymentMethod } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';
import { pollOrSkip } from '@/shared/utils/poll-utils';

// The original charge lands asynchronously after creation and the charge settles asynchronously afterwards
// (direct debits to paid, and even a card charge attributes its method a beat late), so each case
// reload-polls twice — give the per-test budget headroom over the 30s default.
test.describe.configure({ mode: 'default', timeout: 120_000 });

for (const tc of invoiceChargeCustomerHappyTestCases) {
  test(tc.description, async ({ setupInvoiceCreateData, createInvoice, invoiceDetailsPage }) => {
    const data = await setupInvoiceCreateData(tc.setup);
    const invoice = await createInvoice(data);

    const currency = data.location.currency;
    const status = tc.expectedStatus ?? 'paid';
    const total = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, currency);
    const amountDue = status === 'paid' ? formatCurrency(0, currency) : total;
    const paymentStatus = status === 'paid' ? 'Succeeded' : 'Pending';
    // The charged method is only reliably attributed once the charge settles to paid. A pending row's
    // attribution timing is unbounded (observed both within seconds and well past the poll budget), so
    // it is asserted only for paid invoices.
    const paymentMethod = status === 'paid' ? formatPaymentMethod(tc.method) : undefined;

    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 charge has a stable state to act on.
      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 test.step('charge customer for invoice', async () => {
      const dialog = await invoiceDetailsPage.openInvoiceChargeCustomerDialog();
      await dialog.charge(tc.method);
    });

    await test.step('verify charged invoice on details page', async () => {
      // Charging pays the invoice's existing payment intent, so the original "not paid yet" charge row
      // transitions in place instead of a second row landing. The card charge settles synchronously; the
      // direct-debit (sepa/bacs) ones settle to paid asynchronously and only attribute the charged method
      // once settled, and the payments card only fetches on mount — reload-poll until everything is stable.
      await pollOrSkip(
        async () => {
          await invoiceDetailsPage.reload();

          await expectSingleLocatorToHaveText(invoiceDetailsPage.baseStatus, status);
          await expectSingleLocatorToHaveText(invoiceDetailsPage.generalCard.amountDue, amountDue);

          await expect(payments.getRows()).toHaveCount(1);
          await expectDataTableTextColumnToHaveText(payments, invoicePaymentTableColumnTestIds.amountInclVat, total);
          await expectDataTableChipsColumnToHaveText(payments, invoicePaymentTableColumnTestIds.status, paymentStatus);

          if (paymentMethod) {
            await expectDataTableTextColumnToHaveText(
              payments,
              invoicePaymentTableColumnTestIds.paymentMethod,
              paymentMethod
            );
          }
        },
        { timeout: 60_000, reason: 'charge did not settle within 60s — test-env queue backlog (KIN-4692)' }
      );
    });
  });
}

test('rejects charging an invoice with a declined card', async ({
  setupInvoiceCreateData,
  createInvoice,
  invoiceDetailsPage,
}) => {
  const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  const invoice = await createInvoice(data);

  const currency = data.location.currency;
  const total = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, currency);
  const declinedCard = getDefaultCardPayment(stripeCards.declinedOnCharge);

  const payments = invoiceDetailsPage.paymentsCard.dataTable;

  await test.step('wait for the invoice charge to be recorded', async () => {
    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)' }
    );
  });

  const dialog = await test.step('charge customer for invoice', async () => {
    const chargeDialog = await invoiceDetailsPage.openInvoiceChargeCustomerDialog();
    await chargeDialog.fill(declinedCard);
    await chargeDialog.submit();
    return chargeDialog;
  });

  await test.step('verify declined charge on details page', async () => {
    // A failed charge keeps the dialog open and raises an error snackbar.
    await expect(dialog.errorSnackbar).toHaveCountGreaterThan(0);
    await dialog.closeButton.click();

    // The declined attempt transitions the original charge row to failed via webhook — reload-poll until it
    // lands and confirm the invoice itself is unchanged.
    await pollOrSkip(
      async () => {
        await invoiceDetailsPage.reload();

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

        await expect(payments.getRows()).toHaveCount(1);
        await expectDataTableChipsColumnToHaveText(payments, invoicePaymentTableColumnTestIds.status, 'Failed');
      },
      { timeout: 60_000, reason: 'declined charge did not land within 60s — test-env queue backlog (KIN-4692)' }
    );
  });
});

test('hides the charge action for a paid invoice', async ({
  setupInvoiceCreateData,
  createInvoice,
  markInvoiceAsPaid,
  invoiceDetailsPage,
}) => {
  const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  const invoice = await createInvoice(data);
  await markInvoiceAsPaid(invoice.id);

  await invoiceDetailsPage.goto(invoice.id);

  await test.step('open invoice actions menu', async () => {
    await expectSingleLocatorToHaveText(invoiceDetailsPage.baseStatus, 'paid');
    await invoiceDetailsPage.actionMenuButton.click();
  });

  await test.step('verify charge action is absent', async () => {
    // Anchor on a sibling action to prove the menu is open before asserting the charge item's absence.
    await expect(invoiceDetailsPage.actionMenuIssueCreditNoteItem).toBeVisible();
    await expect(invoiceDetailsPage.actionMenuChargeCustomerItem).toHaveCount(0);
  });
});
