import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
import type { Pool } from "pg";
import {
  createPool,
  mockDbWithPool,
  resetDB,
  seedParticipants,
  insertTransaction,
} from "./helpers";

/**
 * Every split adds up to 100%.
 *
 * The failure this guards against is not an arithmetic one — `myShare` has
 * always treated the payer's share as the remainder, so the numbers were right.
 * It is that the remainder was never written down, so a 50/50 arrangement was
 * stored as a single row reading "Sonu 50%" and displayed as half a split.
 */
describe("completeSplit", () => {
  let pool: Pool;
  let completeSplit: (id: number) => Promise<void>;

  beforeAll(async () => {
    pool = createPool();
    mockDbWithPool(pool);
    ({ completeSplit } = await import("@/lib/splits"));
  });

  afterAll(async () => {
    await pool.end();
  });

  beforeEach(async () => {
    await resetDB(pool);
  });

  const sharesOf = async (txId: number) => {
    const r = await pool.query(
      `SELECT participant_id, share_percent::float FROM transaction_splits
        WHERE transaction_id = $1 ORDER BY participant_id`,
      [txId]
    );
    return r.rows as { participant_id: number; share_percent: number }[];
  };

  it("writes the payer's half of a 50/50 recorded as one row", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 50)`,
      [txId, otherId]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: ownerId, share_percent: 50 },
      { participant_id: otherId, share_percent: 50 },
    ]);
  });

  it("leaves an unsplit transaction unsplit", async () => {
    // A transaction nobody shares is not a 100% split of itself. Writing one
    // would put every row in the Shared view.
    const { ownerId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([]);
  });

  it("adds nothing when the other party owes all of it", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 100)`,
      [txId, otherId]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: otherId, share_percent: 100 },
    ]);
  });

  it("removes the owner's row when the others grow to cover the whole amount", async () => {
    // A 50/50 revised to "they owe all of it". The owner's share becomes zero,
    // and a 0% row cannot be stored anyway — `share_percent > 0` is a CHECK
    // constraint — so the row has to go rather than be zeroed.
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 100), ($1, $3, 50)`,
      [txId, otherId, ownerId]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: otherId, share_percent: 100 },
    ]);
  });

  it("fills the remainder for a three-way split, not a half", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const third = (
      await pool.query(`INSERT INTO participants (name) VALUES ('Carol') RETURNING id`)
    ).rows[0].id as number;
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 50), ($1, $3, 25)`,
      [txId, otherId, third]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: ownerId, share_percent: 25 },
      { participant_id: otherId, share_percent: 50 },
      { participant_id: third, share_percent: 25 },
    ]);
  });

  it("leaves an over-allocated split alone instead of trimming someone's share", async () => {
    // >100% is a caller's mistake. Silently deleting a share to force the total
    // down would destroy the evidence of it.
    // No single row may exceed 100 (CHECK constraint), but two can add up past
    // it — 60 + 60 is how an over-allocated split actually arrives.
    const { ownerId, otherId } = await seedParticipants(pool);
    const third = (
      await pool.query(`INSERT INTO participants (name) VALUES ('Dave') RETURNING id`)
    ).rows[0].id as number;
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 60), ($1, $3, 60)`,
      [txId, otherId, third]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: otherId, share_percent: 60 },
      { participant_id: third, share_percent: 60 },
    ]);
  });

  it("gives the remainder to the statement's owner when the row has none", async () => {
    // Statement rows carry no owner_id of their own; it comes from the
    // statement. Transaction 3828 was one of these.
    const { ownerId, otherId } = await seedParticipants(pool);
    const stmt = await pool.query(
      `INSERT INTO statements (owner_id, filename, bank_name, account_number, billing_start_date, billing_end_date)
       VALUES ($1, 'test.pdf', 'Test Bank', '0001', '2026-06-01', '2026-06-30') RETURNING id`,
      [ownerId]
    );
    const tx = await pool.query(
      `INSERT INTO transactions (owner_id, statement_id, transaction_date, description, amount, transaction_type, row_index)
       VALUES (NULL, $1, '2026-06-15', 'Statement row', 29.17, 'debit', 0) RETURNING id`,
      [stmt.rows[0].id]
    );
    const txId = tx.rows[0].id as number;
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 50)`,
      [txId, otherId]
    );

    await completeSplit(txId);

    expect(await sharesOf(txId)).toEqual([
      { participant_id: ownerId, share_percent: 50 },
      { participant_id: otherId, share_percent: 50 },
    ]);
  });

  it("never puts my share on someone else's transaction", async () => {
    // The remainder goes to the transaction's owner, never to "me". A row for
    // me on a transaction I do not own is a debt I owe, and this helper must
    // not invent one.
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, otherId); // they paid
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 50)`,
      [txId, ownerId]
    );

    await completeSplit(txId);

    const shares = await sharesOf(txId);
    expect(shares).toEqual([
      { participant_id: ownerId, share_percent: 50 },
      { participant_id: otherId, share_percent: 50 },
    ]);
    // My share is unchanged — the new row belongs to the payer.
    expect(shares.find((s) => s.participant_id === ownerId)?.share_percent).toBe(50);
  });

  it("does not disturb a settled split", async () => {
    // Adding the payer's row must not touch anyone else's `settled` flag —
    // that is how $37k of discharged debt gets resurrected.
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent, settled)
       VALUES ($1, $2, 50, true)`,
      [txId, otherId]
    );

    await completeSplit(txId);

    const r = await pool.query(
      `SELECT settled FROM transaction_splits
        WHERE transaction_id = $1 AND participant_id = $2`,
      [txId, otherId]
    );
    expect(r.rows[0].settled).toBe(true);
  });

  it("is idempotent", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);
    await pool.query(
      `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
       VALUES ($1, $2, 50)`,
      [txId, otherId]
    );

    await completeSplit(txId);
    const once = await sharesOf(txId);
    await completeSplit(txId);
    expect(await sharesOf(txId)).toEqual(once);
  });
});

/**
 * The rule path, which is where ten of the live rules write a single share.
 */
describe("applyRuleActions completes the split", () => {
  let pool: Pool;
  let applyRuleActions: (
    id: number,
    actions: { apply_split?: { participant_id: number; share_percent: number }[] }
  ) => Promise<void>;

  beforeAll(async () => {
    pool = createPool();
    mockDbWithPool(pool);
    ({ applyRuleActions } = await import("@/lib/rule-actions"));
  });

  afterAll(async () => {
    await pool.end();
  });

  beforeEach(async () => {
    await resetDB(pool);
  });

  it("writes the payer's half for a rule that names only the other person", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId, { description: "Woolworths" });

    await applyRuleActions(txId, {
      apply_split: [{ participant_id: otherId, share_percent: 50 }],
    });

    const r = await pool.query(
      `SELECT participant_id, share_percent::float FROM transaction_splits
        WHERE transaction_id = $1 ORDER BY participant_id`,
      [txId]
    );
    expect(r.rows).toEqual([
      { participant_id: ownerId, share_percent: 50 },
      { participant_id: otherId, share_percent: 50 },
    ]);
  });

  it("leaves a rule that already names both alone", async () => {
    const { ownerId, otherId } = await seedParticipants(pool);
    const txId = await insertTransaction(pool, ownerId);

    await applyRuleActions(txId, {
      apply_split: [
        { participant_id: ownerId, share_percent: 50 },
        { participant_id: otherId, share_percent: 50 },
      ],
    });

    const r = await pool.query(
      `SELECT sum(share_percent)::float AS total, count(*)::int AS n
         FROM transaction_splits WHERE transaction_id = $1`,
      [txId]
    );
    expect(r.rows[0]).toEqual({ total: 100, n: 2 });
  });
});
