import { SyncIncrementalSchedulerService } from '../src/queues/sync-incremental.scheduler';

/**
 * 僵尸同步锁回收(reapStaleRunningLocks)回归。
 *
 * 背景:sync_logs 有 partial UNIQUE(host_id) WHERE status='running' 作并发锁。
 * 部署/崩溃重启时,running 行来不及标终态,永久占住锁 → 之后每次 cron 增量被 skip →
 * 游标不动 → DW 滞后告警(2026-07-23 测试服实测 33.7h)。
 *
 * 修复:service 启动时把 startedAt 早于本进程启动的 running 行标 failed。
 * 判据安全性:sync 只在本进程 cron 或一次性 CLI 里跑,都不可能比本进程更早启动还活着;
 * 本进程启动后新建的 running(可能是并存的手动 CLI 真锁)绝不碰。
 */

function makeService(rows: Array<{ id: string; hostId: string; startedAt: Date; triggeredBy: string }>) {
  const findMany = jest.fn().mockImplementation(async ({ where }) => {
    const lt: Date = where.startedAt.lt;
    return rows.filter((r) => r.startedAt < lt);
  });
  const updateMany = jest.fn().mockImplementation(async ({ where }) => {
    const lt: Date = where.startedAt.lt;
    return { count: rows.filter((r) => r.startedAt < lt).length };
  });
  const prisma = { syncLog: { findMany, updateMany } };
  const svc = new SyncIncrementalSchedulerService(
    prisma as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
    {} as never,
  );
  return { svc, findMany, updateMany };
}

// 触达 private 方法(纯编排,无需走 onModuleInit 以免顺带注册 cron)
const reap = (svc: SyncIncrementalSchedulerService) =>
  (svc as unknown as { reapStaleRunningLocks: () => Promise<void> }).reapStaleRunningLocks();

describe('reapStaleRunningLocks', () => {
  test('回收进程启动前的 running 行 → 标 failed', async () => {
    const old = new Date(Date.now() - 3_600_000); // 1h 前
    const { svc, updateMany } = makeService([
      { id: 'zombie', hostId: 'h1', startedAt: old, triggeredBy: 'sync:jvs-dw:x' },
    ]);
    await reap(svc);
    expect(updateMany).toHaveBeenCalledTimes(1);
    const arg = updateMany.mock.calls[0][0];
    expect(arg.where.status).toBe('running');
    expect(arg.data.status).toBe('failed');
    expect(arg.data.endedAt).toBeInstanceOf(Date);
    expect(typeof arg.data.errorMessage).toBe('string');
  });

  test('⭐ 只回收 startedAt < 进程启动时刻的行 —— where 用 lt,不用 gte', async () => {
    // 判据锁:where 必须是 { startedAt: { lt: <某时刻> } },否则会误杀本进程启动后的真锁
    const { svc, findMany, updateMany } = makeService([
      { id: 'zombie', hostId: 'h1', startedAt: new Date(Date.now() - 1000), triggeredBy: 't' },
    ]);
    await reap(svc);
    expect(findMany.mock.calls[0][0].where.startedAt).toHaveProperty('lt');
    expect(findMany.mock.calls[0][0].where.startedAt).not.toHaveProperty('gte');
    expect(updateMany.mock.calls[0][0].where.startedAt).toHaveProperty('lt');
  });

  test('无僵尸锁 → 不发 updateMany(纯净启动零副作用)', async () => {
    const { svc, updateMany } = makeService([]);
    await reap(svc);
    expect(updateMany).not.toHaveBeenCalled();
  });

  test('回收失败不抛(不阻断启动)', async () => {
    const prisma = {
      syncLog: {
        findMany: jest.fn().mockRejectedValue(new Error('db down')),
        updateMany: jest.fn(),
      },
    };
    const svc = new SyncIncrementalSchedulerService(
      prisma as never, {} as never, {} as never, {} as never, {} as never, {} as never,
    );
    await expect(reap(svc)).resolves.toBeUndefined();
  });
});
