import { JobReaperService } from './job-reaper.service';

describe('JobReaperService', () => {
  let service: JobReaperService;
  let mockUpdateMany: jest.Mock;
  let mockFindMany: jest.Mock;

  beforeEach(() => {
    mockUpdateMany = jest.fn().mockResolvedValue({ count: 0 });
    mockFindMany = jest.fn().mockResolvedValue([]); // default: empty batch → breaks loop
    const mockPrisma = {
      aiRuntimeJob: { updateMany: mockUpdateMany, findMany: mockFindMany },
    } as any;
    service = new JobReaperService(mockPrisma);
    jest.spyOn(global, 'setInterval').mockReturnValue({} as any);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('expires locked jobs past lockUntil', async () => {
    mockUpdateMany.mockResolvedValueOnce({ count: 2 }); // locked step

    const result = await service.reap();

    expect(mockUpdateMany).toHaveBeenCalledWith(expect.objectContaining({
      where: { status: 'locked', lockUntil: { lt: expect.any(Date) } },
      data: { status: 'expired' },
    }));
    expect(result.expired).toBe(2);
  });

  it('expires running jobs past timeout', async () => {
    const now = Date.now();
    // Running batch 1: has a stuck job
    mockFindMany.mockResolvedValueOnce([
      { id: 'j1', startedAt: new Date(now - 180_000), timeoutSeconds: 120 },
    ]);
    // Running batch 2: empty → loop breaks (default mock handles this)
    mockUpdateMany.mockResolvedValueOnce({ count: 0 }); // locked step
    mockUpdateMany.mockResolvedValueOnce({ count: 1 }); // expire running

    const result = await service.reap();

    expect(mockUpdateMany).toHaveBeenCalledWith(expect.objectContaining({
      where: { id: { in: ['j1'] }, status: 'running' },
      data: { status: 'expired' },
    }));
    expect(result.expired).toBe(1);
  });

  it('retries expired jobs with remaining retries', async () => {
    // Running loop: empty on first call → breaks
    // Expired batch 1: has 2 expired jobs
    mockFindMany
      .mockResolvedValueOnce([]) // running batch 1 (empty → skip)
      .mockResolvedValueOnce([
        { id: 'j1', retryCount: 0, maxRetryCount: 3 },
        { id: 'j2', retryCount: 3, maxRetryCount: 3 },
      ]);
    mockUpdateMany
      .mockResolvedValueOnce({ count: 0 }) // locked
      .mockResolvedValueOnce({ count: 1 }) // retry j1
      .mockResolvedValueOnce({ count: 1 }); // fail j2

    const result = await service.reap();

    expect(result.retried).toBe(1);
    expect(result.failed).toBe(1);
  });

  it('handles no stuck jobs gracefully', async () => {
    const result = await service.reap();
    expect(result).toEqual({ expired: 0, retried: 0, failed: 0 });
  });

  it('processes running jobs correctly with single batch', async () => {
    mockFindMany.mockReset().mockResolvedValue([]);
    mockUpdateMany.mockReset().mockResolvedValue({ count: 0 });
    const now = Date.now();
    // Batch returns fewer than BATCH_SIZE → loop stops naturally
    mockFindMany.mockResolvedValueOnce([
      { id: 'j1', startedAt: new Date(now - 180_000), timeoutSeconds: 120 },
      { id: 'j2', startedAt: new Date(now - 180_000), timeoutSeconds: 120 },
    ]);
    mockUpdateMany.mockResolvedValueOnce({ count: 0 }); // locked step
    mockUpdateMany.mockResolvedValueOnce({ count: 2 }); // expire running

    const result = await service.reap();
    expect(result.expired).toBe(2);
  });
});
