// Sales tax invariants — db/migrations/0015_sales_tax.sql (Task 1 of
// docs/superpowers/plans/2026-08-02-sales-tax.md; spec:
// docs/superpowers/specs/2026-08-02-sales-tax-design.md).
// Mirrors tests/invariants/ar.test.ts's per-run database + app-role RLS
// connection patterns exactly.
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { createTestDb, type TestDb } from "../helpers/db.js";
import type { Client } from "pg";

let db: TestDb;
let c: Client; // app client scoped to companyA

beforeAll(async () => {
  db = await createTestDb();
  c = await db.appClient(db.companyA);
}, 60_000);
afterAll(async () => {
  await db.close();
});

// ---- local helpers -------------------------------------------------------

async function createCustomer(
  client: Client, companyId: string, name: string,
): Promise<number> {
  const r = await client.query(
    `INSERT INTO customers (company_id, name) VALUES ($1, $2) RETURNING id`,
    [companyId, name]);
  return Number(r.rows[0].id);
}

async function createRate(
  client: Client, companyId: string, name: string, rate: string,
  opts: { agencyVendorId?: number; active?: boolean } = {},
): Promise<number> {
  const r = await client.query(
    `INSERT INTO tax_rates (company_id, name, rate, agency_vendor_id, active)
     VALUES ($1, $2, $3, $4, $5) RETURNING id`,
    [companyId, name, rate, opts.agencyVendorId ?? null, opts.active ?? true]);
  return Number(r.rows[0].id);
}

interface LineSpec { unitPrice: string; amount: string; incomeAccountId: number; taxable?: boolean }

async function draftInvoice(
  client: Client, companyId: string, userId: string, customerId: number,
  opts: {
    invoiceDate?: string; dueDate?: string; documentOnly?: boolean;
    taxRateId?: number | null; taxAmount?: string; lines: LineSpec[];
  },
): Promise<number> {
  const invoiceDate = opts.invoiceDate ?? "2024-05-01";
  const dueDate = opts.dueDate ?? "2024-05-31";
  const r = await client.query(
    `INSERT INTO invoices
       (company_id, customer_id, invoice_date, due_date, document_only,
        tax_rate_id, tax_amount, created_by)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
    [companyId, customerId, invoiceDate, dueDate, opts.documentOnly ?? false,
     opts.taxRateId ?? null, opts.taxAmount ?? "0", userId]);
  const invoiceId = Number(r.rows[0].id);
  for (const l of opts.lines) {
    await client.query(
      `INSERT INTO invoice_lines
         (company_id, invoice_id, description, quantity, unit_price, amount,
          income_account_id, taxable)
       VALUES ($1, $2, 'item', '1', $3, $4, $5, $6)`,
      [companyId, invoiceId, l.unitPrice, l.amount, l.incomeAccountId, l.taxable ?? false]);
  }
  return invoiceId;
}

async function issueInvoice(client: Client, companyId: string, invoiceId: number, userId: string): Promise<number> {
  const r = await client.query(
    "SELECT issue_invoice($1, $2, $3) AS invoice_no", [companyId, invoiceId, userId]);
  return Number(r.rows[0].invoice_no);
}

async function receivePayment(
  client: Client, companyId: string, userId: string,
  spec: { customerId: number; amount: string; depositAccountId: number; applications?: Array<{ invoiceId: number; amount: string }> },
): Promise<number> {
  const jsonSpec = {
    customer_id: spec.customerId,
    payment_date: "2024-05-15",
    amount: spec.amount,
    deposit_account_id: spec.depositAccountId,
    memo: null,
    applications: (spec.applications ?? []).map((a) => ({ invoice_id: a.invoiceId, amount: a.amount })),
  };
  const r = await client.query(
    "SELECT receive_payment($1, $2::jsonb, $3) AS id", [companyId, JSON.stringify(jsonSpec), userId]);
  return Number(r.rows[0].id);
}

async function openBalance(client: Client, companyId: string, invoiceId: number): Promise<string> {
  const r = await client.query(
    "SELECT invoice_open_balance($1, $2) AS bal", [companyId, invoiceId]);
  return r.rows[0].bal;
}

async function enableSalesTax(client: Client, companyId: string, userId: string): Promise<number> {
  const r = await client.query(
    "SELECT enable_sales_tax($1, $2) AS account_id", [companyId, userId]);
  return Number(r.rows[0].account_id);
}

async function settingsRow(companyId: string): Promise<{ enabled: boolean; accountId: number | null }> {
  const r = await db.admin.query(
    "SELECT sales_tax_enabled, sales_tax_account_id FROM company_settings WHERE company_id = $1",
    [companyId]);
  return {
    enabled: r.rows[0].sales_tax_enabled,
    accountId: r.rows[0].sales_tax_account_id === null ? null : Number(r.rows[0].sales_tax_account_id),
  };
}

/** Posted balance of one account, derived from journal_lines (debit-positive). */
async function accountBalance(client: Client, accountId: number): Promise<string> {
  const r = await client.query(
    `SELECT coalesce(sum(l.debit - l.credit), 0)::numeric(19,4) AS bal
       FROM journal_lines l
       JOIN journal_entries e ON e.company_id = l.company_id AND e.id = l.entry_id
      WHERE e.status = 'posted' AND l.account_id = $1`, [accountId]);
  return r.rows[0].bal;
}

/** Fresh company with a minimal working chart (AR + cash + revenue), for
 * tests that need a company free of other tests' drafts/rates — e.g. the
 * enabled-gate tests, whose disable calls would otherwise trip over
 * companyA's leftover taxed drafts. */
async function createTaxCompany(
  name: string,
): Promise<{ companyId: string; revenueAccountId: number; client: Client }> {
  const comp = await db.admin.query(`INSERT INTO companies (name) VALUES ($1) RETURNING id`, [name]);
  const companyId: string = comp.rows[0].id;
  await db.admin.query(
    `INSERT INTO accounts (company_id, code, name, type) VALUES
       ($1, '1000', 'Cash', 'asset'),
       ($1, '1100', 'Accounts Receivable', 'asset')`,
    [companyId]);
  const rev = await db.admin.query(
    `INSERT INTO accounts (company_id, code, name, type)
     VALUES ($1, '4000', 'Revenue', 'income') RETURNING id`, [companyId]);
  return {
    companyId,
    revenueAccountId: Number(rev.rows[0].id),
    client: await db.appClient(companyId),
  };
}

/** Fresh company built via the admin client so tests control exactly which
 * accounts exist (adoption/ambiguity paths of enable_sales_tax). */
async function createScratchCompany(
  name: string, taxAccounts: Array<{ name: string; active?: boolean; type?: string }>,
): Promise<{ companyId: string }> {
  const comp = await db.admin.query(`INSERT INTO companies (name) VALUES ($1) RETURNING id`, [name]);
  const companyId: string = comp.rows[0].id;
  for (const a of taxAccounts) {
    await db.admin.query(
      `INSERT INTO accounts (company_id, code, name, type, active)
       VALUES ($1, $2, $3, $4, $5)`,
      [companyId, "TX-" + Math.random().toString(36).slice(2, 8), a.name, a.type ?? "liability", a.active ?? true]);
  }
  return { companyId };
}

// ---- tests ----------------------------------------------------------------

describe("enable_sales_tax / disable_sales_tax", () => {
  it("creates the 'Sales Tax Payable' liability account exactly once, and survives a disable/re-enable round trip", async () => {
    const accountId = await enableSalesTax(c, db.companyA, db.userA);
    expect(accountId).toBeGreaterThan(0);

    const accounts = await c.query(
      `SELECT id, code, name, type, active FROM accounts
        WHERE company_id = $1 AND name = 'Sales Tax Payable'`, [db.companyA]);
    expect(accounts.rows.length).toBe(1);
    expect(accounts.rows[0].type).toBe("liability");
    // helper chart already uses code 2200 (Due to Affiliate) — creation
    // walks forward to the first free 22xx code instead of colliding.
    expect(accounts.rows[0].code).toBe("2201");

    let s = await settingsRow(db.companyA);
    expect(s.enabled).toBe(true);
    expect(s.accountId).toBe(accountId);

    // second enable adopts the configured account, never duplicates
    const again = await enableSalesTax(c, db.companyA, db.userA);
    expect(again).toBe(accountId);
    const recount = await c.query(
      `SELECT count(*)::int AS n FROM accounts WHERE company_id = $1 AND name = 'Sales Tax Payable'`,
      [db.companyA]);
    expect(recount.rows[0].n).toBe(1);

    // disable: flag off, account (and any rates) left intact
    await c.query("SELECT disable_sales_tax($1, $2)", [db.companyA, db.userA]);
    s = await settingsRow(db.companyA);
    expect(s.enabled).toBe(false);
    expect(s.accountId).toBe(accountId);

    // re-enable: same account adopted again
    expect(await enableSalesTax(c, db.companyA, db.userA)).toBe(accountId);
    s = await settingsRow(db.companyA);
    expect(s.enabled).toBe(true);
  });

  it("adopts an existing single active 'Sales Tax Payable' account instead of creating one", async () => {
    const scratch = await createScratchCompany("Adopt Tax Co", [{ name: "Sales Tax Payable" }]);
    const sc = await db.appClient(scratch.companyId);
    const existing = await db.admin.query(
      `SELECT id FROM accounts WHERE company_id = $1 AND name = 'Sales Tax Payable'`,
      [scratch.companyId]);
    const adopted = await enableSalesTax(sc, scratch.companyId, db.userA);
    expect(adopted).toBe(Number(existing.rows[0].id));
    const n = await db.admin.query(
      `SELECT count(*)::int AS n FROM accounts WHERE company_id = $1 AND name = 'Sales Tax Payable'`,
      [scratch.companyId]);
    expect(n.rows[0].n).toBe(1);
  });

  it("refuses loudly when more than one active 'Sales Tax Payable' account exists", async () => {
    const scratch = await createScratchCompany("Ambiguous Tax Co", [
      { name: "Sales Tax Payable" }, { name: "Sales Tax Payable" },
    ]);
    const sc = await db.appClient(scratch.companyId);
    await expect(enableSalesTax(sc, scratch.companyId, db.userA))
      .rejects.toThrow(/More than one active account is named 'Sales Tax Payable'/);
  });

  it("refuses to disable while a draft invoice still carries a tax rate; allows it once removed", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "Disable Guard Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Disable Guard Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });

    await expect(
      c.query("SELECT disable_sales_tax($1, $2)", [db.companyA, db.userA]),
    ).rejects.toThrow(/still has sales tax on it/);

    // drafts are editable: strip the rate (and its tax) via a bare UPDATE
    await c.query(
      "UPDATE invoices SET tax_rate_id = NULL, tax_amount = 0 WHERE company_id = $1 AND id = $2",
      [db.companyA, inv]);
    await c.query("SELECT disable_sales_tax($1, $2)", [db.companyA, db.userA]);
    expect((await settingsRow(db.companyA)).enabled).toBe(false);

    await enableSalesTax(c, db.companyA, db.userA); // restore for later tests
  });
});

describe("issuing a taxed invoice posts the three-way entry", () => {
  it("debit AR = lines + tax; income credits per line; payable credit = tax; ledger stays balanced", async () => {
    const taxAccountId = await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "Three Way Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Three Way Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [
        { unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true },
        { unitPrice: "50.0000", amount: "50.0000", incomeAccountId: db.accounts["4000"].A, taxable: false },
      ],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);

    const row = await c.query(
      "SELECT status, posted_entry_id, tax_amount FROM invoices WHERE id = $1", [inv]);
    expect(row.rows[0].status).toBe("open");
    expect(row.rows[0].tax_amount).toBe("10.0000");
    const entryId = Number(row.rows[0].posted_entry_id);

    const lines = await c.query(
      `SELECT account_id, debit, credit FROM journal_lines
        WHERE entry_id = $1 ORDER BY debit DESC, credit DESC`, [entryId]);
    expect(lines.rows).toEqual([
      { account_id: String(db.accounts["1100"].A), debit: "160.0000", credit: "0.0000" },
      { account_id: String(db.accounts["4000"].A), debit: "0.0000", credit: "150.0000" },
      { account_id: String(taxAccountId), debit: "0.0000", credit: "10.0000" },
    ]);

    const oob = await c.query("SELECT ledger_out_of_balance($1) AS x", [db.companyA]);
    expect(oob.rows[0].x).toBe("0.0000");

    // trial balance shows the payable's derived (credit-normal) balance
    const tb = await c.query(
      `SELECT balance FROM trial_balance($1) WHERE account_id = $2`,
      [db.companyA, taxAccountId]);
    expect(Number(tb.rows[0].balance)).toBeLessThan(0);
  });

  it("an untaxed invoice still posts exactly the two-way entry (no zero tax line)", async () => {
    const cust = await createCustomer(c, db.companyA, "Untaxed Still Works Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      lines: [{ unitPrice: "80.0000", amount: "80.0000", incomeAccountId: db.accounts["4000"].A }],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);
    const row = await c.query("SELECT posted_entry_id FROM invoices WHERE id = $1", [inv]);
    const n = await c.query(
      "SELECT count(*)::int AS n FROM journal_lines WHERE entry_id = $1", [row.rows[0].posted_entry_id]);
    expect(n.rows[0].n).toBe(2);
  });
});

describe("sales_tax_enabled gates issuing, never voiding", () => {
  it("refuses to issue a taxed invoice while sales tax is turned off", async () => {
    const co = await createTaxCompany("Gate Off Co");
    await enableSalesTax(co.client, co.companyId, db.userA); // configures the payable account
    const rateId = await createRate(co.client, co.companyId, "Gate Rate", "10");
    await co.client.query("SELECT disable_sales_tax($1, $2)", [co.companyId, db.userA]);

    const cust = await createCustomer(co.client, co.companyId, "Gate Customer");
    const inv = await draftInvoice(co.client, co.companyId, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: co.revenueAccountId, taxable: true }],
    });
    await expect(issueInvoice(co.client, co.companyId, inv, db.userA))
      .rejects.toThrow(/sales tax is turned off for this company/);

    // no half-issued residue: still a draft, nothing posted
    const row = await co.client.query(
      "SELECT status, posted_entry_id FROM invoices WHERE id = $1", [inv]);
    expect(row.rows[0].status).toBe("draft");
    expect(row.rows[0].posted_entry_id).toBeNull();

    // and once re-enabled, the same draft issues fine
    await enableSalesTax(co.client, co.companyId, db.userA);
    await expect(issueInvoice(co.client, co.companyId, inv, db.userA)).resolves.toBeGreaterThan(0);
  });

  it("voiding a taxed invoice reverses all three lines and restores the payable to 0.0000 — even after a disable", async () => {
    const co = await createTaxCompany("Gate Void Co");
    const payableId = await enableSalesTax(co.client, co.companyId, db.userA);
    const rateId = await createRate(co.client, co.companyId, "Void Rate", "10");
    const cust = await createCustomer(co.client, co.companyId, "Void Customer");
    const inv = await draftInvoice(co.client, co.companyId, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: co.revenueAccountId, taxable: true }],
    });
    await issueInvoice(co.client, co.companyId, inv, db.userA);
    expect(await accountBalance(co.client, payableId)).toBe("-10.0000"); // credit-normal

    // disable succeeds (no taxed drafts left) and must NOT block the void:
    // the void reverses the stored entry — no issue path involved.
    await co.client.query("SELECT disable_sales_tax($1, $2)", [co.companyId, db.userA]);
    await co.client.query("SELECT void_invoice($1, $2, '2024-05-20', $3)", [co.companyId, inv, db.userA]);

    expect(await accountBalance(co.client, payableId)).toBe("0.0000");
    const row = await co.client.query("SELECT void_entry_id FROM invoices WHERE id = $1", [inv]);
    const revLines = await co.client.query(
      "SELECT count(*)::int AS n FROM journal_lines WHERE entry_id = $1", [row.rows[0].void_entry_id]);
    expect(revLines.rows[0].n).toBe(3); // AR + income + payable, all mirrored
    const oob = await co.client.query("SELECT ledger_out_of_balance($1) AS x", [co.companyId]);
    expect(oob.rows[0].x).toBe("0.0000");
  });
});

describe("a rate's percent freezes once an issued invoice uses it", () => {
  it("draft-only references leave the percent editable; an issued reference freezes it (name/agency/active stay editable)", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "Percent Freeze Rate", "9");
    const cust = await createCustomer(c, db.companyA, "Percent Freeze Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });

    // referenced only by a DRAFT: correcting the percent is allowed
    // (recompute at issue uses the current percent)
    await c.query("UPDATE tax_rates SET rate = '10' WHERE company_id = $1 AND id = $2",
      [db.companyA, rateId]);
    await issueInvoice(c, db.companyA, inv, db.userA);

    // now issued: the percent is frozen...
    await expect(
      c.query("UPDATE tax_rates SET rate = '9' WHERE company_id = $1 AND id = $2",
        [db.companyA, rateId]),
    ).rejects.toThrow(/deactivate it and create a new rate/);
    await expect(
      db.admin.query("UPDATE tax_rates SET rate = '9' WHERE company_id = $1 AND id = $2",
        [db.companyA, rateId]),
    ).rejects.toThrow(/deactivate it and create a new rate/);

    // ...but name, agency, and active are not
    await c.query(
      "UPDATE tax_rates SET name = 'Percent Freeze Rate (renamed)', active = false WHERE company_id = $1 AND id = $2",
      [db.companyA, rateId]);
    await c.query("UPDATE tax_rates SET active = true WHERE company_id = $1 AND id = $2",
      [db.companyA, rateId]);
  });
});

describe("tax recompute guard (integer-cents HALF-UP, DB-verified)", () => {
  it("refuses a tax_amount that doesn't match rate x taxable lines; accepts the matching one", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "Recompute Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Recompute Co");

    const bad = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "9.9900",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, bad, db.userA))
      .rejects.toThrow(/does not match the .* rate/);

    const good = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, good, db.userA)).resolves.toBeGreaterThan(0);
  });

  it("only taxable lines count toward the recomputed tax", async () => {
    const rateId = await createRate(c, db.companyA, "Mixed Lines Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Mixed Lines Co");
    // 100 taxable + 900 non-taxable: tax must be 10, not 100
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "100.0000",
      lines: [
        { unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true },
        { unitPrice: "900.0000", amount: "900.0000", incomeAccountId: db.accounts["4000"].A, taxable: false },
      ],
    });
    await expect(issueInvoice(c, db.companyA, inv, db.userA))
      .rejects.toThrow(/does not match the .* rate/);
  });

  it("HALF-UP boundary: 7.25% of 99.99 = 7.249275 rounds to 7.25", async () => {
    const rateId = await createRate(c, db.companyA, "CA Boundary Rate", "7.25");
    const cust = await createCustomer(c, db.companyA, "Boundary Co");

    const halfDown = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "7.2400",
      lines: [{ unitPrice: "99.9900", amount: "99.9900", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, halfDown, db.userA))
      .rejects.toThrow(/does not match the .* rate/);

    const halfUp = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "7.2500",
      lines: [{ unitPrice: "99.9900", amount: "99.9900", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, halfUp, db.userA)).resolves.toBeGreaterThan(0);
  });

  it("exact .005 boundary: 5% of 10.10 = 0.505 rounds UP to 0.51, and 0.50 is refused", async () => {
    const rateId = await createRate(c, db.companyA, "Nickel Boundary Rate", "5");
    const cust = await createCustomer(c, db.companyA, "Nickel Boundary Co");

    const halfDown = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "0.5000",
      lines: [{ unitPrice: "10.1000", amount: "10.1000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, halfDown, db.userA))
      .rejects.toThrow(/does not match the .* rate/);

    const halfUp = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "0.5100",
      lines: [{ unitPrice: "10.1000", amount: "10.1000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, halfUp, db.userA)).resolves.toBeGreaterThan(0);
  });
});

describe("tax consistency: amount requires rate", () => {
  it("a nonzero tax_amount with no tax_rate_id is impossible even at the draft-insert level (table CHECK)", async () => {
    const cust = await createCustomer(c, db.companyA, "No Rate Tax Co");
    await expect(
      c.query(
        `INSERT INTO invoices
           (company_id, customer_id, invoice_date, due_date, tax_amount, created_by)
         VALUES ($1, $2, '2024-05-01', '2024-05-31', '5.0000', $3)`,
        [db.companyA, cust, db.userA]),
    ).rejects.toThrow(/invoices_tax_amount_requires_rate|check constraint/);
  });

  it("negative tax_amount is refused (CHECK)", async () => {
    const rateId = await createRate(c, db.companyA, "Negative Tax Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Negative Tax Co");
    await expect(
      c.query(
        `INSERT INTO invoices
           (company_id, customer_id, invoice_date, due_date, tax_rate_id, tax_amount, created_by)
         VALUES ($1, $2, '2024-05-01', '2024-05-31', $3, '-1.0000', $4)`,
        [db.companyA, cust, rateId, db.userA]),
    ).rejects.toThrow(/check constraint/);
  });
});

describe("rate state and ownership at issue", () => {
  it("an inactive rate cannot be used to issue", async () => {
    const rateId = await createRate(c, db.companyA, "Retired Rate", "10");
    await c.query("UPDATE tax_rates SET active = false WHERE company_id = $1 AND id = $2",
      [db.companyA, rateId]);
    const cust = await createCustomer(c, db.companyA, "Retired Rate Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, inv, db.userA))
      .rejects.toThrow(/inactive/);
  });

  it("another company's rate is unusable on an invoice (composite FK)", async () => {
    const cB = await db.appClient(db.companyB);
    const foreignRate = await createRate(cB, db.companyB, "B Only Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Foreign Rate Co");
    await expect(
      draftInvoice(c, db.companyA, db.userA, cust, {
        taxRateId: foreignRate, taxAmount: "10.0000",
        lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
      }),
    ).rejects.toThrow(/foreign key|violates/);
  });
});

describe("tax fields freeze at issue (immutability)", () => {
  async function issuedTaxedInvoice(): Promise<number> {
    const rateId = await createRate(
      c, db.companyA, "Freeze Rate " + Math.random().toString(36).slice(2, 8), "10");
    const cust = await createCustomer(
      c, db.companyA, "Freeze Co " + Math.random().toString(36).slice(2, 8));
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);
    return inv;
  }

  it("tax_amount and tax_rate_id cannot change after issue", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const inv = await issuedTaxedInvoice();
    await expect(
      c.query("UPDATE invoices SET tax_amount = '0.0000' WHERE id = $1", [inv]),
    ).rejects.toThrow(/immutable/);
    await expect(
      c.query("UPDATE invoices SET tax_rate_id = NULL, tax_amount = 0 WHERE id = $1", [inv]),
    ).rejects.toThrow(/immutable/);
    // superuser gets the same refusal — the trigger fires for every role
    await expect(
      db.admin.query("UPDATE invoices SET tax_amount = '999.0000' WHERE id = $1", [inv]),
    ).rejects.toThrow(/immutable/);
  });

  it("a line's taxable flag cannot change after issue", async () => {
    const inv = await issuedTaxedInvoice();
    await expect(
      c.query("UPDATE invoice_lines SET taxable = false WHERE invoice_id = $1", [inv]),
    ).rejects.toThrow(/immutable/);
    await expect(
      db.admin.query("UPDATE invoice_lines SET taxable = false WHERE invoice_id = $1", [inv]),
    ).rejects.toThrow(/immutable/);
  });
});

describe("grand total includes tax everywhere", () => {
  it("invoice_open_balance = lines + tax - payments; ar_aging bucket amount includes tax", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "Balance Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Balance Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      invoiceDate: "2024-05-01", dueDate: "2024-05-31",
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);
    expect(await openBalance(c, db.companyA, inv)).toBe("110.0000");

    await receivePayment(c, db.companyA, db.userA, {
      customerId: cust, amount: "60.0000", depositAccountId: db.accounts["1000"].A,
      applications: [{ invoiceId: inv, amount: "60.0000" }],
    });
    expect(await openBalance(c, db.companyA, inv)).toBe("50.0000");

    const aging = await c.query(
      `SELECT open_amount, bucket FROM ar_aging($1, '2024-06-15') WHERE invoice_id = $2`,
      [db.companyA, inv]);
    expect(aging.rows.length).toBe(1);
    expect(aging.rows[0].open_amount).toBe("50.0000");
    expect(aging.rows[0].bucket).toBe("1-30");
  });

  it("a taxed invoice can be paid in full for lines + tax (over-application guard is tax-inclusive)", async () => {
    const rateId = await createRate(c, db.companyA, "Full Pay Rate", "10");
    const cust = await createCustomer(c, db.companyA, "Full Pay Tax Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);
    // pre-0015 this would have raised "over-applied" (guard summed lines only)
    await receivePayment(c, db.companyA, db.userA, {
      customerId: cust, amount: "110.0000", depositAccountId: db.accounts["1000"].A,
      applications: [{ invoiceId: inv, amount: "110.0000" }],
    });
    expect(await openBalance(c, db.companyA, inv)).toBe("0.0000");

    // but paying MORE than lines + tax is still over-applied
    const inv2 = await draftInvoice(c, db.companyA, db.userA, cust, {
      taxRateId: rateId, taxAmount: "10.0000",
      lines: [{ unitPrice: "100.0000", amount: "100.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await issueInvoice(c, db.companyA, inv2, db.userA);
    await expect(
      receivePayment(c, db.companyA, db.userA, {
        customerId: cust, amount: "120.0000", depositAccountId: db.accounts["1000"].A,
        applications: [{ invoiceId: inv2, amount: "120.0000" }],
      }),
    ).rejects.toThrow(/over-applied/);
  });
});

describe("document_only invoices with tax", () => {
  it("stores the tax fields, posts nothing, and is excluded from sales_tax_summary", async () => {
    await enableSalesTax(c, db.companyA, db.userA);
    const rateId = await createRate(c, db.companyA, "History Rate", "10");
    const cust = await createCustomer(c, db.companyA, "History Tax Co");
    // dedicated date window so the summary assertion sees ONLY this invoice
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      documentOnly: true, invoiceDate: "2025-01-10", dueDate: "2025-02-09",
      taxRateId: rateId, taxAmount: "20.0000",
      lines: [{ unitPrice: "200.0000", amount: "200.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await issueInvoice(c, db.companyA, inv, db.userA);

    const row = await c.query(
      "SELECT status, posted_entry_id, tax_rate_id, tax_amount FROM invoices WHERE id = $1", [inv]);
    expect(row.rows[0].status).toBe("open");
    expect(row.rows[0].posted_entry_id).toBeNull(); // no journal entry
    expect(Number(row.rows[0].tax_rate_id)).toBe(rateId);
    expect(row.rows[0].tax_amount).toBe("20.0000");

    const summary = await c.query(
      "SELECT * FROM sales_tax_summary($1, '2025-01-01', '2025-01-31')", [db.companyA]);
    expect(summary.rows.length).toBe(0);
  });

  it("document_only tax amounts are still validated against the rate", async () => {
    const rateId = await createRate(c, db.companyA, "History Guard Rate", "10");
    const cust = await createCustomer(c, db.companyA, "History Guard Co");
    const inv = await draftInvoice(c, db.companyA, db.userA, cust, {
      documentOnly: true, invoiceDate: "2025-01-11", dueDate: "2025-02-10",
      taxRateId: rateId, taxAmount: "19.0000",
      lines: [{ unitPrice: "200.0000", amount: "200.0000", incomeAccountId: db.accounts["4000"].A, taxable: true }],
    });
    await expect(issueInvoice(c, db.companyA, inv, db.userA))
      .rejects.toThrow(/does not match the .* rate/);
  });
});

describe("sales_tax_summary", () => {
  it("groups by rate with correct math, excludes voided invoices, filters by date, joins agency name", async () => {
    const cB = await db.appClient(db.companyB);
    await enableSalesTax(cB, db.companyB, db.userB);
    const vend = await cB.query(
      `INSERT INTO vendors (company_id, name) VALUES ($1, 'State Board of Equalization') RETURNING id`,
      [db.companyB]);
    const agencyId = Number(vend.rows[0].id);
    const r1 = await createRate(cB, db.companyB, "State Tax 10%", "10", { agencyVendorId: agencyId });
    const r2 = await createRate(cB, db.companyB, "County Tax 5%", "5");
    const cust = await createCustomer(cB, db.companyB, "Summary Customer");
    const inc = db.accounts["4000"].B;

    // R1: 100 taxable + 50 non-taxable (tax 10), then 200 taxable (tax 20)
    const i1 = await draftInvoice(cB, db.companyB, db.userB, cust, {
      invoiceDate: "2024-06-05", dueDate: "2024-07-05", taxRateId: r1, taxAmount: "10.0000",
      lines: [
        { unitPrice: "100.0000", amount: "100.0000", incomeAccountId: inc, taxable: true },
        { unitPrice: "50.0000", amount: "50.0000", incomeAccountId: inc, taxable: false },
      ],
    });
    const i2 = await draftInvoice(cB, db.companyB, db.userB, cust, {
      invoiceDate: "2024-06-20", dueDate: "2024-07-20", taxRateId: r1, taxAmount: "20.0000",
      lines: [{ unitPrice: "200.0000", amount: "200.0000", incomeAccountId: inc, taxable: true }],
    });
    // R2: 40 taxable (tax 2)
    const i3 = await draftInvoice(cB, db.companyB, db.userB, cust, {
      invoiceDate: "2024-06-10", dueDate: "2024-07-10", taxRateId: r2, taxAmount: "2.0000",
      lines: [{ unitPrice: "40.0000", amount: "40.0000", incomeAccountId: inc, taxable: true }],
    });
    // R1 but voided — excluded entirely
    const i4 = await draftInvoice(cB, db.companyB, db.userB, cust, {
      invoiceDate: "2024-06-15", dueDate: "2024-07-15", taxRateId: r1, taxAmount: "6.0000",
      lines: [{ unitPrice: "60.0000", amount: "60.0000", incomeAccountId: inc, taxable: true }],
    });
    // R1 but outside the date range — excluded
    const i5 = await draftInvoice(cB, db.companyB, db.userB, cust, {
      invoiceDate: "2024-07-05", dueDate: "2024-08-04", taxRateId: r1, taxAmount: "50.0000",
      lines: [{ unitPrice: "500.0000", amount: "500.0000", incomeAccountId: inc, taxable: true }],
    });
    for (const inv of [i1, i2, i3, i4, i5]) {
      await issueInvoice(cB, db.companyB, inv, db.userB);
    }
    await cB.query("SELECT void_invoice($1, $2, '2024-06-16', $3)", [db.companyB, i4, db.userB]);

    const res = await cB.query(
      "SELECT * FROM sales_tax_summary($1, '2024-06-01', '2024-06-30') ORDER BY rate_name",
      [db.companyB]);
    const rows = res.rows.filter((r) => [r1, r2].includes(Number(r.tax_rate_id)));
    expect(rows.length).toBe(2);

    const county = rows.find((r) => Number(r.tax_rate_id) === r2)!;
    expect(county.rate_name).toBe("County Tax 5%");
    expect(county.rate).toBe("5.0000");
    expect(county.agency_vendor_id).toBeNull();
    expect(county.agency_name).toBeNull();
    expect(county.taxable_sales).toBe("40.0000");
    expect(county.non_taxable_sales).toBe("0.0000");
    expect(county.tax_collected).toBe("2.0000");

    const state = rows.find((r) => Number(r.tax_rate_id) === r1)!;
    expect(state.rate).toBe("10.0000");
    expect(Number(state.agency_vendor_id)).toBe(agencyId);
    expect(state.agency_name).toBe("State Board of Equalization");
    expect(state.taxable_sales).toBe("300.0000");   // 100 + 200; void + out-of-range excluded
    expect(state.non_taxable_sales).toBe("50.0000");
    expect(state.tax_collected).toBe("30.0000");
  });

  it("refuses when the session company does not match", async () => {
    await expect(
      c.query("SELECT * FROM sales_tax_summary($1, '2024-01-01', '2024-12-31')", [db.companyB]),
    ).rejects.toThrow(/session company does not match/);
  });
});

describe("tax_rates armor (RLS, TRUNCATE, constraints)", () => {
  it("rates are invisible cross-company, and with no session company at all", async () => {
    await createRate(c, db.companyA, "A Private Rate", "8.25");
    const cB = await db.appClient(db.companyB);
    const seen = await cB.query("SELECT * FROM tax_rates WHERE name = 'A Private Rate'");
    expect(seen.rows.length).toBe(0);
    const anon = await db.appClient(null);
    const none = await anon.query("SELECT count(*)::int AS n FROM tax_rates");
    expect(none.rows[0].n).toBe(0);
  });

  it("cannot insert a rate for another company (RLS WITH CHECK)", async () => {
    await expect(
      c.query(
        `INSERT INTO tax_rates (company_id, name, rate) VALUES ($1, 'Sneaky Rate', '5')`,
        [db.companyB]),
    ).rejects.toThrow(/row-level security/);
  });

  it("TRUNCATE is forbidden even for the superuser", async () => {
    await expect(db.admin.query("TRUNCATE tax_rates"))
      .rejects.toThrow(/TRUNCATE is not allowed|foreign key/);
  });

  it("rate bounds and per-company name uniqueness hold", async () => {
    await expect(createRate(c, db.companyA, "Too Big Rate", "101"))
      .rejects.toThrow(/check constraint|violates/);
    await expect(createRate(c, db.companyA, "Negative Rate", "-1"))
      .rejects.toThrow(/check constraint|violates/);
    await createRate(c, db.companyA, "Dup Rate", "5");
    await expect(createRate(c, db.companyA, "Dup Rate", "6"))
      .rejects.toThrow(/duplicate key|unique/);
    // same name is fine in another company
    const cB = await db.appClient(db.companyB);
    await expect(createRate(cB, db.companyB, "Dup Rate", "6")).resolves.toBeGreaterThan(0);
  });

  it("agency vendor must belong to the same company (composite FK)", async () => {
    const cB = await db.appClient(db.companyB);
    const vend = await cB.query(
      `INSERT INTO vendors (company_id, name) VALUES ($1, 'B Vendor For FK') RETURNING id`,
      [db.companyB]);
    await expect(
      createRate(c, db.companyA, "Cross Agency Rate", "5",
        { agencyVendorId: Number(vend.rows[0].id) }),
    ).rejects.toThrow(/foreign key|violates/);
  });

  it("sales_tax_account_id must be a liability account", async () => {
    await expect(
      c.query(
        `UPDATE company_settings SET sales_tax_account_id = $2 WHERE company_id = $1`,
        [db.companyA, db.accounts["4000"].A]),
    ).rejects.toThrow(/liability account/);
  });

  it("the app role cannot flip sales_tax_enabled by hand (functions only)", async () => {
    await expect(
      c.query(
        `UPDATE company_settings SET sales_tax_enabled = false WHERE company_id = $1`,
        [db.companyA]),
    ).rejects.toThrow(/permission denied/);
  });
});
