import { test as customerTest } from '@/manager/modules/customer/customer-fixtures';
import { test as invoiceTest } from '@/manager/modules/invoice/invoice-fixtures';
import { toInvoiceCreateTotalsSummary } from '@/manager/modules/invoice/invoice-mappers';
import { invoicePayWithBalanceHappyTestCases } from '@/manager/modules/invoice/test-cases/invoice-pay-with-balance-test-cases';
import {
  customerBalanceTransactionTableColumnTestIds,
  invoicePaymentTableColumnTestIds,
  invoiceTableColumnTestIds,
} from '@/manager/modules/ui/data-table/data/data-table-column-test-ids';
import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils';
import { formatCurrency } from '@/shared/utils/formatters';
import { expect } from '@/shared/utils/matchers';
import { pollOrSkip } from '@/shared/utils/poll-utils';
import { mergeTests } from '@playwright/test';

// The customer fixtures provide the balance seeding; the invoice fixtures' createCustomer wins the merge,
// so setupInvoiceCreateData keeps tracking its customer into the invoice teardown.
const test = mergeTests(customerTest, invoiceTest);

// The original charge lands asynchronously after creation and the cancellation lands asynchronously after the
// pay, 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 invoicePayWithBalanceHappyTestCases) {
  test(
    tc.description,
    async ({
      setupInvoiceCreateData,
      createCustomerBalanceTransaction,
      createInvoice,
      invoiceDetailsPage,
      customerDetailsPage,
    }) => {
      const data = await setupInvoiceCreateData({ numberOfProducts: 1 });

      const currency = data.location.currency;
      const totalInclVat = toInvoiceCreateTotalsSummary(data).totalInclVat;
      const total = formatCurrency(totalInclVat, currency);
      const amountDue = formatCurrency(0, currency);
      const initialBalance = formatCurrency(totalInclVat + tc.balanceSurplus, currency);
      const remainingBalance = formatCurrency(tc.balanceSurplus, currency);
      const balancePaymentMethodLabel = 'Stripe balance';

      const invoice = await createInvoice(data);

      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 pay 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)' }
        );
      });

      // Seed the balance only after the invoice's charge is recorded: Stripe automatically consumes any
      // pre-existing customer balance when an invoice is created, which would settle the invoice outright
      // instead of leaving it open for the pay action.
      await createCustomerBalanceTransaction(data.customer.id, data.location.id, {
        type: 'credit',
        amount: totalInclVat + tc.balanceSurplus,
        paymentMethod: 'cash',
        paymentDate: new Date(),
      });

      const dialog = await test.step('open pay with balance dialog', async () => {
        // The charge wait left the page on a fetch from before the balance was seeded, so the pay action
        // is still stale-disabled — reload to pick up the balance before opening the menu.
        await invoiceDetailsPage.reload();
        return invoiceDetailsPage.openInvoicePayWithBalanceDialog();
      });

      await test.step('verify balance preview on dialog', async () => {
        await expectSingleLocatorToHaveText(dialog.currentBalance, initialBalance);
        await expectSingleLocatorToHaveText(dialog.newBalance, remainingBalance);
      });

      await test.step('pay invoice with balance', () => dialog.pay());

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

        // Paying with balance cancels the invoice's original charge and records a new succeeded one for the
        // same total, attributed to the customer's balance. 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.reload();

            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.paymentMethod),
              balancePaymentMethodLabel
            );
          },
          {
            timeout: 60_000,
            reason: 'pay-with-balance charges did not settle within 60s — test-env queue backlog (KIN-4692)',
          }
        );
      });

      await test.step('verify customer balance on details page', async () => {
        await customerDetailsPage.goto(data.customer.id);
        await expectSingleLocatorToHaveText(customerDetailsPage.runningBalanceCard.balanceAmount, remainingBalance);
      });

      await test.step('verify payment in transaction history', async () => {
        const historyDialog = await customerDetailsPage.openCustomerBalanceTransactionsDialog();
        const table = historyDialog.dataTable;
        const columns = customerBalanceTransactionTableColumnTestIds;
        // The history lists transactions newest first, so the balance payment is the first row.
        const row = (columnId: string) => table.getRowColumnByIndex(0, columnId);

        await expect(table.getRows()).toHaveCount(2);
        await expectSingleLocatorToHaveText(row(columns.type), 'Invoice payment');
        await expectSingleLocatorToHaveText(row(columns.paymentMethod), balancePaymentMethodLabel);
        await expectSingleLocatorToHaveText(row(columns.credit), undefined);
        await expectSingleLocatorToHaveText(row(columns.debit), total);
        await expectSingleLocatorToHaveText(row(columns.balance), remainingBalance);
        await expectSingleLocatorToHaveText(row(columns.invoice), invoice.number);
      });
    }
  );
}

test('pays an invoice with balance from the invoice list', async ({
  setupInvoiceCreateData,
  createCustomerBalanceTransaction,
  createInvoice,
  invoiceDetailsPage,
  invoiceListPage,
}) => {
  const data = await setupInvoiceCreateData({ numberOfProducts: 1 });

  const totalInclVat = toInvoiceCreateTotalsSummary(data).totalInclVat;
  const amountDue = formatCurrency(0, data.location.currency);
  const balanceSurplus = 50;

  const invoice = await createInvoice(data);

  await test.step('wait for the invoice charge to be recorded', async () => {
    await pollOrSkip(
      async () => {
        await invoiceDetailsPage.goto(invoice.id);
        await expect(invoiceDetailsPage.paymentsCard.dataTable.getRows()).toHaveCount(1);
      },
      { timeout: 60_000, reason: 'invoice charge was not recorded within 60s — test-env queue backlog (KIN-4692)' }
    );
  });

  // Seed the balance only after the invoice's charge is recorded: Stripe automatically consumes any
  // pre-existing customer balance when an invoice is created, which would settle the invoice outright
  // instead of leaving it open for the pay action.
  await createCustomerBalanceTransaction(data.customer.id, data.location.id, {
    type: 'credit',
    amount: totalInclVat + balanceSurplus,
    paymentMethod: 'cash',
    paymentDate: new Date(),
  });

  await invoiceListPage.goto();

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

  await test.step('pay invoice with balance', async () => {
    const dialog = await invoiceListPage.openInvoicePayWithBalanceDialog(0);
    await dialog.pay();
  });

  await test.step('verify paid invoice on list page', async () => {
    await expectSingleLocatorToHaveText(
      invoiceListPage.dataTable.getRowColumnByIndex(0, invoiceTableColumnTestIds.status),
      'paid'
    );
    await expectSingleLocatorToHaveText(
      invoiceListPage.dataTable.getRowColumnByIndex(0, invoiceTableColumnTestIds.amountDue),
      amountDue
    );
  });
});

test('disables the pay with balance action when the balance is insufficient', async ({
  setupInvoiceCreateData,
  createCustomerBalanceTransaction,
  createInvoice,
  invoiceDetailsPage,
}) => {
  const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  const totalInclVat = toInvoiceCreateTotalsSummary(data).totalInclVat;

  const invoice = await createInvoice(data);

  await test.step('wait for the invoice charge to be recorded', async () => {
    await pollOrSkip(
      async () => {
        await invoiceDetailsPage.goto(invoice.id);
        await expect(invoiceDetailsPage.paymentsCard.dataTable.getRows()).toHaveCount(1);
      },
      { timeout: 60_000, reason: 'invoice charge was not recorded within 60s — test-env queue backlog (KIN-4692)' }
    );
  });

  // Seed a balance one unit short of the amount due — enough to prove partial balances don't qualify. Seeded
  // only after the invoice's charge is recorded: Stripe automatically consumes any pre-existing customer
  // balance when an invoice is created, which would reduce the amount due instead of leaving it untouched.
  await createCustomerBalanceTransaction(data.customer.id, data.location.id, {
    type: 'credit',
    amount: totalInclVat - 1,
    paymentMethod: 'cash',
    paymentDate: new Date(),
  });

  await invoiceDetailsPage.goto(invoice.id);

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

  await test.step('verify pay with balance action is disabled', async () => {
    await expect(invoiceDetailsPage.actionMenuPayWithBalanceItem).toHaveClass(/v-list-item--disabled/);
  });
});

test('hides the pay with balance 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 pay with balance action is absent', async () => {
    await expect(invoiceDetailsPage.actionMenuIssueCreditNoteItem).toBeVisible();
    await expect(invoiceDetailsPage.actionMenuPayWithBalanceItem).toHaveCount(0);
  });
});
