import { describe, it, expect, vi, beforeEach } from "vitest";
import { getBudgetVsActualData, getActualTotalsForYear } from "./budgetService";

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();
});

// getBudgetVsActualData fans out to 4 parallel selects (getAllActiveCategories,
// getBudgetEntriesForYear, and getActualsByCategoryRange for the month + YTD
// windows). Only the two `getActualsByCategoryRange` calls query `transactions`
// and are eligible for the accountIds filter — budgets are not tied to an
// account, so the categories/entries queries must never carry the clause.
describe("getBudgetVsActualData — accountIds filter (Issue #273)", () => {
  it("without accountIds, the actuals queries carry no source_id clause (regression)", async () => {
    mockSelect.mockImplementation(() => Promise.resolve([]));

    await getBudgetVsActualData(2026, 3);

    const actualsCalls = mockSelect.mock.calls.filter(([sql]) =>
      (sql as string).includes("FROM transactions"),
    );
    expect(actualsCalls.length).toBe(2); // month + YTD
    for (const [sql, params] of actualsCalls) {
      expect(sql as string).not.toContain("source_id");
      expect(params as unknown[]).toHaveLength(2); // dateFrom, dateTo only
    }
  });

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

    await getBudgetVsActualData(2026, 3, []);

    const actualsCalls = mockSelect.mock.calls.filter(([sql]) =>
      (sql as string).includes("FROM transactions"),
    );
    for (const [sql, params] of actualsCalls) {
      expect(sql as string).not.toContain("source_id");
      expect(params as unknown[]).toHaveLength(2);
    }
  });

  it("applies a parameterized accountIds IN clause to both the month and YTD actuals queries", async () => {
    mockSelect.mockImplementation(() => Promise.resolve([]));

    await getBudgetVsActualData(2026, 3, [4, 9]);

    const actualsCalls = mockSelect.mock.calls.filter(([sql]) =>
      (sql as string).includes("FROM transactions"),
    );
    expect(actualsCalls.length).toBe(2);
    for (const [sql, params] of actualsCalls) {
      expect(sql as string).toContain("source_id IN ($3, $4)");
      expect(sql as string).not.toContain("4,9");
      expect(params as unknown[]).toEqual(expect.arrayContaining([4, 9]));
    }
  });

  it("never adds the filter to the categories/budget-entries queries (a budget is not tied to an account)", async () => {
    mockSelect.mockImplementation(() => Promise.resolve([]));

    await getBudgetVsActualData(2026, 3, [4]);

    const categoriesCall = mockSelect.mock.calls.find(([sql]) =>
      (sql as string).includes("FROM categories"),
    )!;
    const budgetEntriesCall = mockSelect.mock.calls.find(([sql]) =>
      (sql as string).includes("FROM budget_entries"),
    )!;
    expect(categoriesCall[0]).not.toContain("source_id");
    expect(budgetEntriesCall[0]).not.toContain("source_id");
  });
});

// getActualTotalsForYear is the previous-year actuals reference column
// consumed by useBudget (Issue #276) — a thin wrapper over the same
// getActualsByCategoryRange helper, so it gets the same accountIds pass-through.
describe("getActualTotalsForYear — accountIds filter (Issue #276)", () => {
  it("without accountIds, the query carries no source_id clause (regression)", async () => {
    mockSelect.mockImplementation(() => Promise.resolve([]));

    await getActualTotalsForYear(2025);

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

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

    await getActualTotalsForYear(2025, []);

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

  it("applies a parameterized accountIds IN clause", async () => {
    mockSelect.mockImplementation(() => Promise.resolve([]));

    await getActualTotalsForYear(2025, [4, 9]);

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