import { describe, it, expect, vi, beforeEach } from "vitest";
import { getExpensesByCategory, deriveNetWorthTile } from "./dashboardService";
import type { SnapshotTotalPoint } from "./balance.service";

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

import { getDb } from "./db";

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

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

describe("getExpensesByCategory — accountIds filter (Issue #273)", () => {
  it("without accountIds, the query carries no source_id clause (regression)", async () => {
    mockSelect.mockResolvedValueOnce([]);

    await getExpensesByCategory("2025-01-01", "2025-12-31");

    const sql = mockSelect.mock.calls[0][0] as string;
    const params = mockSelect.mock.calls[0][1] as unknown[];
    expect(sql).not.toContain("source_id");
    expect(params).toEqual(["2025-01-01", "2025-12-31"]);
  });

  it("an empty accountIds array behaves exactly like no filter (regression)", async () => {
    mockSelect.mockResolvedValueOnce([]);

    await getExpensesByCategory("2025-01-01", "2025-12-31", []);

    const sql = mockSelect.mock.calls[0][0] as string;
    const params = mockSelect.mock.calls[0][1] as unknown[];
    expect(sql).not.toContain("source_id");
    expect(params).toEqual(["2025-01-01", "2025-12-31"]);
  });

  it("applies a parameterized accountIds IN clause, one placeholder per id", async () => {
    mockSelect.mockResolvedValueOnce([]);

    await getExpensesByCategory("2025-01-01", "2025-12-31", [6, 11]);

    const sql = mockSelect.mock.calls[0][0] as string;
    const params = mockSelect.mock.calls[0][1] as unknown[];
    expect(sql).toContain("t.source_id IN ($3, $4)");
    expect(sql).not.toContain("6,11");
    expect(params).toEqual(["2025-01-01", "2025-12-31", 6, 11]);
  });

  it("applies the accountIds filter alone (no dates) alongside the fixed expense-type clause", async () => {
    mockSelect.mockResolvedValueOnce([]);

    await getExpensesByCategory(undefined, undefined, [2]);

    const sql = mockSelect.mock.calls[0][0] as string;
    const params = mockSelect.mock.calls[0][1] as unknown[];
    expect(sql).toContain("t.source_id IN ($1)");
    expect(sql).toContain("COALESCE(c.type, 'expense') = 'expense'");
    expect(params).toEqual([2]);
  });
});

describe("deriveNetWorthTile (Issue #279)", () => {
  const totals: SnapshotTotalPoint[] = [
    { snapshot_date: "2026-01-31", total: 1000 },
    { snapshot_date: "2026-02-28", total: 1234.56 },
  ];

  it("is hidden when there is no balance account at all", () => {
    expect(deriveNetWorthTile(0, false, totals)).toEqual({
      visible: false,
      total: null,
      asOfDate: null,
    });
  });

  it("is hidden when accounts exist but no snapshot has ever been recorded (never a misleading $0)", () => {
    expect(deriveNetWorthTile(3, false, [])).toEqual({
      visible: false,
      total: null,
      asOfDate: null,
    });
  });

  it("is hidden when accounts exist but no snapshot has ever been recorded, even if totals is non-empty (defensive)", () => {
    expect(deriveNetWorthTile(3, false, totals)).toEqual({
      visible: false,
      total: null,
      asOfDate: null,
    });
  });

  it("is visible with the latest (last) point once accounts and a snapshot both exist", () => {
    expect(deriveNetWorthTile(3, true, totals)).toEqual({
      visible: true,
      total: 1234.56,
      asOfDate: "2026-02-28",
    });
  });

  it("stays hidden if the two signals disagree with an empty totals series (defensive guard)", () => {
    expect(deriveNetWorthTile(3, true, [])).toEqual({
      visible: false,
      total: null,
      asOfDate: null,
    });
  });
});
