// Unit tests for mapPgError (Task 2 of docs/superpowers/plans/2026-07-31-local-app.md).
// Pure function — constructed error-shaped objects, no database needed.
// Message strings are lifted verbatim from db/migrations/0002_ledger_core.sql
// and 0003_import_layer.sql so these tests stay honest about what the
// database actually raises.

import { describe, it, expect } from "vitest";
import { mapPgError } from "../../server/src/errors.js";
import { mapDuplicateCheckNo } from "../../server/src/billPayments.js";

function pgErr(code: string, message: string, extra: Record<string, unknown> = {}): Error {
  return Object.assign(new Error(message), { code, ...extra });
}

describe("mapPgError", () => {
  it("maps 'must have at least two lines' (assert_entry_balanced)", () => {
    const err = pgErr("P0001", "journal entry 42 must have at least two lines");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/at least two lines/i);
  });

  it("maps 'is out of balance by' and surfaces the amount", () => {
    const err = pgErr("P0001", "journal entry 42 is out of balance by 12.5000");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toContain("12.50");
  });

  it("maps 'cannot post into locked period' to the exact final message (no stray paren)", () => {
    const err = pgErr("P0001", "cannot post into locked period (locked through 2023-12-31)");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    // Regression check: an earlier \S+ capture swallowed the message's own
    // closing ')', producing "...(through 2023-12-31))" — assert the WHOLE
    // string so a stray paren can never silently ship again.
    expect(mapped.message).toBe(
      "That year is locked (through 2023-12-31); record it in the current year or unlock the period first.",
    );
  });

  it("maps posted-entry immutability (forbid_posted_entry_change)", () => {
    const err = pgErr("P0001", "posted entries are immutable; create a reversing entry instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/reversing/i);
  });

  it("maps posted-line immutability (forbid_posted_line_change)", () => {
    const err = pgErr(
      "P0001", "lines of a posted entry are immutable; create a reversing entry instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/reversing/i);
  });

  it("maps 'has already been reversed' (reverse_entry_internal)", () => {
    const err = pgErr("P0001", "entry 7 has already been reversed");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already been reversed/i);
  });

  it("maps reverse_entry's inventory guard (0018) instead of leaking a 500", () => {
    const err = pgErr(
      "P0001",
      "entry 12 moved stock, so reversing it on its own would leave the quantity on hand wrong — void the document it came from, or record an inventory adjustment instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/moved stock/i);
    expect(mapped.message).toMatch(/inventory adjustment/i);
    expect(mapped.message).not.toMatch(/—/); // plain English, not the raw RAISE text
  });

  it("maps the stock/money mismatch (0018 check_movement_entry_posted) and steers the user", () => {
    const err = pgErr(
      "P0001",
      "journal entry 9 moves 50.0000 of stock through inventory account 41, but posts 5.0000 to it — the books and the shelf have to say the same thing");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/pick the item instead/i);
    expect(mapped.message).not.toMatch(/journal entry 9/); // no raw internals
  });

  it("maps the one-transaction rule (0018)", () => {
    const err = pgErr(
      "P0001",
      "journal entry 9 already has stock movements from an earlier transaction — an entry and the stock it moved are recorded together, once");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already been recorded/i);
  });

  it("maps record_adjustment's guards (0018)", () => {
    const noMemo = mapPgError(pgErr(
      "P0001",
      "say why the quantity is changing — an adjustment without a reason is impossible to audit later"));
    expect(noMemo.status).toBe(422);
    expect(noMemo.message).toMatch(/say why/i);

    const ownAccount = mapPgError(pgErr(
      "P0001",
      "'Inventory' is this item's own inventory account — pick the account that explains the change, like Shrinkage."));
    expect(ownAccount.status).toBe(422);
    expect(ownAccount.message).toMatch(/Shrinkage/);

    // the one that mattered: ANOTHER item's inventory account
    const otherAccount = mapPgError(pgErr(
      "P0001",
      "'Inventory - Finished Goods' is another item's inventory account — pick the account that explains the change, like Shrinkage."));
    expect(otherAccount.status).toBe(422);
    expect(otherAccount.message).toMatch(/Inventory - Finished Goods/);

    const wrongType = mapPgError(pgErr(
      "P0001",
      "an inventory adjustment posts to an expense, income or equity account — 'Cash' is an asset account"));
    expect(wrongType.status).toBe(422);
    expect(wrongType.message).toMatch(/expense, income or equity/);
  });

  it("tells an already-reversed ENTRY apart from an already-reversed STOCK MOVEMENT", () => {
    // Regression: reverse_entry_internal's "entry % has already been reversed"
    // was matched unanchored, several hundred lines before the inventory
    // mapper ran, so 0021's "stock movement % has already been reversed"
    // answered with the word "entry". Right status, wrong vocabulary — which is
    // exactly the kind of thing that survives review unless a test pins it.
    const entry = mapPgError(pgErr("P0001", "entry 12 has already been reversed"));
    expect(entry.status).toBe(409);
    expect(entry.message).toBe("This entry has already been reversed.");

    const movement = mapPgError(pgErr("P0001", "stock movement 44 has already been reversed"));
    expect(movement.status).toBe(409);
    expect(movement.message).toBe("That stock movement has already been reversed.");
  });

  it("maps the entry-side half of the tie (0022) and names the account", () => {
    const err = pgErr(
      "P0001",
      "You can't move money into or out of 'Inventory' directly — it is an inventory account. Receive stock on a bill, or record an inventory adjustment, so the quantity and the value move together.");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/'Inventory' directly/);
    expect(mapped.message).toMatch(/receive stock on a bill/i);
  });

  it("passes negative-stock refusals through verbatim — they are already English", () => {
    const err = pgErr("P0001", "You only have 12 of Widget on hand — this would use 20.");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("You only have 12 of Widget on hand — this would use 20.");
  });

  it("maps 'reversal date cannot precede the entry it reverses'", () => {
    const err = pgErr("P0001", "reversal date cannot precede the entry it reverses");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/reversal date/i);
  });

  it("maps a duplicate import file (23505 on the file_sha256 constraint)", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "import_batches_company_id_file_sha256_key"',
      { constraint: "import_batches_company_id_file_sha256_key" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already been imported/i);
  });

  it("maps a duplicate external_ref (23505 on the source/external_ref constraint)", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "journal_entries_company_id_source_external_ref_key"',
      { constraint: "journal_entries_company_id_source_external_ref_key" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already been imported/i);
  });

  it("maps any other unique_violation to a generic duplicate message", () => {
    const err = pgErr(
      "23505", 'duplicate key value violates unique constraint "users_email_key"',
      { constraint: "users_email_key" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
  });

  it("maps a CHECK violation (e.g. journal_lines' one-sided debit/credit rule)", () => {
    const err = pgErr(
      "23514",
      'new row for relation "journal_lines" violates check constraint "journal_lines_check"',
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
  });

  it("maps a foreign key violation (e.g. cross-company account reference)", () => {
    const err = pgErr(
      "23503",
      'insert or update on table "journal_lines" violates foreign key constraint ' +
        '"journal_lines_company_id_account_id_fkey"',
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
  });

  it("maps a numeric field overflow (22003) to a friendly too-large message", () => {
    const err = pgErr(
      "22003",
      'numeric field overflow\nDETAIL:  A field with precision 19, scale 4 must round to an ' +
        'absolute value less than 10^15.',
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("That number is too large to store.");
  });

  it("maps an RLS denial to a generic not-allowed message", () => {
    const err = pgErr(
      "42501", 'new row violates row-level security policy for table "accounts"');
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(403);
  });

  it("maps a generic permission-denied error to the SAME not-allowed message", () => {
    const rls = mapPgError(pgErr(
      "42501", 'new row violates row-level security policy for table "accounts"'));
    const perm = mapPgError(pgErr(
      "42501", 'permission denied for table schema_migrations'));
    expect(perm.status).toBe(403);
    expect(perm.message).toBe(rls.message);
  });

  it("falls through an unrecognized error to a generic 500", () => {
    const err = pgErr("XX000", "internal error: cache lookup failed");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(500);
  });

  it("never throws given a non-Error input", () => {
    expect(() => mapPgError(undefined)).not.toThrow();
    expect(() => mapPgError(null)).not.toThrow();
    expect(() => mapPgError("plain string")).not.toThrow();
    expect(mapPgError(null).status).toBe(500);
  });
});

// ---------------------------------------------------------------------------
// Accounts Receivable (Task 2 of docs/superpowers/plans/2026-07-31-ar.md) —
// messages lifted verbatim from db/migrations/0005_ar.sql and 0006_ar_fixes.sql.
// ---------------------------------------------------------------------------

describe("mapPgError — Accounts Receivable", () => {
  it("maps a duplicate customer name (23505 on the company_id/name constraint)", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "customers_company_id_name_key"',
      { constraint: "customers_company_id_name_key" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already exists/i);
  });

  it("passes ensure_ar_account()'s missing-account message through verbatim", () => {
    const err = pgErr(
      "P0001",
      "No account named 'Accounts Receivable' exists — create one or set it in Settings",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe(
      "No account named 'Accounts Receivable' exists — create one or set it in Settings",
    );
  });

  it("passes ensure_ar_account()'s ambiguous-account message through verbatim", () => {
    const err = pgErr(
      "P0001",
      "More than one active account is named 'Accounts Receivable' — set the one to use in Settings",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/more than one active account/i);
  });

  it("maps void_invoice()'s 'void the payments applied to it first' to the exact invoice message", () => {
    // Exact .toBe, not just a loose /void those payments first/i match:
    // regression check for a masking bug where this regex (formerly
    // unanchored to "invoice \d+") shadowed void_bill()'s identically-shaped
    // AP RAISE, so a BILL with applied payments incorrectly got this
    // invoice-worded message. See the matching exact-match bill test below.
    const err = pgErr(
      "P0001",
      "invoice 5 has payments applied — void the payments applied to it first",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe(
      "This invoice has payments applied to it — void those payments first, then void this invoice.",
    );
  });

  it("maps void_invoice()'s refusal to void a draft", () => {
    const err = pgErr("P0001", "invoice 5 is draft — only open invoices can be voided");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/issue this one first/i);
  });

  it("maps void_invoice()'s refusal to double-void", () => {
    const err = pgErr("P0001", "invoice 5 is void — only open invoices can be voided");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This invoice has already been voided.");
  });

  it("maps issue_invoice()'s double-issue refusal", () => {
    const err = pgErr("P0001", "invoice 5 is already open");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This invoice has already been issued.");
  });

  it("maps issue_invoice()'s refusal to (re)issue a voided invoice", () => {
    const err = pgErr("P0001", "invoice 5 is already void");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This invoice has already been voided.");
  });

  it("maps issue_invoice()'s no-lines refusal", () => {
    const err = pgErr("P0001", "invoice 5 has no lines");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/at least one line/i);
  });

  it("maps invoices_immutable_when_issued()'s edit refusal", () => {
    const err = pgErr("P0001", "issued invoice 5 is immutable; void it instead of editing");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void it instead/i);
  });

  it("maps invoice_lines_immutable_when_issued()'s refusal", () => {
    const err = pgErr(
      "P0001", "lines of an issued invoice are immutable; void the invoice instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void it instead/i);
  });

  it("maps payments_immutable()'s refusal", () => {
    const err = pgErr("P0001", "payments are immutable; void payment 9 instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void it instead/i);
  });

  it("maps void_payment()'s refusal to double-void", () => {
    const err = pgErr("P0001", "payment 9 is already void");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This payment has already been voided.");
  });

  it("maps apply_credit()'s refusal to apply credit from a voided payment", () => {
    const err = pgErr("P0001", "payment 9 is void");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("That payment has been voided and can't be used.");
  });

  it("maps payment_applications_immutable()'s refusal", () => {
    const err = pgErr("P0001", "payment applications are immutable; void the payment instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void the payment instead/i);
  });

  it("maps check_payment_application()'s payment over-application", () => {
    const err = pgErr("P0001", "payment 3 is over-applied: 120.0000 applied against a 100.0000 payment");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/more than this payment covers/i);
  });

  it("maps check_payment_application()'s invoice over-application", () => {
    const err = pgErr("P0001", "invoice 3 is over-applied: 120.0000 applied against a 100.0000 invoice");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/more than this invoice's balance/i);
  });

  it("maps check_payment_application()'s cross-customer refusal", () => {
    const err = pgErr("P0001", "payment 3 and invoice 5 belong to different customers");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/different customers/i);
  });

  it("maps receive_payment()/apply_credit()'s not-open refusal", () => {
    const err = pgErr("P0001", "invoice 5 is draft — only open invoices accept payment");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/issued, unpaid invoice/i);
  });

  it("maps receive_payment()/apply_credit()'s cross-customer invoice refusal", () => {
    const err = pgErr("P0001", "invoice 5 does not belong to this customer");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/doesn't belong to this customer/i);
  });

  it("maps a not-found customer/invoice/payment", () => {
    expect(mapPgError(pgErr("P0001", "customer 5 not found")).status).toBe(404);
    expect(mapPgError(pgErr("P0001", "invoice 5 not found")).status).toBe(404);
    expect(mapPgError(pgErr("P0001", "payment 5 not found")).status).toBe(404);
  });

  it("maps invoice_lines_income_account_only()'s non-income-account refusal", () => {
    const err = pgErr(
      "P0001", "invoice lines must post to an income account (account 7 is asset)");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/income category/i);
  });
});

// ---------------------------------------------------------------------------
// Accounts Payable (Task 2 of docs/superpowers/plans/2026-08-01-ap.md) —
// messages lifted verbatim from db/migrations/0009_ap.sql. Deliberately a
// mechanical vendor/bill mirror of the Accounts Receivable block above.
// ---------------------------------------------------------------------------

describe("mapPgError — Accounts Payable", () => {
  it("maps a duplicate vendor name (23505 on the company_id/name constraint)", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "vendors_company_id_name_key"',
      { constraint: "vendors_company_id_name_key" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/already exists/i);
  });

  it("maps a duplicate (vendor, ref_no) with the specific reference in English", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "bills_vendor_ref_no_unique"',
      {
        constraint: "bills_vendor_ref_no_unique",
        detail: "Key (company_id, vendor_id, ref_no)=(11111111-1111-1111-1111-111111111111, 3, INV-100) already exists.",
      },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("You've already entered a bill with reference INV-100 for this vendor.");
  });

  it("falls back to a generic (vendor, ref_no) message when detail is unavailable", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "bills_vendor_ref_no_unique"',
      { constraint: "bills_vendor_ref_no_unique" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("You've already entered a bill with that reference for this vendor.");
  });

  it("passes ensure_ap_account()'s missing-account message through verbatim", () => {
    const err = pgErr(
      "P0001",
      "No account named 'Accounts Payable' exists — create one or set it in Settings",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe(
      "No account named 'Accounts Payable' exists — create one or set it in Settings",
    );
  });

  it("passes ensure_ap_account()'s ambiguous-account message through verbatim", () => {
    const err = pgErr(
      "P0001",
      "More than one active account is named 'Accounts Payable' — set the one to use in Settings",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/more than one active account/i);
  });

  it("maps bill_lines_category_not_ap()'s refusal to post to the A/P account itself", () => {
    const err = pgErr(
      "P0001",
      "bill lines cannot post to the Accounts Payable account itself (account 7)",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/Accounts Payable account itself/i);
  });

  it("maps void_bill()'s 'void the payments applied to it first' to the exact bill message, not the AR invoice one", () => {
    // Regression check (the shipped defect this fix addresses): before the
    // AR/AP regexes were anchored to "invoice \d+"/"bill \d+" respectively,
    // mapArRaiseException ran first and its unanchored regex matched this
    // identically-shaped AP RAISE, so a bill with applied payments got "This
    // invoice has payments applied..." instead of "This bill...". Exact
    // .toBe (not a loose match) so this can never silently regress either
    // direction.
    const err = pgErr(
      "P0001",
      "bill 5 has payments applied — void the payments applied to it first",
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe(
      "This bill has payments applied to it — void those payments first, then void this bill.",
    );
  });

  it("maps void_bill()'s refusal to void a draft", () => {
    const err = pgErr("P0001", "bill 5 is draft — only open bills can be voided");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/issue this one first/i);
  });

  it("maps void_bill()'s refusal to double-void", () => {
    const err = pgErr("P0001", "bill 5 is void — only open bills can be voided");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This bill has already been voided.");
  });

  it("maps issue_bill()'s double-issue refusal", () => {
    const err = pgErr("P0001", "bill 5 is already open");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This bill has already been issued.");
  });

  it("maps issue_bill()'s refusal to (re)issue a voided bill", () => {
    const err = pgErr("P0001", "bill 5 is already void");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("This bill has already been voided.");
  });

  it("maps issue_bill()'s no-lines refusal", () => {
    const err = pgErr("P0001", "bill 5 has no lines");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/at least one line/i);
  });

  it("maps bills_immutable_when_issued()'s edit refusal", () => {
    const err = pgErr("P0001", "issued bill 5 is immutable; void it instead of editing");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void it instead/i);
  });

  it("maps bill_lines_immutable_when_issued()'s refusal", () => {
    const err = pgErr(
      "P0001", "lines of an issued bill are immutable; void the bill instead");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/void it instead/i);
  });

  it("maps a not-found vendor/bill", () => {
    expect(mapPgError(pgErr("P0001", "vendor 5 not found")).status).toBe(404);
    expect(mapPgError(pgErr("P0001", "bill 5 not found")).status).toBe(404);
  });
});

// ---------------------------------------------------------------------------
// Accounts Payable — payments + check printing (Task 3 of
// docs/superpowers/plans/2026-08-01-ap.md) — messages lifted verbatim from
// db/migrations/0009_ap.sql's pay_bills(), check_bill_payment_application(),
// set_next_check_no(), and assign_check_numbers(). Mirrors the AR payments
// block's pattern (mapPgError — Accounts Receivable, above).
// ---------------------------------------------------------------------------

describe("mapPgError — Accounts Payable payments + check printing", () => {
  it("maps pay_bills()'s not-open refusal", () => {
    const err = pgErr("P0001", "bill 5 is draft — only open bills accept payment");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("Only an issued, unpaid bill can accept a payment.");
  });

  it("maps pay_bills()'s cross-vendor bill refusal", () => {
    const err = pgErr("P0001", "bill 5 does not belong to this vendor");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("That bill doesn't belong to this vendor.");
  });

  it("maps check_bill_payment_application()'s bill over-application", () => {
    const err = pgErr("P0001", "bill 3 is over-applied: 120.0000 applied against a 100.0000 bill");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("That's more than this bill's balance — reduce the amount applied.");
  });

  it("maps check_bill_payment_application()'s cross-vendor refusal", () => {
    const err = pgErr("P0001", "payment 3 and bill 5 belong to different vendors");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe("That payment and that bill belong to different vendors.");
  });

  it("maps set_next_check_no()'s fast-forward-only refusal, surfacing the highest used", () => {
    const err = pgErr("P0001", "next check number must be greater than the highest used (500)");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(422);
    expect(mapped.message).toBe(
      "The next check number must be greater than the highest one already used on this account (500).",
    );
  });

  it("maps assign_check_numbers()'s no-payments-selected refusal", () => {
    const err = pgErr("P0001", "no payments selected for check printing");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(400);
    expect(mapped.message).toBe("Pick at least one payment to print a check for.");
  });

  it("maps assign_check_numbers()'s refusal for a payment that's no longer queued to print", () => {
    const err = pgErr(
      "P0001", "payment 7 is not a check payment queued to print on this account");
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe(
      "That payment isn't waiting to be printed anymore — it may already have been printed or voided.",
    );
  });
});

// ---------------------------------------------------------------------------
// Duplicate check number (23505 on bill_payments_check_no_unique) — the
// generic mapPgError fallback. server/src/billPayments.ts's own
// mapDuplicateCheckNo() is what actually names the number (using the value
// the caller submitted); this is the defense-in-depth fallback for any other
// caller of mapPgError that hits this constraint without going through that
// helper (mirrors the identical bills_vendor_ref_no_unique fallback test
// above).
// ---------------------------------------------------------------------------

describe("mapPgError — duplicate check number (bill_payments_check_no_unique)", () => {
  it("maps a duplicate check number to a generic (no number) English fallback", () => {
    const err = pgErr(
      "23505",
      'duplicate key value violates unique constraint "bill_payments_check_no_unique"',
      { constraint: "bill_payments_check_no_unique" },
    );
    const mapped = mapPgError(err);
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe(
      "That check number has already been used on this account — pick another number or update the next check number in Settings.",
    );
  });
});

// ---------------------------------------------------------------------------
// billPayments.ts's own mapDuplicateCheckNo() — the pre-mapPgError helper
// that NAMES the number (mirrors bills.ts's mapDuplicateRefNo, which has no
// equivalent direct unit test of its own — this one exists because the
// number-vs-no-number branching is easy to get backwards and worth locking
// down directly, independent of any HTTP route).
// ---------------------------------------------------------------------------

describe("mapDuplicateCheckNo", () => {
  const constraintErr = pgErr(
    "23505",
    'duplicate key value violates unique constraint "bill_payments_check_no_unique"',
    { constraint: "bill_payments_check_no_unique" },
  );

  it("names the number when the caller supplied one (the manual-entry POST path)", () => {
    const mapped = mapDuplicateCheckNo(constraintErr, "1024");
    expect(mapped).toEqual({
      status: 409,
      error: "Check #1024 has already been used on this account — pick another number or update the next check number in Settings.",
    });
  });

  it("falls back to a generic (no-number) message when none is known (the print-run race path)", () => {
    const mapped = mapDuplicateCheckNo(constraintErr, null);
    expect(mapped).toEqual({
      status: 409,
      error: "That check number has already been used on this account — pick another number or update the next check number in Settings.",
    });
  });

  it("returns null for an unrelated error (defers to mapPgError)", () => {
    expect(mapDuplicateCheckNo(pgErr("23505", "x", { constraint: "vendors_company_id_name_key" }), "1024"))
      .toBeNull();
    expect(mapDuplicateCheckNo(pgErr("P0001", "bill 5 not found"), "1024")).toBeNull();
    expect(mapDuplicateCheckNo(undefined, "1024")).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// Sales tax — db/migrations/0015_sales_tax.sql. Message strings lifted
// verbatim from that migration's RAISE EXCEPTIONs so these tests stay honest
// about what the database actually raises.
// ---------------------------------------------------------------------------

describe("mapPgError — sales tax (0015)", () => {
  it("passes through the ambiguous 'Sales Tax Payable' enable refusal (already English)", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "More than one active account is named 'Sales Tax Payable' — set the one to use in Settings",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/More than one active account is named 'Sales Tax Payable'/);
  });

  it("maps the exhausted 2200-2299 code range to English", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "no free account code between 2200 and 2299 — create a 'Sales Tax Payable' liability account yourself",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/create a liability account named 'Sales Tax Payable'/i);
    expect(mapped.message).not.toMatch(/debit|credit/i);
  });

  it("maps the taxed-draft disable refusal and names the draft", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "draft invoice 42 still has sales tax on it — remove its tax rate first",
    ));
    expect(mapped.status).toBe(409);
    expect(mapped.message).toContain("Draft invoice 42 still has sales tax on it");
  });

  it("maps the frozen-percent trigger to English", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "tax rate 'CA Sales Tax' has been used on issued invoices — deactivate it and create a new rate instead of changing the percent",
    ));
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/deactivate it and create a new rate/i);
  });

  it("maps the enabled gate on issue", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "sales tax is turned off for this company — turn it on in Settings before issuing a taxed invoice",
    ));
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe(
      "Sales tax is turned off for this company — turn it on in Settings before issuing a taxed invoice.",
    );
  });

  it("maps the tax-amount-without-rate refusal (both the RAISE and the table CHECK)", () => {
    const fromRaise = mapPgError(pgErr("P0001", "invoice 7 has a tax amount but no tax rate"));
    expect(fromRaise.status).toBe(422);
    expect(fromRaise.message).toMatch(/can't carry sales tax without a tax rate/i);

    const fromCheck = mapPgError(pgErr(
      "23514",
      'new row for relation "invoices" violates check constraint "invoices_tax_amount_requires_rate"',
      { constraint: "invoices_tax_amount_requires_rate" },
    ));
    expect(fromCheck.status).toBe(422);
    expect(fromCheck.message).toMatch(/can't carry sales tax without a tax rate/i);
  });

  it("maps an inactive rate at issue", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "tax rate 'Old Rate' is inactive — reactivate it or pick another rate",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/inactive/i);
  });

  it("maps a missing tax rate to a 404", () => {
    const mapped = mapPgError(pgErr("P0001", "tax rate 99 not found"));
    expect(mapped.status).toBe(404);
    expect(mapped.message).toBe("We couldn't find that tax rate.");
  });

  it("maps the recompute-mismatch guard to a logged 500 (a server bug, never a user mistake)", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "invoice 7 tax amount 7.30 does not match the 7.2500 rate on its taxable lines (should be 7.25)",
    ));
    expect(mapped.status).toBe(500);
    expect(mapped.message).toMatch(/didn't add up/i);
    expect(mapped.message).not.toMatch(/debit|credit/i);
  });

  it("maps a taxed issue with no payable account configured", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "no Sales Tax Payable account is configured — turn sales tax on in Settings first",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/turn sales tax on in Settings/i);
  });

  it("maps the liability-only settings trigger", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "sales tax account must be a liability account (account 5 is income)",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/liability account/i);
    expect(mapped.message).not.toMatch(/debit|credit/i);
  });

  it("falls back to a generic named-value-free 409 for a duplicate rate name (routes name the value themselves)", () => {
    const mapped = mapPgError(pgErr(
      "23505",
      'duplicate key value violates unique constraint "tax_rates_company_id_name_key"',
      { constraint: "tax_rates_company_id_name_key" },
    ));
    expect(mapped.status).toBe(409);
    expect(mapped.message).toBe("You already have a tax rate with that name.");
  });
});

describe("mapPgError — sales tax regex anchoring (adversarial rate names)", () => {
  it("a rate NAMED like the inactive message still maps the frozen-percent raise to the frozen 409", () => {
    const evilName = "Evil' is inactive — reactivate it or pick another rate";
    const mapped = mapPgError(pgErr(
      "P0001",
      `tax rate '${evilName}' has been used on issued invoices — deactivate it and create a new rate instead of changing the percent`,
    ));
    expect(mapped.status).toBe(409);
    expect(mapped.message).toMatch(/deactivate it and create a new rate/i);
  });

  it("a rate NAMED like the frozen message still maps the inactive raise to the inactive 422", () => {
    const evilName =
      "Evil' has been used on issued invoices — deactivate it and create a new rate instead of changing the percent";
    const mapped = mapPgError(pgErr(
      "P0001",
      `tax rate '${evilName}' is inactive — reactivate it or pick another rate`,
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toMatch(/inactive/i);
  });
});

// ---------------------------------------------------------------------------
// Inventory (0018/0023/0025). Every branch below is a RAISE that reaches a user
// through a screen, and the failure mode being guarded against is the one Task
// 5's review found: a refusal with perfectly good English in the database
// falling through to "Something went wrong on our end", because the only
// pattern that looked like it was a DIFFERENT function's twin of the message.
// ---------------------------------------------------------------------------

describe("mapPgError — inventory adjustments and builds (0018/0023)", () => {
  it("maps record_adjustment's zero-value refusal, verbatim", () => {
    // 0018:901. Until Task 5's review this matched NOTHING: the nearest branch
    // was post_build()'s twin, anchored `^the parts for '.+' have no value on
    // hand`, which this wording cannot satisfy. It became a bare 500.
    const mapped = mapPgError(pgErr(
      "P0001",
      "'Zero Cost Widget' has no value on hand, so this adjustment would move no money at all — there is nothing to post",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toContain("has no value on hand");
    expect(mapped.message).toContain("Zero Cost Widget");
    expect(mapped.message).not.toMatch(/went wrong on our end/i);
  });

  it("still maps post_build's own twin of that message", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "the parts for 'Empty Assembly' have no value on hand, so this build would move no money at all",
    ));
    expect(mapped.status).toBe(422);
    expect(mapped.message).toContain("the parts for 'Empty Assembly'");
  });

  it("maps the imported-history double-count refusal (0025) to a 409, verbatim", () => {
    const mapped = mapPgError(pgErr(
      "P0001",
      "This company brought in transaction history, which already recorded what you paid for stock. Taking opening quantities out of 'Opening Balance Equity' as well would count the same inventory twice — use the account your imported history put the value in instead, so the value moves across rather than being created a second time.",
    ));
    // 409, not 422: nothing is malformed — it conflicts with the books' state,
    // and the caller may legitimately answer it.
    expect(mapped.status).toBe(409);
    expect(mapped.message).toContain("count the same inventory twice");
    expect(mapped.message).toContain("use the account your imported history put the value in");
  });
});
