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 真锁)绝不碰。
 *
 * 本用例**不碰真实时钟**:时间界(进程启动时刻)作参数注入 reapStaleRunningLocks,
 * 行的 startedAt 全部相对固定基准构造。此前用 `Date.now() - 1000` 造时间差,判据里的界
 * 却是模块加载时刻 —— 机器有重负载(如并行 next build)时,模块加载到用例执行的间隔
 * 会超过 1s,行反而落到界之后不再算僵尸,间歇性假失败。
 */

/** 固定时间基准:代表"本进程启动时刻",不取真实时钟 */
const PROCESS_STARTED_AT = new Date('2026-07-23T02:00:00.000Z');
/** 早于进程启动 ms 毫秒 → 应被回收的僵尸锁 */
const before = (ms: number) => new Date(PROCESS_STARTED_AT.getTime() - ms);
/** 晚于进程启动 ms 毫秒 → 可能是并存 CLI 的真锁,绝不碰 */
const after = (ms: number) => new Date(PROCESS_STARTED_AT.getTime() + ms);

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, processStartedAt = PROCESS_STARTED_AT) =>
  (
    svc as unknown as { reapStaleRunningLocks: (at: Date) => Promise<void> }
  ).reapStaleRunningLocks(processStartedAt);

describe('reapStaleRunningLocks', () => {
  test('回收进程启动前的 running 行 → 标 failed', async () => {
    const { svc, updateMany } = makeService([
      { id: 'zombie', hostId: 'h1', startedAt: before(3_600_000), 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: before(1000), triggeredBy: 't' },
    ]);
    await reap(svc);
    expect(findMany.mock.calls[0][0].where.startedAt).toHaveProperty('lt', PROCESS_STARTED_AT);
    expect(findMany.mock.calls[0][0].where.startedAt).not.toHaveProperty('gte');
    expect(updateMany.mock.calls[0][0].where.startedAt).toHaveProperty('lt', PROCESS_STARTED_AT);
  });

  test('⭐ 进程启动后新建的 running 绝不碰(并存 CLI 的真锁)', async () => {
    // 边界行为锁:startedAt >= 界的行不进回收集合,连 updateMany 都不该发
    const { svc, updateMany } = makeService([
      { id: 'live-cli-lock', hostId: 'h1', startedAt: after(1000), triggeredBy: 'cli:manual' },
      { id: 'boundary', hostId: 'h2', startedAt: PROCESS_STARTED_AT, triggeredBy: 'cli:manual' },
    ]);
    await reap(svc);
    expect(updateMany).not.toHaveBeenCalled();
  });

  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();
  });
});
