import { AssignmentExpiryScheduler } from '../src/modules/plan/assignment-expiry.scheduler';
import type { PrismaService } from '../src/prisma/prisma.service';

/**
 * 分配单到期自动回池回归。
 *
 * 产品定调:到期回池是给主管减负(他不必再去追"这单还要不要")。
 * 但它是**系统主动把单从客服手里收走**,三条红线错一条都会造成静默的数据/信任损失:
 *   ① 约好回访的单绝不能被收(snoozedUntil 守卫)—— 收了就是系统主动毁客服对患者的承诺
 *   ② 不写 release_reason —— 那列只属于客服的处置,到期混进去退回率分子分母一起脏
 *   ③ 不清批次归因三列 —— 清了这批的分母就少一条
 */

const NOW = new Date('2026-08-10T03:00:00Z');

function makeService(rows: Array<Record<string, unknown>>) {
  const captured: {
    where?: Record<string, unknown>;
    data?: Record<string, unknown>;
    updateWhere?: Record<string, unknown>;
  } = {};
  const events: Array<Record<string, unknown>> = [];
  const findMany = jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
    captured.where = where;
    return rows;
  });
  const tx = {
    followupPlan: {
      updateMany: jest.fn(
        async (args: { where: Record<string, unknown>; data: Record<string, unknown> }) => {
          captured.data = args.data;
          captured.updateWhere = args.where;
          return { count: rows.length };
        },
      ),
    },
    planEventLog: {
      createMany: jest.fn(async ({ data }: { data: Array<Record<string, unknown>> }) => {
        events.push(...data);
        return { count: data.length };
      }),
    },
  };
  const prisma = {
    followupPlan: { findMany },
    $transaction: jest.fn(async (fn: (t: typeof tx) => Promise<unknown>) => fn(tx)),
  } as unknown as PrismaService;
  return { svc: new AssignmentExpiryScheduler(prisma), captured, events, findMany, tx };
}

const PLAN = {
  id: 'p1',
  hostId: 'h1',
  tenantId: 't1',
  patientId: 'pat1',
  assigneeUserId: 'u-staff',
  assignedAt: new Date(NOW.getTime() - 3 * 86400_000), // 3 天前分的
};

describe('AssignmentExpiryScheduler', () => {
  const OLD = process.env.PAC_ASSIGNMENT_EXPIRY;
  afterEach(() => {
    if (OLD === undefined) delete process.env.PAC_ASSIGNMENT_EXPIRY;
    else process.env.PAC_ASSIGNMENT_EXPIRY = OLD;
  });

  test('⭐⭐ 红线①:查询条件必须带 snoozedUntil 守卫(约好回访的单不能被收)', async () => {
    delete process.env.PAC_ASSIGNMENT_EXPIRY;
    const { svc, captured } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    // 客服约了 6/10 回访、plan snooze 到 6/10 —— 到期也不能收,
    // 收了客服就丢了已对患者承诺的回访关系,而且单子还会被别人从池里捞走
    expect(captured.where?.OR).toEqual([
      { snoozedUntil: null },
      { snoozedUntil: { lte: expect.any(Date) } },
    ]);
    // 只收已过期的 assigned
    expect(captured.where?.status).toBe('assigned');
    expect(captured.where?.assignmentExpiresAt).toMatchObject({ not: null });
    expect(captured.where?.supersededAt).toBeNull();
  });

  test('⭐⭐ 红线②:**不写 release_reason** —— 到期不是客服的处置', async () => {
    const { svc, captured } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    // 退回(客服看了判断"不该我做")与到期(客服压根没动)是两件事。
    // 混进同一列,退回率的分子分母一起虚高,而"到期未动"这个数本身才是主管要的信号。
    expect(captured.data).not.toHaveProperty('releaseReason');
    expect(captured.data).not.toHaveProperty('releaseNote');
  });

  test('⭐⭐ 红线③:**不清批次归因三列**,但清在办期限', async () => {
    const { svc, captured } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(captured.data).not.toHaveProperty('assignmentId');
    expect(captured.data).not.toHaveProperty('assignedBy');
    expect(captured.data).not.toHaveProperty('assignStrategy');
    expect(captured.data).toMatchObject({
      status: 'active',
      assigneeUserId: null,
      assignmentExpiresAt: null,
    });
  });

  test('⭐ 红线④:绝不动 snoozedUntil(与退回同一条纪律)', async () => {
    const { svc, captured } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(captured.data).not.toHaveProperty('snoozedUntil');
  });

  test('落账本:auto_release + reason=assignment_expired + 持有时长算得出来', async () => {
    const { svc, events } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(events).toHaveLength(1);
    expect(events[0]).toMatchObject({
      event: 'auto_release',
      reason: 'assignment_expired',
      assigneeUserId: null,
      actorUserId: null, // 系统行为
    });
    // 时间界注入 → 精确 3 天,不给宽容区间也不会 flake
    expect(events[0]!.heldSeconds).toBe(3 * 86400);
  });

  test('并发安全:updateMany 的 where 带 status 条件(客服刚提交执行则本次不生效)', async () => {
    const { svc, captured } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(captured.updateWhere?.status).toBe('assigned');
  });

  test('无到期单 → 不发任何写(纯净轮次零副作用)', async () => {
    const { svc, tx } = makeService([]);
    await svc.runExpiry(NOW);
    expect(tx.followupPlan.updateMany).not.toHaveBeenCalled();
    expect(tx.planEventLog.createMany).not.toHaveBeenCalled();
  });

  test('⭐ 开关 off → 一行都不碰(连查询都不发)', async () => {
    process.env.PAC_ASSIGNMENT_EXPIRY = 'off';
    const { svc, findMany } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(findMany).not.toHaveBeenCalled();
  });

  test('⭐ 默认是**开**的 —— 与 PAC_PLAN_AUTO_RECYCLE(默认关)相反,别搞混', async () => {
    delete process.env.PAC_ASSIGNMENT_EXPIRY;
    const { svc, findMany } = makeService([PLAN]);
    await svc.runExpiry(NOW);
    expect(findMany).toHaveBeenCalled();
    // 不收回的后果不是"多几条过期单",是容量口径整体失效:在手只增不减,几批后全员触顶
  });
});
