/**
 * UserProfile reads keyed by (accountId, userId).
 *
 * These tests pin the read doctrine on the scheduling lib's getUserTimezone:
 *   - Missing userId is a caller bug, surfaced as MissingUserIdError, not as
 *     a silent widening to an account-wide read.
 *   - The cypher predicates BOTH accountId AND userId — the discriminating
 *     anti-regression check that prevents reversion to bare-accountId reads.
 *   - The "no timezone" error message names the exact UserProfile target so
 *     the operator can update the same row that was read.
 *
 * The Neo4j driver is mocked through vi.mock so these tests never need a
 * live database. Tests are colocated under src/lib/__tests__/ to match the
 * codebase convention (memory plugin).
 */
import { describe, it, expect, beforeEach, vi } from "vitest";
const mockRun = vi.fn();
const mockClose = vi.fn().mockResolvedValue(undefined);
vi.mock("neo4j-driver", () => {
    return {
        default: {
            driver: vi.fn(() => ({
                session: vi.fn(() => ({
                    run: mockRun,
                    close: mockClose,
                })),
                close: vi.fn().mockResolvedValue(undefined),
            })),
            auth: { basic: vi.fn() },
        },
    };
});
// Set required env vars before importing the module under test
process.env.NEO4J_URI = "bolt://localhost:7687";
process.env.NEO4J_PASSWORD = "test";
const { getUserTimezone, MissingUserIdError } = await import("../neo4j.js");
function makeRecord(timezone) {
    return { get: (key) => (key === "timezone" ? timezone : null) };
}
describe("getUserTimezone read doctrine", () => {
    beforeEach(() => {
        mockRun.mockReset();
        mockClose.mockClear();
    });
    it("throws MissingUserIdError when userId is empty string", async () => {
        await expect(getUserTimezone("acct-abc12345", "")).rejects.toBeInstanceOf(MissingUserIdError);
        // Read-side guard fires before any session.run, so no query is made.
        expect(mockRun).not.toHaveBeenCalled();
    });
    it("throws MissingUserIdError when userId is null/undefined", async () => {
        // The signature is (accountId, userId: string), but at runtime callers can
        // still pass undefined/null when their own wiring is broken — the lib is
        // the last line of defence.
        await expect(
        // @ts-expect-error — exercising runtime-undefined case
        getUserTimezone("acct-abc12345", undefined)).rejects.toBeInstanceOf(MissingUserIdError);
        await expect(
        // @ts-expect-error — exercising runtime-null case
        getUserTimezone("acct-abc12345", null)).rejects.toBeInstanceOf(MissingUserIdError);
        expect(mockRun).not.toHaveBeenCalled();
    });
    it("returns the timezone for the matched (accountId, userId)", async () => {
        mockRun.mockResolvedValueOnce({ records: [makeRecord("Europe/London")] });
        const tz = await getUserTimezone("acct-abc12345", "user-xyz98765");
        expect(tz).toBe("Europe/London");
    });
    it("query keys on BOTH accountId AND userId — discriminating anti-regression check", async () => {
        mockRun.mockResolvedValueOnce({ records: [makeRecord("America/Toronto")] });
        await getUserTimezone("acct-abc12345", "user-xyz98765");
        expect(mockRun).toHaveBeenCalledTimes(1);
        const [query, params] = mockRun.mock.calls[0];
        expect(query).toMatch(/accountId:\s*\$accountId/);
        expect(query).toMatch(/userId:\s*\$userId/);
        expect(params).toEqual({ accountId: "acct-abc12345", userId: "user-xyz98765" });
    });
    it("throws with the new error-message text when matched UserProfile has timezone=NULL", async () => {
        // userId.slice(0, 8) of "user-xyz98765" = "user-xyz" — pin the slice
        // shape so a future change to slice length surfaces here, not silently.
        mockRun.mockResolvedValueOnce({ records: [makeRecord(null)] });
        await expect(getUserTimezone("acct-abc12345", "user-xyz98765")).rejects.toThrow(/No timezone on UserProfile for userId user-xyz/);
        mockRun.mockResolvedValueOnce({ records: [makeRecord(null)] });
        await expect(getUserTimezone("acct-abc12345", "user-xyz98765")).rejects.toThrow(/set via profile-update with profileFields: \{ timezone: 'IANA_TZ' \}/);
    });
    it("throws when no UserProfile matches the (accountId, userId) pair (post-Task-792 0-or-1-row state)", async () => {
        mockRun.mockResolvedValueOnce({ records: [] });
        // slice(0, 8) of "user-nope0000" = "user-nop"
        await expect(getUserTimezone("acct-abc12345", "user-nope0000")).rejects.toThrow(/No UserProfile for userId user-nop/);
    });
    it("never returns a timezone from a non-matching UserProfile when multiple UserProfiles exist for one accountId", async () => {
        // Simulate the ghost-rows world from before the constraint: 3
        // UserProfiles share an accountId but only the targeted userId has the
        // timezone field set. The cypher predicate must include userId so Neo4j
        // returns ONLY the targeted row — the mock asserts this by inspecting
        // the actual query parameters and returning the appropriate result set.
        const multiRowUniverse = {
            "user-target-001": "Europe/Berlin",
            "user-ghost-aaa": null,
            "user-ghost-bbb": null,
        };
        mockRun.mockImplementation(async (_query, params) => {
            // The lib's query filters on userId, so a correctly-implemented call
            // returns at most one row. If a future regression drops the userId
            // predicate, this mock still returns only the matching row — but the
            // anti-regression check above (query string assertion) would have
            // already caught it.
            const tz = multiRowUniverse[params.userId];
            if (tz === undefined)
                return { records: [] };
            return { records: [makeRecord(tz)] };
        });
        // Targeted user: returns the targeted timezone.
        expect(await getUserTimezone("acct-shared", "user-target-001")).toBe("Europe/Berlin");
        // Ghost user with NULL timezone: throws the "No timezone" branch — does
        // NOT silently leak the targeted user's timezone.
        // slice(0, 8) of "user-ghost-aaa" = "user-gho"
        await expect(getUserTimezone("acct-shared", "user-ghost-aaa")).rejects.toThrow(/No timezone on UserProfile for userId user-gho/);
    });
});
//# sourceMappingURL=getUserTimezone.test.js.map