// StarterAccountsModal — unit tests (issue #179)
//
// NOTE: This project does not have @testing-library/react or jsdom configured
// (matches the BalanceOnboardingCard.test.tsx pattern from #178). Tests cover
// the service-layer helpers (`getStarterCollisions`, `proposeStarterAccounts`)
// and the `STARTER_ACCOUNTS` constant — the modal itself is pure orchestration
// over those helpers.

import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("../../services/db", () => ({
  getDb: vi.fn(),
}));

import { getDb } from "../../services/db";
import {
  STARTER_ACCOUNTS,
  getStarterCollisions,
  proposeStarterAccounts,
} from "../../services/balance.service";

const mockSelect = vi.fn();
const mockExecute = vi.fn();
const mockDb = { select: mockSelect, execute: mockExecute };

beforeEach(() => {
  vi.mocked(getDb).mockResolvedValue(mockDb as never);
  mockSelect.mockReset();
  mockExecute.mockReset();
});

describe("STARTER_ACCOUNTS", () => {
  it("ships exactly 4 starters mapping cash/tfsa/rrsp/other", () => {
    expect(STARTER_ACCOUNTS).toHaveLength(4);
    expect(STARTER_ACCOUNTS.map((s) => s.key)).toEqual([
      "cash",
      "tfsa",
      "rrsp",
      "other",
    ]);
    for (const s of STARTER_ACCOUNTS) {
      expect(s.categoryKey).toBe(s.key);
      expect(s.i18nKey).toMatch(/^balance\.starters\.items\./);
    }
  });
});

describe("getStarterCollisions", () => {
  it("returns empty set when no accounts collide", async () => {
    mockSelect.mockResolvedValueOnce([]);
    const result = await getStarterCollisions();
    expect(result.size).toBe(0);
  });

  it("flags exact-name collisions case-insensitive trim", async () => {
    mockSelect.mockResolvedValueOnce([
      { key: "cash", account_name: "  compte chèque " },
      { key: "tfsa", account_name: "Mon CELI 2024" }, // does NOT match "CELI" exactly
    ]);
    const result = await getStarterCollisions();
    expect(result.has("cash")).toBe(true);
    expect(result.has("tfsa")).toBe(false);
    expect(result.has("rrsp")).toBe(false);
    expect(result.has("other")).toBe(false);
  });

  it("requires the account to live in the matching category", async () => {
    // CELI-named account but in 'cash' category → not a collision for tfsa starter
    mockSelect.mockResolvedValueOnce([
      { key: "cash", account_name: "CELI" },
    ]);
    const result = await getStarterCollisions();
    expect(result.has("tfsa")).toBe(false);
    expect(result.has("cash")).toBe(false); // name "CELI" != "Compte chèque"
  });
});

describe("proposeStarterAccounts", () => {
  it("returns [] when no keys selected without opening a transaction", async () => {
    const result = await proposeStarterAccounts([]);
    expect(result).toEqual([]);
    expect(mockExecute).not.toHaveBeenCalled();
  });

  it("inserts selected starters atomically and returns their ids", async () => {
    // BEGIN
    mockExecute.mockResolvedValueOnce({ rowsAffected: 0, lastInsertId: 0 });
    // For each starter: SELECT id FROM balance_categories + INSERT
    mockSelect
      .mockResolvedValueOnce([{ id: 11 }]) // cash category
      .mockResolvedValueOnce([{ id: 13 }]); // rrsp category
    mockExecute
      .mockResolvedValueOnce({ rowsAffected: 1, lastInsertId: 100 }) // INSERT cash
      .mockResolvedValueOnce({ rowsAffected: 1, lastInsertId: 101 }) // INSERT rrsp
      .mockResolvedValueOnce({ rowsAffected: 0, lastInsertId: 0 }); // COMMIT

    const result = await proposeStarterAccounts(["cash", "rrsp"]);
    expect(result).toEqual([100, 101]);

    const sqls = mockExecute.mock.calls.map((c) => c[0]);
    expect(sqls[0]).toBe("BEGIN");
    expect(sqls[sqls.length - 1]).toBe("COMMIT");
    expect(sqls.filter((s) => /INSERT INTO balance_accounts/.test(s))).toHaveLength(2);
  });

  it("rolls back on insert failure", async () => {
    mockExecute.mockResolvedValueOnce({ rowsAffected: 0, lastInsertId: 0 }); // BEGIN
    mockSelect.mockResolvedValueOnce([{ id: 11 }]);
    mockExecute.mockRejectedValueOnce(new Error("disk full"));
    mockExecute.mockResolvedValueOnce({ rowsAffected: 0, lastInsertId: 0 }); // ROLLBACK

    await expect(proposeStarterAccounts(["cash"])).rejects.toThrow();

    const sqls = mockExecute.mock.calls.map((c) => c[0]);
    expect(sqls).toContain("BEGIN");
    expect(sqls).toContain("ROLLBACK");
    expect(sqls).not.toContain("COMMIT");
  });
});
