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;
beforeAll(async () => {
  db = await createTestDb();
  c = await db.appClient(db.companyA);
}, 60_000);
afterAll(async () => { await db.close(); });

const SHA = "a".repeat(64);

describe("import idempotency (CLAUDE.md §8, §11)", () => {
  it("re-importing an identical file is rejected (SHA-256 unique per company)", async () => {
    await c.query(
      `INSERT INTO import_batches (company_id, source, filename, file_sha256, created_by)
       VALUES ($1, 'quickbooks_journal', 'alpha-2019.csv', $2, $3)`,
      [db.companyA, SHA, db.userA]);
    await expect(
      c.query(
        `INSERT INTO import_batches (company_id, source, filename, file_sha256, created_by)
         VALUES ($1, 'quickbooks_journal', 'alpha-2019-renamed.csv', $2, $3)`,
        [db.companyA, SHA, db.userA]),
    ).rejects.toThrow(/duplicate key/);
  });

  it("the same file content in a DIFFERENT company is fine", async () => {
    const cB = await db.appClient(db.companyB);
    await cB.query(
      `INSERT INTO import_batches (company_id, source, filename, file_sha256, created_by)
       VALUES ($1, 'quickbooks_journal', 'beta-2019.csv', $2, $3)`,
      [db.companyB, SHA, db.userB]);
  });

  it("re-importing the same external_ref under a different filename is rejected", async () => {
    // insert a complete, balanced entry carrying source + external_ref
    await c.query("BEGIN");
    const e = await c.query(
      `INSERT INTO journal_entries
         (company_id, entry_date, memo, source, external_ref, created_by)
       VALUES ($1, '2019-05-01', 'from file one', 'quickbooks_journal', 'TX-2002', $2)
       RETURNING id`, [db.companyA, db.userA]);
    await c.query(
      `INSERT INTO journal_lines (company_id, entry_id, account_id, debit, credit)
       VALUES ($1, $2, $3, '10.0000', 0), ($1, $2, $4, 0, '10.0000')`,
      [db.companyA, e.rows[0].id, db.accounts["1000"].A, db.accounts["4000"].A]);
    await c.query("COMMIT");

    // same source+external_ref again, "different file": unique index refuses
    await c.query("BEGIN");
    await expect(
      c.query(
        `INSERT INTO journal_entries
           (company_id, entry_date, memo, source, external_ref, created_by)
         VALUES ($1, '2019-05-01', 'from file two', 'quickbooks_journal', 'TX-2002', $2)`,
        [db.companyA, db.userA]),
    ).rejects.toThrow(/duplicate key/);
    await c.query("ROLLBACK");
  });

  it("an entry cannot carry only one of source/external_ref", async () => {
    await c.query("BEGIN");
    await expect(
      c.query(
        `INSERT INTO journal_entries
           (company_id, entry_date, memo, source, created_by)
         VALUES ($1, '2019-05-01', 'half-tagged', 'quickbooks_journal', $2)`,
        [db.companyA, db.userA]),
    ).rejects.toThrow(/check constraint/);
    await c.query("ROLLBACK");
  });

  it("staging keeps the raw source row as jsonb", async () => {
    const batch = await c.query(
      `SELECT id FROM import_batches WHERE file_sha256 = $1 AND company_id = $2`,
      [SHA, db.companyA]);
    await c.query(
      `INSERT INTO staging_journal
         (company_id, batch_id, entry_key, account_raw, debit_raw, raw)
       VALUES ($1, $2, 'TX-1', 'Checking', '100.00', '{"Trans #": "TX-1"}'::jsonb)`,
      [db.companyA, batch.rows[0].id]);
    const r = await c.query(
      `SELECT raw->>'Trans #' AS t FROM staging_journal WHERE entry_key = 'TX-1'`);
    expect(r.rows[0].t).toBe("TX-1");
  });

  it("staging and batches are tenant-isolated like everything else", async () => {
    const cB = await db.appClient(db.companyB);
    const batches = await cB.query("SELECT company_id FROM import_batches");
    expect(batches.rows.every((r) => r.company_id === db.companyB)).toBe(true);
    const staging = await cB.query("SELECT count(*)::int AS n FROM staging_journal");
    expect(staging.rows[0].n).toBe(0); // company A's staging rows invisible
  });

  it("batch identity columns are immutable for the app role", async () => {
    await expect(
      c.query("UPDATE import_batches SET file_sha256 = repeat('b', 64) WHERE file_sha256 = $1", [SHA]),
    ).rejects.toThrow(/permission denied/);
    await expect(
      c.query("UPDATE staging_journal SET raw = '{}'::jsonb WHERE entry_key = 'TX-1'"),
    ).rejects.toThrow(/permission denied/);
    await expect(
      c.query("UPDATE import_batches SET status = 'validated' WHERE file_sha256 = $1", [SHA]),
    ).rejects.toThrow(/permission denied/);
  });

  it("TRUNCATE is forbidden on import history even for the superuser", async () => {
    await expect(db.admin.query("TRUNCATE staging_journal"))
      .rejects.toThrow(/TRUNCATE is not allowed/);
    await expect(db.admin.query("TRUNCATE import_batches CASCADE"))
      .rejects.toThrow(/TRUNCATE is not allowed/);
  });

  it("a staging row cannot claim posted without its ledger link", async () => {
    const batch = await c.query(
      `SELECT id FROM import_batches WHERE file_sha256 = $1 AND company_id = $2`,
      [SHA, db.companyA]);
    await expect(
      c.query(
        `INSERT INTO staging_journal
           (company_id, batch_id, entry_key, raw, status)
         VALUES ($1, $2, 'TX-9', '{}'::jsonb, 'posted')`,
        [db.companyA, batch.rows[0].id]),
    ).rejects.toThrow(/check constraint/);
  });
});
