import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanupExpiredInvitations } from '../cleanup-expired-invitations.js';
describe('cleanupExpiredInvitations', () => {
    let mockQuery;
    let mockRelease;
    let mockConnect;
    let mockDeleteUser;
    let db;
    let auth0;
    let logger;
    beforeEach(() => {
        mockQuery = vi.fn();
        mockRelease = vi.fn();
        mockConnect = vi.fn().mockResolvedValue({
            query: mockQuery,
            release: mockRelease,
        });
        db = {
            pool: {
                connect: mockConnect,
            },
            query: mockQuery,
        };
        mockDeleteUser = vi.fn().mockResolvedValue(undefined);
        auth0 = {
            deleteUser: mockDeleteUser,
        };
        logger = {
            info: vi.fn(),
            error: vi.fn(),
        };
    });
    it('deletes expired invitations, orphaned business users, and writes audit logs', async () => {
        mockQuery
            .mockResolvedValueOnce({
            rows: [
                {
                    id: 'inv-1',
                    auth0_user_id: 'auth0|123',
                    auth0_user_created: true,
                    user_id: 'user-1',
                },
            ],
        })
            .mockResolvedValue({ rows: [], rowCount: 1 });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 1, errors: 0 });
        expect(auth0.deleteUser).toHaveBeenCalledWith('auth0|123');
        const queries = mockQuery.mock.calls.map(([query]) => query);
        expect(queries.some(query => query.includes('DELETE FROM accounter_schema.business_users'))).toBe(true);
        expect(queries.some(query => query.includes('DELETE FROM accounter_schema.invitations'))).toBe(true);
        expect(queries.some(query => query.includes('INSERT INTO accounter_schema.audit_logs'))).toBe(true);
        expect(mockQuery).toHaveBeenCalledWith(expect.stringContaining('DELETE FROM accounter_schema.business_users'), ['user-1']);
        expect(mockRelease).toHaveBeenCalledTimes(1);
    });
    it('does not attempt to delete Auth0 user when invitation has no Auth0 account', async () => {
        mockQuery
            .mockResolvedValueOnce({
            rows: [
                {
                    id: 'inv-2',
                    auth0_user_id: null,
                    auth0_user_created: false,
                    user_id: 'user-2',
                },
            ],
        })
            .mockResolvedValue({ rows: [], rowCount: 1 });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 1, errors: 0 });
        expect(auth0.deleteUser).not.toHaveBeenCalled();
    });
    it('does not delete accepted invitations because selector excludes accepted rows', async () => {
        mockQuery.mockResolvedValueOnce({ rows: [] });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 0, errors: 0 });
        expect(auth0.deleteUser).not.toHaveBeenCalled();
        const [selectorQuery] = mockQuery.mock.calls[0];
        expect(selectorQuery).toContain('WHERE accepted_at IS NULL');
    });
    it('does not delete non-expired invitations because selector only fetches expired rows', async () => {
        mockQuery.mockResolvedValueOnce({ rows: [] });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 0, errors: 0 });
        expect(auth0.deleteUser).not.toHaveBeenCalled();
        const [selectorQuery] = mockQuery.mock.calls[0];
        expect(selectorQuery).toContain('AND expires_at < NOW()');
    });
    it('logs Auth0/API failures and continues processing remaining invitations', async () => {
        mockDeleteUser.mockRejectedValueOnce(new Error('Auth0 rate limit')).mockResolvedValueOnce(undefined);
        mockQuery
            .mockResolvedValueOnce({
            rows: [
                {
                    id: 'inv-fail',
                    auth0_user_id: 'auth0|fail',
                    auth0_user_created: true,
                    user_id: 'user-fail',
                },
                {
                    id: 'inv-ok',
                    auth0_user_id: 'auth0|ok',
                    auth0_user_created: true,
                    user_id: 'user-ok',
                },
            ],
        })
            .mockResolvedValue({ rows: [], rowCount: 1 });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 1, errors: 1 });
        expect(logger.error).toHaveBeenCalledTimes(1);
        expect(logger.error).toHaveBeenCalledWith('Failed to cleanup invitation', expect.objectContaining({ invitationId: 'inv-fail' }));
        expect(mockQuery).toHaveBeenCalledWith('DELETE FROM accounter_schema.invitations WHERE id = $1', ['inv-ok']);
    });
    it('creates one audit log entry per cleaned invitation', async () => {
        mockQuery
            .mockResolvedValueOnce({
            rows: [
                {
                    id: 'inv-1',
                    auth0_user_id: 'auth0|1',
                    auth0_user_created: true,
                    user_id: 'user-1',
                },
                {
                    id: 'inv-2',
                    auth0_user_id: 'auth0|2',
                    auth0_user_created: true,
                    user_id: 'user-2',
                },
            ],
        })
            .mockResolvedValue({ rows: [], rowCount: 1 });
        const result = await cleanupExpiredInvitations(db, auth0, logger);
        expect(result).toEqual({ deleted: 2, errors: 0 });
        const auditInsertCalls = mockQuery.mock.calls.filter(([query]) => query.includes('INSERT INTO accounter_schema.audit_logs'));
        expect(auditInsertCalls).toHaveLength(2);
    });
});
//# sourceMappingURL=cleanup-expired-invitations.test.js.map