// Integration tests for the sales tax API (Task 2 of
// docs/superpowers/plans/2026-08-02-sales-tax.md): the per-company
// enable/disable toggle (incl. the taxed-draft disable refusal), tax-rate
// CRUD (validation, duplicate names, the percent freeze), the taxed invoice
// lifecycle end-to-end (server-computed breakdown, HALF-UP boundary
// agreement with the DB's own recompute guard, payment to zero with the
// payable untouched), exempt-customer refusal at draft AND issue time, the
// remittance story (bill to the agency clears the payable), the carry-over
// tax-inclusive-total fixes in list/picker payloads, the sales-tax report,
// and RLS isolation. Same createTestDb/buildApp/app.inject patterns as
// tests/server/ar-invoices.test.ts.

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createTestDb, type TestDb } from "../helpers/db.js";
// Where the database is, resolved once for the whole run: your ADMIN_URL /
// DATABASE_URL if you set one, otherwise the PostgreSQL Balanced Sum installed on
// this machine. Every file in this directory used to carry its own copy of that
// decision, hardcoded to the Docker container that no longer exists.
import { dbUrl } from "../helpers/test-db-admin.js";
import { buildApp } from "../../server/src/app.js";
import type { FastifyInstance } from "fastify";

async function buildTestApp(dbName: string): Promise<FastifyInstance> {
  return buildApp({
    databaseUrl: await dbUrl(dbName, "app", "app"),
    adminDatabaseUrl: await dbUrl(dbName),
  });
}

function post(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "POST", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function patch(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "PATCH", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function put(app: FastifyInstance, companyId: string, url: string, payload?: unknown) {
  return app.inject({
    method: "PUT", url, headers: { "x-company-id": companyId }, payload: payload ?? {},
  });
}
function get(app: FastifyInstance, companyId: string, url: string) {
  return app.inject({ method: "GET", url, headers: { "x-company-id": companyId } });
}

let db: TestDb;
let app: FastifyInstance;

beforeAll(async () => {
  db = await createTestDb();
  app = await buildTestApp(db.dbName);
}, 60_000);

afterAll(async () => {
  await app.close();
  await db.close();
});

// tests/helpers/db.ts's seeded chart (both companies): 1000 Cash (asset),
// 1100 Accounts Receivable (asset), 1200 Due from Affiliate (asset), 2000
// Accounts Payable (liability), 2200 Due to Affiliate (liability), 3000
// Opening Equity (equity), 4000 Revenue (income), 6000 Operating Expense
// (expense). Note 2200 is TAKEN — enable_sales_tax()'s code walk must land
// the auto-created 'Sales Tax Payable' on 2201.

async function createCustomer(name: string, extra: Record<string, unknown> = {}): Promise<string> {
  const res = await post(app, db.companyA, "/api/c/customers", { name, ...extra });
  expect(res.statusCode).toBe(201);
  return res.json().id;
}

async function createRate(
  name: string, ratePercent: string, extra: Record<string, unknown> = {},
): Promise<string> {
  const res = await post(app, db.companyA, "/api/c/tax-rates", { name, ratePercent, ...extra });
  expect(res.statusCode).toBe(201);
  return res.json().id;
}

interface LineInput {
  description?: string;
  quantity?: string;
  unitPrice: string;
  incomeAccountId?: string;
  taxable?: boolean;
}
function draftBody(customerId: string, lines: LineInput[], extra: Record<string, unknown> = {}) {
  return {
    customerId,
    invoiceDate: "2024-06-01",
    lines: lines.map((l) => ({ incomeAccountId: String(db.accounts["4000"]!.A), ...l })),
    ...extra,
  };
}

/** Trial-balance row lookup by account id, through the API (so it's the same
 *  derived-at-query-time number the user sees). */
async function trialBalanceOf(companyId: string, accountId: string): Promise<string> {
  const res = await get(app, companyId, "/api/c/reports/trial-balance");
  expect(res.statusCode).toBe(200);
  const row = res.json().rows.find((r: { accountId: string }) => r.accountId === accountId);
  return row ? row.balance : "0.0000";
}

// ---------------------------------------------------------------------------
// settings — enable / disable
// ---------------------------------------------------------------------------

describe("GET/PUT /api/c/settings/sales-tax", () => {
  it("reports disabled with no account for a company that never touched sales tax", async () => {
    const res = await get(app, db.companyA, "/api/c/settings/sales-tax");
    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual({ enabled: false, accountId: null, accountName: null });
  });

  it("enable auto-creates 'Sales Tax Payable', walking past the taken 2200 code to 2201", async () => {
    const res = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: true });
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.enabled).toBe(true);
    expect(body.accountName).toBe("Sales Tax Payable");
    expect(body.accountId).not.toBeNull();

    // 2200 is 'Due to Affiliate' in the seeded chart — the walk must have
    // landed on 2201, type liability.
    const acct = await db.admin.query(
      "SELECT code, type FROM accounts WHERE company_id = $1 AND id = $2",
      [db.companyA, body.accountId],
    );
    expect(acct.rows[0]).toEqual({ code: "2201", type: "liability" });
  });

  it("enabling again is idempotent (same account, still enabled)", async () => {
    const before = (await get(app, db.companyA, "/api/c/settings/sales-tax")).json();
    const res = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: true });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual(before);
  });

  it("400s in English for a non-boolean enabled", async () => {
    const res = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: "yes" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/on or off/i);
  });

  it("refuses to disable, in English naming the draft, while a draft still carries a tax rate — then disables cleanly once it's removed", async () => {
    const customerId = await createCustomer("Disable Refusal Co");
    const rateId = await createRate("Disable Refusal Rate", "5");
    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "10.00", taxable: true },
    ], { taxRateId: rateId }));
    expect(draft.statusCode).toBe(201);
    const invoiceId = draft.json().invoiceId;

    const refused = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: false });
    expect(refused.statusCode).toBe(409);
    expect(refused.json().error).toContain(`Draft invoice ${invoiceId} still has sales tax on it`);
    expect(refused.json().error).not.toMatch(/debit|credit/i);

    // Remove the rate from the draft; disable now succeeds; history intact.
    const cleared = await patch(app, db.companyA, `/api/c/invoices/${invoiceId}`, draftBody(customerId, [
      { unitPrice: "10.00", taxable: true },
    ], { taxRateId: null }));
    expect(cleared.statusCode).toBe(200);
    expect(cleared.json().taxAmount).toBe("0.0000");

    const disabled = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: false });
    expect(disabled.statusCode).toBe(200);
    expect(disabled.json().enabled).toBe(false);
    // The account is remembered even while disabled.
    expect(disabled.json().accountName).toBe("Sales Tax Payable");

    // Re-enable for the rest of this file.
    const reEnabled = await put(app, db.companyA, "/api/c/settings/sales-tax", { enabled: true });
    expect(reEnabled.statusCode).toBe(200);
    expect(reEnabled.json().enabled).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// tax rates — CRUD
// ---------------------------------------------------------------------------

describe("/api/c/tax-rates", () => {
  it("creates a rate with a 4-decimal percent, echoed as the numeric(7,4) string", async () => {
    const res = await post(app, db.companyA, "/api/c/tax-rates", {
      name: "NY Combined", ratePercent: "8.8750",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({
      name: "NY Combined", ratePercent: "8.8750", agencyVendorId: null,
      agencyVendorName: null, active: true,
    });
  });

  it("409s in English, naming the rate, on a duplicate name", async () => {
    const res = await post(app, db.companyA, "/api/c/tax-rates", {
      name: "NY Combined", ratePercent: "8.0000",
    });
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toBe("You already have a tax rate called NY Combined.");
  });

  it("400s for a rate over 100", async () => {
    const res = await post(app, db.companyA, "/api/c/tax-rates", { name: "Too Big", ratePercent: "101" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/between 0 and 100/i);
  });

  it("accepts exactly 100 but not 100.0001", async () => {
    const ok = await post(app, db.companyA, "/api/c/tax-rates", { name: "Exactly 100", ratePercent: "100" });
    expect(ok.statusCode).toBe(201);
    const bad = await post(app, db.companyA, "/api/c/tax-rates", { name: "Over 100", ratePercent: "100.0001" });
    expect(bad.statusCode).toBe(400);
  });

  it("400s for a negative rate", async () => {
    const res = await post(app, db.companyA, "/api/c/tax-rates", { name: "Negative", ratePercent: "-5" });
    expect(res.statusCode).toBe(400);
  });

  it("400s for a 5-decimal-place rate", async () => {
    const res = await post(app, db.companyA, "/api/c/tax-rates", { name: "Five DP", ratePercent: "1.23456" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/4 decimal places/i);
  });

  it("400s in English (never a raw 500) for non-numeric rate shapes — the bounds refine must be throw-proof", async () => {
    // Regression: zod runs .refine even after .regex has failed, so these
    // inputs reach the BigInt bounds check — an unguarded BigInt("1e2")
    // throws and used to surface as a raw 500 leaking the conversion error.
    for (const ratePercent of ["1e2", "abc", "7a"]) {
      const res = await post(app, db.companyA, "/api/c/tax-rates", {
        name: `Bad Shape ${ratePercent}`, ratePercent,
      });
      expect(res.statusCode).toBe(400);
      const body = res.json();
      expect(Object.keys(body)).toEqual(["error"]);
      expect(body.error).toMatch(/Enter a rate like 7\.25/i);
      expect(body.error).not.toMatch(/BigInt|SyntaxError|convert/i);
    }
  });

  it("400s the same way on PATCH for a non-numeric rate shape", async () => {
    const id = await createRate("Patch Bad Shape", "5");
    const res = await patch(app, db.companyA, `/api/c/tax-rates/${id}`, { ratePercent: "1e2" });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/Enter a rate like 7\.25/i);
    expect(res.json().error).not.toMatch(/BigInt/i);
  });

  it("joins the agency vendor's name, and 422s in English for a bogus agency vendor", async () => {
    const vendorRes = await post(app, db.companyA, "/api/c/vendors", { name: "State Tax Board" });
    expect(vendorRes.statusCode).toBe(201);
    const vendorId = vendorRes.json().id;

    const res = await post(app, db.companyA, "/api/c/tax-rates", {
      name: "With Agency", ratePercent: "6.5", agencyVendorId: vendorId,
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({ agencyVendorId: vendorId, agencyVendorName: "State Tax Board" });

    const bad = await post(app, db.companyA, "/api/c/tax-rates", {
      name: "Bogus Agency", ratePercent: "6.5", agencyVendorId: "999999999",
    });
    expect(bad.statusCode).toBe(422);
    expect(bad.json().error).toMatch(/vendors as the tax agency/i);
  });

  it("PATCH edits name/active, clears the agency with null, and keeps it when absent", async () => {
    const vendorRes = await post(app, db.companyA, "/api/c/vendors", { name: "Kept Agency" });
    const vendorId = vendorRes.json().id;
    const id = await createRate("Editable Rate", "4.25", { agencyVendorId: vendorId });

    // absent agencyVendorId -> kept
    const renamed = await patch(app, db.companyA, `/api/c/tax-rates/${id}`, { name: "Editable Rate 2" });
    expect(renamed.statusCode).toBe(200);
    expect(renamed.json()).toMatchObject({
      name: "Editable Rate 2", ratePercent: "4.2500", agencyVendorId: vendorId, active: true,
    });

    // null -> cleared; active toggles off
    const cleared = await patch(app, db.companyA, `/api/c/tax-rates/${id}`, {
      agencyVendorId: null, active: false,
    });
    expect(cleared.statusCode).toBe(200);
    expect(cleared.json()).toMatchObject({ agencyVendorId: null, agencyVendorName: null, active: false });
  });

  it("PATCH can change the percent while only drafts reference the rate", async () => {
    const customerId = await createCustomer("Draft Only Rate Co");
    const id = await createRate("Draft Only Rate", "3");
    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "10.00", taxable: true },
    ], { taxRateId: id }));
    expect(draft.statusCode).toBe(201);

    const res = await patch(app, db.companyA, `/api/c/tax-rates/${id}`, { ratePercent: "3.5" });
    expect(res.statusCode).toBe(200);
    expect(res.json().ratePercent).toBe("3.5000");
  });

  it("404s for a made-up rate id", async () => {
    const res = await patch(app, db.companyA, "/api/c/tax-rates/999999999", { name: "Ghost" });
    expect(res.statusCode).toBe(404);
  });
});

// ---------------------------------------------------------------------------
// taxed invoice lifecycle — end to end
// ---------------------------------------------------------------------------

describe("taxed invoice lifecycle", () => {
  let customerId: string;
  let caRateId: string;
  let invoiceId: string;
  let payableAccountId: string;

  beforeAll(async () => {
    customerId = await createCustomer("Lifecycle Co");
    caRateId = await createRate("CA Sales Tax", "7.25");
    payableAccountId = (await get(app, db.companyA, "/api/c/settings/sales-tax")).json().accountId;
  });

  it("draft with mixed taxable/non-taxable lines returns the exact server-computed breakdown", async () => {
    const res = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { description: "Widgets", unitPrice: "100.00", taxable: true },
      { description: "Shipping", unitPrice: "50.00" },
    ], { taxRateId: caRateId }));
    expect(res.statusCode).toBe(201);
    const body = res.json();
    invoiceId = body.invoiceId;
    expect(body.subtotal).toBe("150.0000");
    expect(body.taxableSubtotal).toBe("100.0000");
    expect(body.taxAmount).toBe("7.2500");
    expect(body.total).toBe("157.2500");
    expect(body.taxRate).toEqual({ id: caRateId, name: "CA Sales Tax", ratePercent: "7.2500" });
    expect(body.lines[0].taxable).toBe(true);
    expect(body.lines[1].taxable).toBe(false);
  });

  it("PATCHing the taxable line to non-taxable drops the tax to zero", async () => {
    const res = await patch(app, db.companyA, `/api/c/invoices/${invoiceId}`, draftBody(customerId, [
      { description: "Widgets", unitPrice: "100.00", taxable: false },
      { description: "Shipping", unitPrice: "50.00" },
    ], { taxRateId: caRateId }));
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.taxableSubtotal).toBe("0.0000");
    expect(body.taxAmount).toBe("0.0000");
    expect(body.total).toBe("150.0000");
    // the rate stays on the invoice — no taxable lines just means no tax
    expect(body.taxRate).toMatchObject({ id: caRateId });
  });

  it("PATCHing back restores the tax; issuing names the tax in the summary", async () => {
    const restore = await patch(app, db.companyA, `/api/c/invoices/${invoiceId}`, draftBody(customerId, [
      { description: "Widgets", unitPrice: "100.00", taxable: true },
      { description: "Shipping", unitPrice: "50.00" },
    ], { taxRateId: caRateId }));
    expect(restore.statusCode).toBe(200);
    expect(restore.json().taxAmount).toBe("7.2500");

    const issue = await post(app, db.companyA, `/api/c/invoices/${invoiceId}/issue`);
    expect(issue.statusCode).toBe(200);
    expect(issue.json().summary).toContain("$157.25 sent to the books — includes $7.25 CA Sales Tax.");
  });

  it("detail shows the tax breakdown, applied 0, open = grand total (carry-over fix)", async () => {
    const res = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.subtotal).toBe("150.0000");
    expect(body.taxableSubtotal).toBe("100.0000");
    expect(body.taxAmount).toBe("7.2500");
    expect(body.total).toBe("157.2500");
    expect(body.applied).toBe("0.0000"); // pre-fix this was "-7.2500"
    expect(body.open).toBe("157.2500");
    expect(body.state).toBe("open");
    expect(body.taxRate).toEqual({ id: caRateId, name: "CA Sales Tax", ratePercent: "7.2500" });
  });

  it("list total is tax-inclusive with applied 0 (carry-over fix)", async () => {
    const res = await get(app, db.companyA, `/api/c/invoices?customerId=${customerId}`);
    expect(res.statusCode).toBe(200);
    const row = res.json().invoices.find((i: { invoiceId: string }) => i.invoiceId === invoiceId);
    expect(row).toMatchObject({ total: "157.2500", applied: "0.0000", open: "157.2500" });
  });

  it("payment picker total is tax-inclusive (carry-over fix)", async () => {
    const res = await get(app, db.companyA, `/api/c/customers/${customerId}/open-invoices`);
    expect(res.statusCode).toBe(200);
    const row = res.json().invoices.find((i: { invoiceId: string }) => i.invoiceId === invoiceId);
    expect(row).toMatchObject({ total: "157.2500", open: "157.2500" });
  });

  it("print payload carries the tax breakdown and the taxable flags", async () => {
    const res = await get(app, db.companyA, `/api/c/invoices/${invoiceId}/print`);
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.subtotal).toBe("150.0000");
    expect(body.taxAmount).toBe("7.2500");
    expect(body.total).toBe("157.2500");
    expect(body.applied).toBe("0.0000");
    expect(body.balanceDue).toBe("157.2500");
    expect(body.taxRate).toEqual({ id: caRateId, name: "CA Sales Tax", ratePercent: "7.2500" });
    expect(body.lines.map((l: { taxable: boolean }) => l.taxable)).toEqual([true, false]);
  });

  it("the posted entry hits the payable: AR carries the grand total, the payable the tax", async () => {
    // Payable credit balance is negative in raw debit-positive storage.
    expect(await trialBalanceOf(db.companyA, payableAccountId)).toBe("-7.2500");
  });

  it("paying in full zeroes AR and the open balance; the payable is untouched by the payment", async () => {
    const payableBefore = await trialBalanceOf(db.companyA, payableAccountId);

    const pay = await post(app, db.companyA, "/api/c/payments", {
      customerId,
      paymentDate: "2024-06-15",
      amount: "157.25",
      depositAccountId: String(db.accounts["1000"]!.A),
      applications: [{ invoiceId, amount: "157.25" }],
    });
    expect(pay.statusCode).toBe(201);

    const detail = await get(app, db.companyA, `/api/c/invoices/${invoiceId}`);
    expect(detail.json().open).toBe("0.0000");
    expect(detail.json().applied).toBe("157.2500");
    expect(detail.json().state).toBe("paid");

    // AR nets to zero for this invoice's amounts; payable balance unchanged.
    expect(await trialBalanceOf(db.companyA, payableAccountId)).toBe(payableBefore);
    expect(await trialBalanceOf(db.companyA, String(db.accounts["1100"]!.A))).toBe("0.0000");
  });
});

// ---------------------------------------------------------------------------
// sales tax report
// ---------------------------------------------------------------------------

describe("GET /api/c/reports/sales-tax", () => {
  it("returns the per-rate rows and the payable balance for an enabled company", async () => {
    const res = await get(app, db.companyA, "/api/c/reports/sales-tax?from=2024-01-01&to=2024-12-31");
    expect(res.statusCode).toBe(200);
    const body = res.json();
    expect(body.enabled).toBe(true);
    expect(body.payableAccountName).toBe("Sales Tax Payable");

    const ca = body.rows.find((r: { rateName: string }) => r.rateName === "CA Sales Tax");
    expect(ca).toMatchObject({
      ratePercent: "7.2500",
      taxableSales: "100.0000",
      nonTaxableSales: "50.0000",
      taxCollected: "7.2500",
    });

    // payableBalance is the presentation-signed (positive-owed) flip of the
    // raw trial-balance value for the payable account, as of `to`.
    const raw = await trialBalanceOf(db.companyA, body.payableAccountId);
    const expected = raw.startsWith("-") ? raw.slice(1) : raw === "0.0000" ? "0.0000" : `-${raw}`;
    expect(body.payableBalance).toBe(expected);
  });

  it("400s when from/to are missing", async () => {
    const res = await get(app, db.companyA, "/api/c/reports/sales-tax");
    expect(res.statusCode).toBe(400);
  });

  it("returns the quiet disabled shape for a company with sales tax off", async () => {
    const res = await get(app, db.companyB, "/api/c/reports/sales-tax?from=2024-01-01&to=2024-12-31");
    expect(res.statusCode).toBe(200);
    expect(res.json()).toEqual({ enabled: false, rows: [] });
  });
});

// ---------------------------------------------------------------------------
// HALF-UP boundary agreement — server tax == DB recompute, proven by issue
// succeeding (issue_invoice() refuses loudly on any mismatch)
// ---------------------------------------------------------------------------

describe("HALF-UP boundary agreement (server == database)", () => {
  const cases: Array<{ name: string; rate: string; unitPrice: string; expectedTax: string }> = [
    // 10.10 * 5% = 0.5050 exactly — the .005 boundary rounds UP
    { name: "Boundary 5", rate: "5", unitPrice: "10.10", expectedTax: "0.5100" },
    // 10.20 * 2.5% = 0.2550 exactly — boundary again
    { name: "Boundary 2.5", rate: "2.5", unitPrice: "10.20", expectedTax: "0.2600" },
    // 4.30 * 5% = 0.2150 exactly — boundary
    { name: "Boundary 5b", rate: "5", unitPrice: "4.30", expectedTax: "0.2200" },
    // 99.99 * 7.25% = 7.249275 — rounds up to 7.25
    { name: "CA 7.25 case", rate: "7.25", unitPrice: "99.99", expectedTax: "7.2500" },
    // 45.86 * 8.875% = 4.070075 — rounds down to 4.07
    { name: "NY 8.875 case", rate: "8.875", unitPrice: "45.86", expectedTax: "4.0700" },
  ];

  for (const c of cases) {
    it(`${c.rate}% of ${c.unitPrice} -> ${c.expectedTax}, and the DB's recompute agrees at issue`, async () => {
      const customerId = await createCustomer(`Boundary Co ${c.name}`);
      const rateId = await createRate(`Rate ${c.name}`, c.rate);
      const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
        { unitPrice: c.unitPrice, taxable: true },
      ], { taxRateId: rateId }));
      expect(draft.statusCode).toBe(201);
      expect(draft.json().taxAmount).toBe(c.expectedTax);

      // The DB independently recomputes round(taxable * rate / 100, 2) at
      // issue and REFUSES on mismatch — a 200 here IS the agreement proof.
      const issue = await post(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}/issue`);
      expect(issue.statusCode).toBe(200);
    });
  }
});

// ---------------------------------------------------------------------------
// rate percent freeze once issued
// ---------------------------------------------------------------------------

describe("rate percent freeze", () => {
  it("409s in English when changing the percent of a rate used on an issued invoice; name/agency/active stay editable", async () => {
    const customerId = await createCustomer("Freeze Co");
    const rateId = await createRate("Freeze Rate", "6");
    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "20.00", taxable: true },
    ], { taxRateId: rateId }));
    const issue = await post(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}/issue`);
    expect(issue.statusCode).toBe(200);

    const frozen = await patch(app, db.companyA, `/api/c/tax-rates/${rateId}`, { ratePercent: "6.5" });
    expect(frozen.statusCode).toBe(409);
    expect(frozen.json().error).toMatch(/deactivate it and create a new rate/i);
    expect(frozen.json().error).not.toMatch(/debit|credit/i);

    const renamed = await patch(app, db.companyA, `/api/c/tax-rates/${rateId}`, {
      name: "Freeze Rate (old)", active: false,
    });
    expect(renamed.statusCode).toBe(200);
    expect(renamed.json()).toMatchObject({ name: "Freeze Rate (old)", active: false, ratePercent: "6.0000" });
  });

  it("an inactive rate is refused on a new draft, in English", async () => {
    const customerId = await createCustomer("Inactive Rate Co");
    const rateId = await createRate("Soon Inactive", "4");
    await patch(app, db.companyA, `/api/c/tax-rates/${rateId}`, { active: false });

    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "10.00", taxable: true },
    ], { taxRateId: rateId }));
    expect(draft.statusCode).toBe(422);
    expect(draft.json().error).toMatch(/inactive/i);
  });
});

// ---------------------------------------------------------------------------
// exempt customers — refused at draft AND at issue
// ---------------------------------------------------------------------------

describe("tax-exempt customers", () => {
  it("stores taxExempt/exemptionNote on create and returns them on list and PATCH", async () => {
    const res = await post(app, db.companyA, "/api/c/customers", {
      name: "Exempt Church", taxExempt: true, exemptionNote: "Cert #12345",
    });
    expect(res.statusCode).toBe(201);
    expect(res.json()).toMatchObject({ taxExempt: true, exemptionNote: "Cert #12345" });

    const list = await get(app, db.companyA, "/api/c/customers");
    const row = list.json().find((c: { name: string }) => c.name === "Exempt Church");
    expect(row).toMatchObject({ taxExempt: true, exemptionNote: "Cert #12345" });
  });

  it("refuses a tax rate on a draft for an exempt customer, in English", async () => {
    const exemptId = (await post(app, db.companyA, "/api/c/customers", {
      name: "Exempt Draft Co", taxExempt: true,
    })).json().id;
    const rateId = await createRate("Exempt Draft Rate", "5.5");

    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(exemptId, [
      { unitPrice: "10.00", taxable: true },
    ], { taxRateId: rateId }));
    expect(draft.statusCode).toBe(422);
    expect(draft.json().error).toBe(
      "This customer is tax-exempt, so sales tax can't be added to their invoices.",
    );
  });

  it("re-checks exemption at ISSUE time: a customer flipped to exempt after the draft was saved blocks the issue", async () => {
    const customerId = await createCustomer("Late Exempt Co");
    const rateId = await createRate("Late Exempt Rate", "5.25");
    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "40.00", taxable: true },
    ], { taxRateId: rateId }));
    expect(draft.statusCode).toBe(201);

    // Flipping the customer to exempt is ALLOWED even with taxed drafts
    // (documented decision) — issue is the airtight gate.
    const flip = await patch(app, db.companyA, `/api/c/customers/${customerId}`, {
      name: "Late Exempt Co", taxExempt: true,
    });
    expect(flip.statusCode).toBe(200);
    expect(flip.json().taxExempt).toBe(true);

    const issue = await post(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}/issue`);
    expect(issue.statusCode).toBe(422);
    expect(issue.json().error).toBe(
      "This customer is tax-exempt, so sales tax can't be added to their invoices.",
    );

    // Flip back — the same draft issues cleanly again.
    await patch(app, db.companyA, `/api/c/customers/${customerId}`, {
      name: "Late Exempt Co", taxExempt: false,
    });
    const issue2 = await post(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}/issue`);
    expect(issue2.statusCode).toBe(200);
  });
});

// ---------------------------------------------------------------------------
// stale draft self-heal — percent edited while only drafts referenced it
// ---------------------------------------------------------------------------

describe("stale taxed draft", () => {
  it("recomputes against the CURRENT percent at issue (the draft's stored tax was computed at the old percent)", async () => {
    const customerId = await createCustomer("Stale Draft Co");
    const rateId = await createRate("Stale Rate", "5");
    const draft = await post(app, db.companyA, "/api/c/invoices", draftBody(customerId, [
      { unitPrice: "100.00", taxable: true },
    ], { taxRateId: rateId }));
    expect(draft.json().taxAmount).toBe("5.0000");

    // Legal while only drafts reference the rate.
    const edited = await patch(app, db.companyA, `/api/c/tax-rates/${rateId}`, { ratePercent: "6" });
    expect(edited.statusCode).toBe(200);

    const issue = await post(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}/issue`);
    expect(issue.statusCode).toBe(200);
    expect(issue.json().summary).toContain("includes $6.00 Stale Rate.");

    const detail = await get(app, db.companyA, `/api/c/invoices/${draft.json().invoiceId}`);
    expect(detail.json().taxAmount).toBe("6.0000");
    expect(detail.json().total).toBe("106.0000");
  });
});

// ---------------------------------------------------------------------------
// remittance — a bill to the agency clears the payable
// ---------------------------------------------------------------------------

describe("remittance through the bill flow", () => {
  it("entering and paying a bill to the agency, categorized to Sales Tax Payable, returns the payable to zero", async () => {
    const settings = (await get(app, db.companyA, "/api/c/settings/sales-tax")).json();
    const payableAccountId: string = settings.accountId;

    // Everything collected so far in this file is sitting in the payable.
    const rawBefore = await trialBalanceOf(db.companyA, payableAccountId);
    expect(rawBefore.startsWith("-")).toBe(true); // credit balance — money owed
    const owed = rawBefore.slice(1); // e.g. "23.2700"
    const owedTwoDp = owed.slice(0, -2); // whole cents by construction -> "23.27"

    const vendorRes = await post(app, db.companyA, "/api/c/vendors", { name: "CDTFA" });
    const vendorId = vendorRes.json().id;

    const bill = await post(app, db.companyA, "/api/c/bills", {
      vendorId,
      billDate: "2024-07-01",
      lines: [{
        description: "Q2 sales tax remittance",
        unitPrice: owedTwoDp,
        categoryAccountId: payableAccountId,
      }],
    });
    expect(bill.statusCode).toBe(201);
    const billId = bill.json().billId;

    const issue = await post(app, db.companyA, `/api/c/bills/${billId}/issue`);
    expect(issue.statusCode).toBe(200);

    // Issuing the bill already debits the payable (credit A/P) — payable zero.
    expect(await trialBalanceOf(db.companyA, payableAccountId)).toBe("0.0000");

    const pay = await post(app, db.companyA, "/api/c/bill-payments", {
      vendorId,
      paymentDate: "2024-07-05",
      amount: owedTwoDp,
      moneyAccountId: String(db.accounts["1000"]!.A),
      method: "electronic",
      applications: [{ billId, amount: owedTwoDp }],
    });
    expect(pay.statusCode).toBe(201);

    // Paid: payable still zero, A/P back to zero for this bill.
    expect(await trialBalanceOf(db.companyA, payableAccountId)).toBe("0.0000");

    const billDetail = await get(app, db.companyA, `/api/c/bills/${billId}`);
    expect(billDetail.json().open).toBe("0.0000");
  });
});

// ---------------------------------------------------------------------------
// disabled company + RLS
// ---------------------------------------------------------------------------

describe("disabled company and RLS isolation", () => {
  it("refuses a taxRateId on a draft, in English, while sales tax is off (company B)", async () => {
    // Rates themselves can be managed while disabled (settings prep) — only
    // USING one on an invoice requires the toggle.
    const rateRes = await post(app, db.companyB, "/api/c/tax-rates", {
      name: "B Rate", ratePercent: "5",
    });
    expect(rateRes.statusCode).toBe(201);

    const customerRes = await post(app, db.companyB, "/api/c/customers", { name: "B Customer" });
    const draft = await post(app, db.companyB, "/api/c/invoices", {
      customerId: customerRes.json().id,
      invoiceDate: "2024-06-01",
      taxRateId: rateRes.json().id,
      lines: [{ unitPrice: "10.00", incomeAccountId: String(db.accounts["4000"]!.B), taxable: true }],
    });
    expect(draft.statusCode).toBe(422);
    expect(draft.json().error).toBe(
      "Sales tax is turned off for this company — turn it on in Settings before adding tax to an invoice.",
    );
  });

  it("company B cannot see company A's rates (RLS)", async () => {
    const listB = await get(app, db.companyB, "/api/c/tax-rates");
    expect(listB.statusCode).toBe(200);
    const names = listB.json().map((r: { name: string }) => r.name);
    expect(names).toContain("B Rate");
    expect(names).not.toContain("CA Sales Tax");
  });

  it("company B cannot reach company A's rate by id — 404, not 500 (RLS)", async () => {
    const listA = await get(app, db.companyA, "/api/c/tax-rates");
    const caRate = listA.json().find((r: { name: string }) => r.name === "CA Sales Tax");
    const res = await patch(app, db.companyB, `/api/c/tax-rates/${caRate.id}`, { name: "Hijacked" });
    expect(res.statusCode).toBe(404);
  });

  it("company B cannot use company A's rate on an invoice even if enabled (composite FK / RLS)", async () => {
    // Enable B briefly for this check, then disable again (no taxed drafts).
    await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: true });

    const listA = await get(app, db.companyA, "/api/c/tax-rates");
    const caRate = listA.json().find((r: { name: string }) => r.name === "CA Sales Tax");
    const customerRes = await post(app, db.companyB, "/api/c/customers", { name: "B Cross Co" });
    const draft = await post(app, db.companyB, "/api/c/invoices", {
      customerId: customerRes.json().id,
      invoiceDate: "2024-06-01",
      taxRateId: caRate.id,
      lines: [{ unitPrice: "10.00", incomeAccountId: String(db.accounts["4000"]!.B), taxable: true }],
    });
    expect(draft.statusCode).toBe(422);
    expect(draft.json().error).toBe("We couldn't find that tax rate.");

    const off = await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: false });
    expect(off.statusCode).toBe(200);
  });
});

// ---------------------------------------------------------------------------
// PUT settings with an explicit accountId (Task 3 item B: the Settings UI's
// payable-account picker — resolves enable's ambiguous-name refusal and
// changes the account while enabled)
// ---------------------------------------------------------------------------

describe("PUT /api/c/settings/sales-tax with an explicit accountId", () => {
  it("enables using the chosen active liability account instead of the remembered one", async () => {
    // Company B is currently disabled with the auto-created 'Sales Tax
    // Payable' remembered from the RLS test above — an explicit accountId
    // must override it.
    const accountId = String(db.accounts["2200"]!.B);
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId,
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({
      enabled: true, accountId, accountName: "Due to Affiliate",
    });

    const readBack = await get(app, db.companyB, "/api/c/settings/sales-tax");
    expect(readBack.json().accountId).toBe(accountId);
  });

  it("switches the payable account while already enabled", async () => {
    const accounts = (await get(app, db.companyB, "/api/c/accounts")).json() as Array<{
      id: string; name: string; type: string;
    }>;
    const payable = accounts.find((a) => a.name === "Sales Tax Payable");
    expect(payable).toBeDefined();

    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: payable!.id,
    });
    expect(res.statusCode).toBe(200);
    expect(res.json()).toMatchObject({
      enabled: true, accountId: payable!.id, accountName: "Sales Tax Payable",
    });
  });

  it("refuses a non-liability account in English, changing nothing", async () => {
    const before = (await get(app, db.companyB, "/api/c/settings/sales-tax")).json();
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: String(db.accounts["4000"]!.B), // Revenue (income)
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/liability account \(money you owe\)/);
    expect(res.json().error).not.toMatch(/debit|credit/i);
    expect((await get(app, db.companyB, "/api/c/settings/sales-tax")).json()).toEqual(before);
  });

  it("refuses an unknown account id, and another company's account looks exactly unknown (RLS)", async () => {
    const unknown = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: "999999999",
    });
    expect(unknown.statusCode).toBe(422);
    expect(unknown.json().error).toMatch(/couldn't find that account/i);

    const aSettings = (await get(app, db.companyA, "/api/c/settings/sales-tax")).json();
    const cross = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: aSettings.accountId,
    });
    expect(cross.statusCode).toBe(422);
    expect(cross.json().error).toMatch(/couldn't find that account/i);
  });

  it("400s in English for a malformed accountId", async () => {
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: "not-a-number",
    });
    expect(res.statusCode).toBe(400);
  });

  it("disables cleanly afterwards, leaving company B off again", async () => {
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: false });
    expect(res.statusCode).toBe(200);
    expect(res.json().enabled).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// PUT accountId coherence (review findings 6-7)
// ---------------------------------------------------------------------------

describe("PUT accountId coherence", () => {
  it("400s in English when accountId rides along with enabled:false, changing nothing", async () => {
    const before = (await get(app, db.companyB, "/api/c/settings/sales-tax")).json();
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: false, accountId: String(db.accounts["2200"]!.B),
    });
    expect(res.statusCode).toBe(400);
    expect(res.json().error).toMatch(/turning sales tax on/i);
    expect(res.json().error).not.toMatch(/debit|credit/i);
    expect((await get(app, db.companyB, "/api/c/settings/sales-tax")).json()).toEqual(before);
  });

  it("422s for an INACTIVE liability account (only active accounts are choosable)", async () => {
    const inserted = await db.admin.query<{ id: string }>(
      `INSERT INTO accounts (company_id, code, name, type, active)
       VALUES ($1, '2950', 'Retired Liability', 'liability', false)
       RETURNING id`,
      [db.companyB],
    );
    const before = (await get(app, db.companyB, "/api/c/settings/sales-tax")).json();
    const res = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: true, accountId: String(inserted.rows[0]!.id),
    });
    expect(res.statusCode).toBe(422);
    expect(res.json().error).toMatch(/couldn't find that account/i);
    expect((await get(app, db.companyB, "/api/c/settings/sales-tax")).json()).toEqual(before);
  });

  it("a taxed-draft company sending accountId with a disable is refused up front — account and toggle both untouched", async () => {
    // Enable B and park a taxed draft on it (the disable-refusal state).
    const enabled = await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: true });
    expect(enabled.statusCode).toBe(200);
    const before = enabled.json();

    const customerRes = await post(app, db.companyB, "/api/c/customers", { name: "B Coherence Co" });
    const rateRes = await post(app, db.companyB, "/api/c/tax-rates", {
      name: "B Coherence Rate", ratePercent: "5",
    });
    const draft = await post(app, db.companyB, "/api/c/invoices", {
      customerId: customerRes.json().id,
      invoiceDate: "2024-06-01",
      taxRateId: rateRes.json().id,
      lines: [{ unitPrice: "10.00", incomeAccountId: String(db.accounts["4000"]!.B), taxable: true }],
    });
    expect(draft.statusCode).toBe(201);
    const invoiceId = draft.json().invoiceId;

    // The accountId guard fires BEFORE anything is written or toggled.
    const refused = await put(app, db.companyB, "/api/c/settings/sales-tax", {
      enabled: false, accountId: String(db.accounts["2200"]!.B),
    });
    expect(refused.statusCode).toBe(400);
    expect((await get(app, db.companyB, "/api/c/settings/sales-tax")).json()).toEqual(before);

    // A plain disable still hits the DB's own taxed-draft refusal — proving
    // the 400 above came from the accountId guard, not from the draft.
    const draftRefused = await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: false });
    expect(draftRefused.statusCode).toBe(409);
    expect(draftRefused.json().error).toContain(`Draft invoice ${invoiceId} still has sales tax on it`);

    // Clean up: clear the draft's rate, disable B, leaving it off as before.
    const cleared = await patch(app, db.companyB, `/api/c/invoices/${invoiceId}`, {
      customerId: customerRes.json().id,
      invoiceDate: "2024-06-01",
      taxRateId: null,
      lines: [{ unitPrice: "10.00", incomeAccountId: String(db.accounts["4000"]!.B), taxable: true }],
    });
    expect(cleared.statusCode).toBe(200);
    const off = await put(app, db.companyB, "/api/c/settings/sales-tax", { enabled: false });
    expect(off.statusCode).toBe(200);
  });
});
