import { JobsService } from './jobs.service';

describe('JobsService', () => {
  function makeService() {
    const prisma = {
      backgroundJob: {
        findFirst: jest.fn(),
        findUnique: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
      },
    };
    return { prisma, service: new JobsService(prisma as any) };
  }

  it('dedupes active jobs by dedupeKey before creating a new row', async () => {
    const { prisma, service } = makeService();
    prisma.backgroundJob.findFirst.mockResolvedValue({ id: 'job-1', status: 'queued' });

    const result = await service.enqueue({
      type: 'xiaobao.summary.refresh',
      dedupeKey: 'version-1',
      payload: { versionId: 'version-1' },
    });

    expect(result).toEqual({ id: 'job-1', status: 'queued' });
    expect(prisma.backgroundJob.findFirst).toHaveBeenCalledWith({
      where: {
        type: 'xiaobao.summary.refresh',
        dedupeKey: 'version-1',
        status: { in: ['queued', 'running'] },
      },
      orderBy: { createdAt: 'asc' },
    });
    expect(prisma.backgroundJob.create).not.toHaveBeenCalled();
  });

  it('creates a queued job when no active dedupe match exists', async () => {
    const { prisma, service } = makeService();
    prisma.backgroundJob.findFirst.mockResolvedValue(null);
    prisma.backgroundJob.create.mockResolvedValue({ id: 'job-2', status: 'queued' });

    await service.enqueue({
      type: 'xiaobao.summary.refresh',
      dedupeKey: 'version-2',
      payload: { versionId: 'version-2' },
      maxAttempts: 5,
    });

    expect(prisma.backgroundJob.create).toHaveBeenCalledWith({
      data: {
        type: 'xiaobao.summary.refresh',
        dedupeKey: 'version-2',
        payload: { versionId: 'version-2' },
        maxAttempts: 5,
        availableAt: undefined,
      },
    });
  });

  it('recovers from concurrent dedupe unique conflicts by returning the active job', async () => {
    const { prisma, service } = makeService();
    prisma.backgroundJob.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce({ id: 'job-existing', status: 'queued' });
    prisma.backgroundJob.create.mockRejectedValue(Object.assign(new Error('duplicate'), { code: 'P2002' }));

    const result = await service.enqueue({
      type: 'xiaobao.summary.refresh',
      dedupeKey: 'version-2',
      payload: { versionId: 'version-2' },
    });

    expect(result).toEqual({ id: 'job-existing', status: 'queued' });
    expect(prisma.backgroundJob.findFirst).toHaveBeenCalledTimes(2);
  });

  it('requeues failed jobs while attempts remain', async () => {
    const { prisma, service } = makeService();
    const now = new Date('2026-07-08T10:00:00.000Z');
    prisma.backgroundJob.findUnique.mockResolvedValue({ id: 'job-1', attempts: 1, maxAttempts: 3 });

    await service.markFailed('job-1', new Error('boom'), { now, retryDelayMs: 30_000 });

    expect(prisma.backgroundJob.update).toHaveBeenCalledWith({
      where: { id: 'job-1' },
      data: {
        status: 'queued',
        availableAt: new Date('2026-07-08T10:00:30.000Z'),
        lockedBy: null,
        lockedUntil: null,
        lastError: 'boom',
      },
    });
  });

  it('marks failed jobs terminal when max attempts is reached', async () => {
    const { prisma, service } = makeService();
    const now = new Date('2026-07-08T10:00:00.000Z');
    prisma.backgroundJob.findUnique.mockResolvedValue({ id: 'job-1', attempts: 3, maxAttempts: 3 });

    await service.markFailed('job-1', 'still broken', { now, retryDelayMs: 30_000 });

    expect(prisma.backgroundJob.update).toHaveBeenCalledWith({
      where: { id: 'job-1' },
      data: {
        status: 'failed',
        availableAt: now,
        lockedBy: null,
        lockedUntil: null,
        lastError: 'still broken',
      },
    });
  });
});
