import { JobLockService } from './job-lock.service';

describe('JobLockService', () => {
  it('claims queued or expired jobs with a lease and increments attempts', async () => {
    const tx = {
      $queryRawUnsafe: jest.fn().mockResolvedValue([{ id: 'job-1' }]),
      backgroundJob: {
        update: jest.fn().mockResolvedValue({ id: 'job-1', status: 'running' }),
      },
    };
    const prisma = {
      $transaction: jest.fn((callback) => callback(tx)),
    };
    const service = new JobLockService(prisma as any);
    const now = new Date('2026-07-08T10:00:00.000Z');

    const result = await service.claimNext({
      workerId: 'worker-a',
      types: ['xiaobao.summary.refresh'],
      now,
      leaseMs: 60_000,
    });

    expect(result).toEqual({ id: 'job-1', status: 'running' });
    expect(tx.$queryRawUnsafe.mock.calls[0][0]).toContain('FOR UPDATE SKIP LOCKED');
    expect(tx.backgroundJob.update).toHaveBeenCalledWith({
      where: { id: 'job-1' },
      data: {
        status: 'running',
        lockedBy: 'worker-a',
        lockedUntil: new Date('2026-07-08T10:01:00.000Z'),
        attempts: { increment: 1 },
        lastError: null,
      },
    });
  });

  it('returns null when no claimable jobs exist', async () => {
    const tx = {
      $queryRawUnsafe: jest.fn().mockResolvedValue([]),
      backgroundJob: { update: jest.fn() },
    };
    const prisma = {
      $transaction: jest.fn((callback) => callback(tx)),
    };
    const service = new JobLockService(prisma as any);

    await expect(service.claimNext({ workerId: 'worker-a' })).resolves.toBeNull();
    expect(tx.backgroundJob.update).not.toHaveBeenCalled();
  });
});
