import { test } from '@/manager/modules/invoice/invoice-fixtures';
import { getCustomerCreateData } from '@/manager/modules/customer/customer-factories';
import { getInvoiceCreateData, getInvoiceIssueCreditNoteData } from '@/manager/modules/invoice/invoice-factories';
import {
  toInvoiceCreateTotalsSummary,
  toInvoiceCreditNoteRefundMethodLabel,
  toInvoiceIssueCreditNoteTotalsSummary,
} from '@/manager/modules/invoice/invoice-mappers';
import { invoiceIssueCreditNoteHappyTestCases } from '@/manager/modules/invoice/test-cases/invoice-issue-credit-note-test-cases';
import { getProductOneTimeCreateData } from '@/manager/modules/product/product-factories';
import { expectDataTableChipsColumnToHaveText } from '@/manager/modules/ui/data-table/data-table-assertions';
import { 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 { ProductOneTime } from '@/shared/types/product-types';
import { expect } from '@/shared/utils/matchers';
import { pollOrSkip } from '@/shared/utils/poll-utils';

// The credit-note line amounts/quantities in the test cases assume these fixed product prices, so the invoice
// products are created with them (not faker defaults). The invoice is API-created open and, for the paid
// cases, marked paid via API (out of band) before the credit-note flow — the system under test — runs.
const location = locations.viennaSouth;
const productPrices = [200, 100];

test.describe.configure({ mode: 'default', timeout: 120_000 });

for (const tc of invoiceIssueCreditNoteHappyTestCases) {
  test(
    tc.description,
    async ({ createCustomer, createProductOneTime, createInvoice, markInvoiceAsPaid, invoiceDetailsPage }) => {
      const customer = await createCustomer(getCustomerCreateData({ type: 'private', locations: [location] }));
      const products: ProductOneTime[] = [];

      for (let index = 0; index < tc.setup.numberOfProducts; index++) {
        products.push(
          await createProductOneTime(getProductOneTimeCreateData({ location, priceExclVat: productPrices[index] }))
        );
      }

      const invoiceData = getInvoiceCreateData({ customer, products });
      const creditData = getInvoiceIssueCreditNoteData(invoiceData, tc.credit);

      const currency = invoiceData.location.currency;
      const invoiceTotals = toInvoiceCreateTotalsSummary(invoiceData);
      const creditTotals = toInvoiceIssueCreditNoteTotalsSummary(creditData);
      const issuedFullCredit = invoiceTotals.totalInclVat === creditTotals.totalInclVat;
      const invoiceStatus = tc.setup.paid ? 'paid' : 'open';
      // An open invoice lands a single charge; marking it paid out of band cancels that charge and records a
      // new succeeded one, so a paid invoice carries two.
      const expectedChargeCount = tc.setup.paid ? 2 : 1;
      const newStatus = issuedFullCredit ? 'paid' : invoiceStatus;
      const creditNoteStatus = 'credit note issued';
      const refundMethod = toInvoiceCreditNoteRefundMethodLabel(creditData.option);

      const invoice = await createInvoice(invoiceData);

      if (tc.setup.paid) {
        await markInvoiceAsPaid(invoice.id);
      }

      const payments = invoiceDetailsPage.paymentsCard.dataTable;

      await invoiceDetailsPage.goto(invoice.id);

      // The credit note is issued against the invoice's charge(s), which land asynchronously after creation
      // (and, for the paid cases, after the out-of-band mark settles the original charge into a cancelled +
      // succeeded pair) — and the payments card only fetches on mount. Reload-poll until the header reflects
      // the expected status and the expected charge count has landed before opening the dialog: issuing a
      // credit note against the wrong (or zero) charge count is meaningless, so fail fast if it never settles.
      // The status gate also matters for the paid cases — the mark-as-paid API is acknowledged before the
      // status projection settles, and opening the dialog while a paid invoice still reads 'open' would miss
      // the refund-method chooser only a paid invoice offers (and that these cases select), failing the fill.
      await test.step('wait for the invoice charges to settle', () =>
        pollOrSkip(
          async () => {
            await invoiceDetailsPage.reload();
            await expectSingleLocatorToHaveText(invoiceDetailsPage.baseStatus, invoiceStatus);
            await expect(payments.getRows()).toHaveCount(expectedChargeCount);
          },
          { timeout: 60_000, reason: 'invoice charges did not settle within 60s — test-env queue backlog (KIN-4692)' }
        ));

      const dialog = await test.step('open invoice issue credit note dialog', () =>
        invoiceDetailsPage.openInvoiceIssueCreditNoteDialog());

      await test.step('verify initial totals summary in dialog', () =>
        verifyTotalsSummarySection(dialog.itemsFieldSet.totalsSummarySection, invoiceTotals, currency));

      await test.step('fill invoice issue credit note form', () => dialog.fill(creditData));

      await test.step('verify totals summary in dialog', () =>
        verifyTotalsSummarySection(dialog.itemsFieldSet.totalsSummarySection, creditTotals, currency));

      await test.step('submit invoice issue credit note form', () => dialog.submit());

      const newDetailsPage = await test.step('confirm invoice issue credit note', () => dialog.confirm());

      await test.step('verify credit note issued on details page', async () => {
        // A full credit note flips an open invoice to 'paid' server-side, but the details page doesn't refetch
        // the status after issuing — it stays stale until a reload. Reload-poll so the assertion reads the
        // settled status rather than the stale one.
        await pollOrSkip(
          async () => {
            await newDetailsPage.reload();
            await expectSingleLocatorToHaveText(newDetailsPage.baseStatus, newStatus);
            await expectSingleLocatorToHaveText(newDetailsPage.creditNoteIssuedStatus, creditNoteStatus);
            await expectSingleLocatorToHaveText(newDetailsPage.generalCard.refundMethod, refundMethod);
          },
          {
            timeout: 30_000,
            reason: 'credit note status did not settle within 30s — test-env queue backlog (KIN-4692)',
          }
        );
      });

      await test.step('verify credit note issued on list page', async () => {
        const listPage = await newDetailsPage.returnToInvoiceListPage();

        await listPage.searchTextField.fill(invoice.number);

        await expect(listPage.dataTable.getRows()).toHaveCount(1);
        await expectDataTableChipsColumnToHaveText(listPage.dataTable, invoiceTableColumnTestIds.status, [
          newStatus,
          creditNoteStatus,
        ]);
      });
    }
  );
}

test('rejects issuing a credit note without all required fields', async ({
  setupInvoiceCreateData,
  createInvoice,
  invoiceDetailsPage,
}) => {
  const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  const invoice = await createInvoice(data);

  await invoiceDetailsPage.goto(invoice.id);

  const dialog = await test.step('submit invalid invoice issue credit note form', async () => {
    const creditNoteDialog = await invoiceDetailsPage.openInvoiceIssueCreditNoteDialog();
    await creditNoteDialog.submit();
    return creditNoteDialog;
  });

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