// Integration tests for the sample-company seeder (Task 2 of
// docs/superpowers/plans/2026-08-01-launch.md; spec: docs/superpowers/specs/
// 2026-08-01-launch-design.md's "Sample company" section). Same patterns as
// tests/server/bankrec.test.ts (createTestDb, app.inject, shared beforeAll/
// afterAll) — the seeder itself is expensive (a few hundred app.inject calls
// driving every real route it touches), so it runs ONCE in beforeAll and
// every assertion below reuses that one seeded company rather than
// re-seeding per test.

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, url: string, companyId?: string, payload?: unknown) {
  const headers: Record<string, string> = {};
  if (companyId) headers["x-company-id"] = companyId;
  return app.inject({ method: "POST", url, headers, payload: payload ?? {} });
}
function get(app: FastifyInstance, url: string, companyId?: string) {
  const headers: Record<string, string> = {};
  if (companyId) headers["x-company-id"] = companyId;
  return app.inject({ method: "GET", url, headers });
}

let db: TestDb;
let app: FastifyInstance;
let sampleCompanyId: string;

// The seeder itself (a few hundred in-process app.inject calls) can take a
// while on a slow disk/CI runner — a generous timeout on beforeAll avoids a
// flaky false failure that isn't actually about correctness.
beforeAll(async () => {
  db = await createTestDb();
  app = await buildTestApp(db.dbName);

  const res = await post(app, "/api/sample-company");
  expect(res.statusCode).toBe(201);
  sampleCompanyId = (res.json() as { companyId: string }).companyId;
}, 180_000);

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

describe("POST /api/sample-company — seeds Riverbend Design Studio", () => {
  it("creates a company flagged is_sample = true", async () => {
    const res = await get(app, "/api/companies");
    const body = res.json() as { companies: Array<{ id: string; name: string; isSample: boolean }> };
    const riverbend = body.companies.find((c) => c.id === sampleCompanyId);
    expect(riverbend).toBeDefined();
    expect(riverbend?.name).toBe("Riverbend Design Studio");
    expect(riverbend?.isSample).toBe(true);
  });

  it("balances the ledger exactly (ledger_out_of_balance = 0.0000)", async () => {
    const client = await db.appClient(sampleCompanyId);
    const r = await client.query("SELECT ledger_out_of_balance($1) AS d", [sampleCompanyId]);
    expect(r.rows[0].d).toBe("0.0000");
  });

  it("creates customers and vendors in the expected counts", async () => {
    const customers = await get(app, "/api/c/customers", sampleCompanyId);
    expect((customers.json() as unknown[]).length).toBe(5);

    const vendors = await get(app, "/api/c/vendors", sampleCompanyId);
    const vendorList = vendors.json() as Array<{ track1099: boolean }>;
    expect(vendorList.length).toBe(5);
    expect(vendorList.filter((v) => v.track1099).length).toBe(1);
  });

  it("creates invoices and bills within the expected ranges", async () => {
    const invoices = await get(app, "/api/c/invoices?limit=500", sampleCompanyId);
    const invoiceBody = invoices.json() as { total: number };
    expect(invoiceBody.total).toBeGreaterThanOrEqual(20);
    expect(invoiceBody.total).toBeLessThanOrEqual(30);

    const bills = await get(app, "/api/c/bills?limit=500", sampleCompanyId);
    const billBody = bills.json() as { total: number };
    expect(billBody.total).toBeGreaterThanOrEqual(25);
    // Widened from 35 when the two queued-check bills landed (35 exactly), so
    // this range keeps meaning "roughly this many" rather than sitting on its
    // own bound and failing the next time a chapter adds a bill.
    expect(billBody.total).toBeLessThanOrEqual(40);
  });

  it("posts more than 80 journal entries total", async () => {
    const client = await db.appClient(sampleCompanyId);
    const r = await client.query(
      "SELECT count(*)::int AS n FROM journal_entries WHERE company_id = $1 AND status = 'posted'",
      [sampleCompanyId],
    );
    expect(r.rows[0].n).toBeGreaterThan(80);
  });

  it("has non-zero current and overdue buckets in AR aging", async () => {
    const res = await get(app, "/api/c/reports/aging", sampleCompanyId);
    const body = res.json() as {
      totals: { current: string; d1_30: string; d31_60: string; d61_90: string; d90_plus: string };
    };
    expect(body.totals.current).not.toBe("0.0000");
    const overdueTotal = [body.totals.d1_30, body.totals.d31_60, body.totals.d61_90, body.totals.d90_plus]
      .some((b) => b !== "0.0000");
    expect(overdueTotal).toBe(true);
  });

  it("has a non-empty AP due-soon (current bucket, unpaid) balance", async () => {
    const res = await get(app, "/api/c/reports/ap-aging", sampleCompanyId);
    const body = res.json() as { totals: { current: string } };
    expect(body.totals.current).not.toBe("0.0000");
  });

  it("reconciles exactly one month, leaving exactly one open statement", async () => {
    const client = await db.appClient(sampleCompanyId);
    const reconCount = await client.query(
      "SELECT count(*)::int AS n FROM account_reconciliations WHERE company_id = $1", [sampleCompanyId],
    );
    expect(reconCount.rows[0].n).toBe(1);

    const openCount = await client.query(
      "SELECT count(*)::int AS n FROM bank_statements WHERE company_id = $1 AND status = 'open'",
      [sampleCompanyId],
    );
    expect(openCount.rows[0].n).toBe(1);

    const reconciledCount = await client.query(
      "SELECT count(*)::int AS n FROM bank_statements WHERE company_id = $1 AND status = 'reconciled'",
      [sampleCompanyId],
    );
    expect(reconciledCount.rows[0].n).toBe(1);
  });

  it("printed checks with sequential numbers", async () => {
    const client = await db.appClient(sampleCompanyId);
    const r = await client.query(
      `SELECT check_no::text AS check_no FROM bill_payments
        WHERE company_id = $1 AND method = 'check' AND check_no IS NOT NULL
        ORDER BY check_no`,
      [sampleCompanyId],
    );
    const numbers = r.rows.map((row: { check_no: string }) => Number(row.check_no));
    expect(numbers.length).toBe(3);
    for (let i = 1; i < numbers.length; i++) {
      expect(numbers[i]).toBe(numbers[i - 1]! + 1);
    }
  });

  it("also leaves checks QUEUED to print, so the queue is never empty", async () => {
    // Every check this sample held used to be already printed, so the Checks
    // screen read "No checks queued to print" for every account and the print
    // route correctly refused each one — the sample could not demonstrate
    // check printing at all. Check printing is something QuickBooks charges
    // for, and it is one of the images on the marketing site.
    //
    // Asserted through the route the SCREEN actually reads, not only against
    // the column, because "the queue is not empty" is a claim about what a
    // visitor sees.
    const accounts = await get(app, "/api/c/accounts?grouped=1", sampleCompanyId);
    const money = (accounts.json() as { money: Array<{ id: string; name: string }> }).money;
    const checking = money.find((a) => a.name === "Checking Account");
    expect(checking).toBeDefined();

    const queue = await get(
      app, `/api/c/checks/queue?accountId=${checking!.id}`, sampleCompanyId,
    );
    expect(queue.statusCode).toBe(200);
    const body = queue.json() as {
      payments: Array<{ paymentId: string; amount: string; vendorName: string }>;
      nextCheckNo: string;
    };
    // TWO, not one: the check stock is three to a page, and a queue of one
    // orphan photographs as an edge case rather than as a Friday-afternoon run.
    expect(body.payments.length).toBeGreaterThanOrEqual(2);
    // The queue has to follow the printed run, or the numbering demo is a lie.
    expect(Number(body.nextCheckNo)).toBeGreaterThan(1);

    // Both states present at once: printed checks carry a number, queued ones
    // do not yet.
    const client = await db.appClient(sampleCompanyId);
    const byStatus = await client.query(
      `SELECT check_status, count(*)::int AS n,
              count(check_no)::int AS numbered
         FROM bill_payments
        WHERE company_id = $1 AND method = 'check'
        GROUP BY check_status`,
      [sampleCompanyId],
    );
    const rows = Object.fromEntries(
      byStatus.rows.map((r2: { check_status: string; n: number; numbered: number }) =>
        [r2.check_status, r2]),
    );
    expect(rows.printed?.n).toBe(3);
    expect(rows.printed?.numbered).toBe(3);
    expect(rows.to_print?.n).toBe(2);
    // A cheque waiting to be printed cannot already have a number.
    expect(rows.to_print?.numbered).toBe(0);
  });

  // -------------------------------------------------------------------------
  // Two things that are wrong in a DEMO rather than wrong in the books, and so
  // have no other guard anywhere: the sample is the first screen a stranger
  // sees, and both of these were visible in it. Pinned here because the ledger
  // itself is perfectly happy with either one.
  // -------------------------------------------------------------------------
  it("never dates anything in the future", async () => {
    // The seeder builds most dates as `first-of-some-month + n days`, so for a
    // company seeded early in a month that arithmetic overshoots: the shipped
    // sample carried a customer payment and a postage payment dated a week and
    // a half AHEAD of the seed. A transaction register showing next week's
    // transactions reads as a broken program. notFuture() in the seeder clamps
    // every posting date; this is what stops it coming back.
    const client = await db.appClient(sampleCompanyId);

    const entries = await client.query(
      `SELECT count(*)::int AS n,
              coalesce(max(entry_date)::text, '') AS worst
         FROM journal_entries
        WHERE company_id = $1 AND entry_date > current_date`,
      [sampleCompanyId],
    );
    expect(entries.rows[0].n, `entries dated as late as ${entries.rows[0].worst}`).toBe(0);

    // Imported bank lines post nothing, but they are what the reconcile-first
    // home screen shows, and a reader cannot tell the difference.
    const lines = await client.query(
      `SELECT count(*)::int AS n FROM bank_statement_lines
        WHERE company_id = $1 AND posted_date > current_date`,
      [sampleCompanyId],
    );
    expect(lines.rows[0].n).toBe(0);
  });

  it("ends with a healthy, positive checking balance", async () => {
    // The home screen leads with the bank balance. At the original $5,000
    // owner contribution this sample finished about $2,500 OVERDRAWN — a demo
    // business arguing against the product on the product's own front page.
    //
    // Compared in SQL, in numeric: a monetary value never becomes a JS number
    // to satisfy a matcher (CLAUDE.md §2).
    const client = await db.appClient(sampleCompanyId);
    const r = await client.query(
      `SELECT b::text AS balance, b >= 0 AS non_negative, b > 10000 AS healthy
         FROM account_book_balance_asof(
                $1,
                (SELECT id FROM accounts WHERE company_id = $1 AND code = '1010'),
                current_date) AS b`,
      [sampleCompanyId],
    );
    const row = r.rows[0];
    expect(row.non_negative, `checking balance was ${row.balance}`).toBe(true);
    // Not merely non-negative: a sample that limps to $12.40 is no better a
    // demo than one that limps to -$12.40.
    expect(row.healthy, `checking balance was ${row.balance}`).toBe(true);
  });

  // -------------------------------------------------------------------------
  // Inventory chapter (added 2026-08-06). The sample is the guided tour of the
  // product, so it has to demonstrate the inventory milestone too — and, more
  // than that, it has to demonstrate it CORRECTLY: every number below is
  // hand-computable from the seeder's literal catalog, so a costing regression
  // shows up here as a wrong figure rather than as a vague "it still ties".
  //
  //   print  (40 @ 14.00 + 24 @ 15.50) / 64   = 932.00  / 64   = 14.5625
  //   frame  (30 @ 22.50 + 18 @ 24.00) / 48   = 1107.00 / 48   = 23.0625
  //   box    (100 @ 1.85 + 150 @ 2.10) / 250  = 500.00  / 250  = 2.00
  //   seal   (5000 @ .0042 + 3000 @ .0060) / 8000 = 39.00 / 8000 = 0.004875
  // -------------------------------------------------------------------------
  describe("inventory chapter", () => {
    it("creates stocked items, a finished good and a service item", async () => {
      const res = await get(app, "/api/c/items", sampleCompanyId);
      expect(res.statusCode).toBe(200);
      const body = res.json() as {
        items: Array<{ name: string; sku: string | null; kind: string }>;
        everHeldStock: boolean;
      };
      expect(body.everHeldStock).toBe(true);
      expect(body.items.length).toBe(6);
      expect(body.items.filter((i) => i.kind === "inventory").length).toBe(5);
      expect(body.items.filter((i) => i.kind === "service").length).toBe(1);
      expect(body.items.map((i) => i.sku).sort()).toEqual(
        ["BOX-LG", "FRM-1824", "KIT-1824", "PRT-1824", "STK-FOIL", "SVC-HOUR"],
      );
    });

    it("gives the finished good a parts list", async () => {
      const items = await get(app, "/api/c/items", sampleCompanyId);
      const kit = (items.json() as { items: Array<{ id: string; sku: string | null }> })
        .items.find((i) => i.sku === "KIT-1824")!;
      const res = await get(app, `/api/c/items/${kit.id}/components`, sampleCompanyId);
      expect(res.statusCode).toBe(200);
      const body = res.json() as { components: Array<{ sku: string | null; qtyPer: string }> };
      expect(body.components.length).toBe(4);
      const bySku = Object.fromEntries(body.components.map((c) => [c.sku, c.qtyPer]));
      expect(bySku).toEqual({
        "PRT-1824": "1.0000", "FRM-1824": "1.0000",
        "BOX-LG": "1.0000", "STK-FOIL": "2.0000",
      });
    });

    it("closes the purchase order by entering the receiving bill", async () => {
      const res = await get(app, "/api/c/purchase-orders?limit=50", sampleCompanyId);
      const body = res.json() as {
        total: number;
        purchaseOrders: Array<{ state: string; poNo: string | null; overReceivedLines: number }>;
      };
      expect(body.total).toBe(1);
      expect(body.purchaseOrders[0]!.poNo).toBe("1");
      expect(body.purchaseOrders[0]!.state).toBe("closed");
      expect(body.purchaseOrders[0]!.overReceivedLines).toBe(0);
    });

    it("records exactly one build of the finished good", async () => {
      const res = await get(app, "/api/c/builds", sampleCompanyId);
      const body = res.json() as {
        total: number;
        builds: Array<{ qty: string; status: string; value: string; entryId: string }>;
      };
      expect(body.total).toBe(1);
      const build = body.builds[0]!;
      expect(build.status).toBe("posted");
      expect(build.qty).toBe("12.0000");
      // 12 x (14.5625 + 23.0625 + 2.00) + 24 x 0.004875
      //   = 174.75 + 276.75 + 24.00 + 0.117
      expect(build.value).toBe("475.6170");
      // Every movement rides a posted journal entry — including a build whose
      // components and assembly share one inventory account, which posts a
      // wash entry rather than nothing (spec as-built decision 3).
      expect(build.entryId).toBeTruthy();
    });

    it("costs stock at a weighted average, not at either purchase price", async () => {
      const res = await get(app, "/api/c/reports/inventory-valuation", sampleCompanyId);
      expect(res.statusCode).toBe(200);
      const body = res.json() as {
        items: Array<{ sku: string | null; qtyOnHand: string; averageCost: string; value: string }>;
        accounts: Array<{ ledgerValue: string; movementValue: string; difference: string }>;
        totalValue: string;
        outOfTie: string;
        ties: boolean;
      };

      const bySku = Object.fromEntries(body.items.map((i) => [i.sku, i]));
      expect(bySku["PRT-1824"]).toMatchObject({
        qtyOnHand: "46.0000", averageCost: "14.562500", value: "669.8750",
      });
      expect(bySku["FRM-1824"]).toMatchObject({
        qtyOnHand: "33.0000", averageCost: "23.062500", value: "761.0625",
      });
      // The one to look at: $2.00 is neither $1.85 nor $2.10.
      expect(bySku["BOX-LG"]).toMatchObject({
        qtyOnHand: "238.0000", averageCost: "2.000000", value: "476.0000",
      });
      // Sub-cent, which is why unit_cost is numeric(19,6).
      expect(bySku["STK-FOIL"]).toMatchObject({
        qtyOnHand: "7976.0000", averageCost: "0.004875", value: "38.8830",
      });
      expect(bySku["KIT-1824"]).toMatchObject({
        qtyOnHand: "8.0000", averageCost: "39.634750", value: "317.0780",
      });

      expect(body.totalValue).toBe("2262.8985");
    });

    it("ties inventory to the ledger exactly, per account and in total", async () => {
      const res = await get(app, "/api/c/reports/inventory-valuation", sampleCompanyId);
      const body = res.json() as {
        accounts: Array<{ ledgerValue: string; movementValue: string; difference: string }>;
        outOfTie: string;
        ties: boolean;
      };
      expect(body.accounts.length).toBe(1);
      for (const account of body.accounts) {
        // Two independent derivations of the same number — journal_lines vs.
        // inventory_movements — with nothing between them.
        expect(account.difference).toBe("0.0000");
        expect(account.ledgerValue).toBe(account.movementValue);
        expect(account.ledgerValue).toBe("2262.8985");
      }
      expect(body.outOfTie).toBe("0.0000");
      expect(body.ties).toBe(true);

      // ...and the same assertion straight from the database function the
      // health panel reads, not only through the report that formats it.
      const client = await db.appClient(sampleCompanyId);
      const tie = await client.query(
        "SELECT inventory_out_of_tie($1, NULL) AS d", [sampleCompanyId],
      );
      expect(tie.rows[0].d).toBe("0.0000");
      const balanced = await client.query(
        "SELECT ledger_out_of_balance($1) AS d", [sampleCompanyId],
      );
      expect(balanced.rows[0].d).toBe("0.0000");
    });

    it("puts cost of goods sold on the P&L", async () => {
      const client = await db.appClient(sampleCompanyId);
      // Debit-positive in storage (CLAUDE.md §12), so an expense balance is
      // positive here; the sign flip is the presentation layer's job.
      const r = await client.query(
        `SELECT coalesce(sum(l.debit - l.credit), 0)::numeric(19,4) AS balance
           FROM journal_lines l
           JOIN accounts a ON a.company_id = l.company_id AND a.id = l.account_id
          WHERE l.company_id = $1 AND a.name = 'Cost of Goods Sold'`,
        [sampleCompanyId],
      );
      // 4 framed prints @ 39.63475 = 158.5390, 6 prints @ 14.5625 = 87.3750,
      // 3 frames @ 23.0625 = 69.1875.
      expect(r.rows[0].balance).toBe("315.1015");
    });

    it("leaves the sample company's sales tax switched off", async () => {
      // A prior spec decision keeps Riverbend a non-taxed business so the tax
      // features show as OFF in the tour; selling goods must not change that.
      const res = await get(app, "/api/c/settings/sales-tax", sampleCompanyId);
      expect(res.statusCode).toBe(200);
      expect((res.json() as { enabled: boolean }).enabled).toBe(false);
    });
  });

  it("refuses a second run with a 409 and does not create another company", async () => {
    const before = await get(app, "/api/companies");
    const beforeCount = (before.json() as { companies: unknown[] }).companies.length;

    const res = await post(app, "/api/sample-company");
    expect(res.statusCode).toBe(409);
    expect(res.json().error).toMatch(/sample company already exists/i);

    const after = await get(app, "/api/companies");
    const afterCount = (after.json() as { companies: unknown[] }).companies.length;
    expect(afterCount).toBe(beforeCount);
  });

  it("is invisible under RLS to a request scoped to a different company", async () => {
    // db.companyA (seeded by createTestDb — "Alpha LLC") is a real, unrelated
    // company; querying the sample company's customers under ITS header must
    // return nothing, never another tenant's rows (CLAUDE.md §4/§11).
    const res = await get(app, "/api/c/customers", db.companyA);
    expect(res.statusCode).toBe(200);
    const customers = res.json() as Array<{ name: string }>;
    expect(customers.find((c) => c.name === "Maple & Co. Bakery")).toBeUndefined();
  });
});
