import { Test } from '@nestjs/testing';
import { PlanEventType, PLAN_EVENT_META, HUMAN_TOUCH_EVENTS } from '@pac/types';
import { computeHeldSeconds } from '../src/modules/plan/plan-event.recorder';
import { PlanService } from '../src/modules/plan/plan.service';
import { PrismaService } from '../src/prisma/prisma.service';
import { RecycleSchedulerService } from '../src/modules/plan/recycle-scheduler.service';

/**
 * 归属账本(PlanEventLog)回归。
 *
 * 由来:「认领」要当作「该患者已被客服处理」用于统计,但 followup_plans 只存**当前**归属
 * (assignee_user_id / assigned_at),返池时就地置 null —— 痕迹消失。生产实测两天自动回收
 * 32 单,这些"接手了但没继续"的样本(恰恰是判断召回池准不准最有价值的)全部无法追溯。
 *
 * 本文件锁四条:
 *   1. 认领必须落账,且 actor==assignee 记 claim、actor!=assignee 记 assign(为门诊经理分配预留)
 *   2. 返池必须落账,且 heldSeconds 要在清空 assignedAt **之前**算出来(事后算不出来)
 *   3. 幂等重复认领不重复记账(那是续期,不是新的归属变更)
 *   4. 自动回收默认关闭 —— 开关没开就不能动数据
 */

const SCOPE = { hostId: 'h1', tenantId: 't1', sourceUnits: [] as string[], clinicIds: [] as string[] } as never;

const PLAN = {
  id: 'p1',
  hostId: 'h1',
  tenantId: 't1',
  patientId: 'pat1',
  status: 'active',
  assigneeUserId: null as string | null,
  assignedAt: null as Date | null,
};

function makePrisma(planOverride: Partial<typeof PLAN> = {}) {
  const created: Record<string, unknown>[] = [];
  const tx = {
    followupPlan: { update: jest.fn().mockResolvedValue({}), updateMany: jest.fn().mockResolvedValue({ count: 1 }) },
    planEventLog: {
      create: jest.fn().mockImplementation(({ data }: { data: Record<string, unknown> }) => {
        created.push(data);
        return Promise.resolve(data);
      }),
    },
  };
  const prisma = {
    followupPlan: {
      findFirst: jest.fn().mockResolvedValue({ ...PLAN, ...planOverride }),
      findMany: jest.fn().mockResolvedValue([]),
    },
    $transaction: jest.fn().mockImplementation(async (fn: (t: typeof tx) => Promise<unknown>) => fn(tx)),
  };
  return { prisma: prisma as unknown as PrismaService, created, tx };
}

async function buildService(prisma: PrismaService) {
  const mod = await Test.createTestingModule({
    providers: [PlanService, { provide: PrismaService, useValue: prisma }],
  })
    .useMocker(() => ({}))
    .compile();
  return mod.get(PlanService);
}

describe('PlanEventLog — 认领 / 返池落账', () => {
  test('⭐ 自认领(actor == assignee)→ action=claim', async () => {
    const { prisma, created } = makePrisma();
    const svc = await buildService(prisma);
    await svc.assign(SCOPE, 'p1', 'u-me', 'u-me');
    expect(created).toHaveLength(1);
    expect(created[0]).toMatchObject({
      event: 'claim',
      assigneeUserId: 'u-me',
      actorUserId: 'u-me',
      planId: 'p1',
      patientId: 'pat1',
    });
  });

  test('⭐ 指派他人(actor != assignee)→ action=assign(门诊经理分配上线即可用)', async () => {
    const { prisma, created } = makePrisma();
    const svc = await buildService(prisma);
    await svc.assign(SCOPE, 'p1', 'u-staff', 'u-leader');
    expect(created[0]).toMatchObject({
      event: 'assign',
      assigneeUserId: 'u-staff',
      actorUserId: 'u-leader',
    });
  });

  test('调用方没传 actor(老路径)→ 按自认领记,不产生错误的 assign', async () => {
    const { prisma, created } = makePrisma();
    const svc = await buildService(prisma);
    await svc.assign(SCOPE, 'p1', 'u-me');
    expect(created[0]).toMatchObject({ event: 'claim', actorUserId: 'u-me' });
  });

  test('⭐ 幂等:重复认领给同一人 → 只续期,不重复记账', async () => {
    const { prisma, created } = makePrisma({ status: 'assigned', assigneeUserId: 'u-me' });
    const svc = await buildService(prisma);
    await svc.assign(SCOPE, 'p1', 'u-me', 'u-me');
    expect(created).toHaveLength(0);
  });

  test('⭐ 返池落账,且 heldSeconds 按 assignedAt 算出(清空前)', async () => {
    const assignedAt = new Date(Date.now() - 3600_000); // 1 小时前
    const { prisma, created } = makePrisma({ status: 'assigned', assigneeUserId: 'u-me', assignedAt });
    const svc = await buildService(prisma);
    await svc.recycle(SCOPE, 'p1', 'u-leader');
    expect(created).toHaveLength(1);
    expect(created[0]).toMatchObject({
      event: 'release',
      assigneeUserId: null, // 释放后无人归属
      actorUserId: 'u-leader',
    });
    // 允许几秒抖动
    const held = created[0]!.heldSeconds as number;
    expect(held).toBeGreaterThanOrEqual(3595);
    expect(held).toBeLessThanOrEqual(3605);
  });

  test('本来就没人认领的"返池" → 空操作,不记噪声', async () => {
    const { prisma, created } = makePrisma({ status: 'active', assigneeUserId: null });
    const svc = await buildService(prisma);
    await svc.recycle(SCOPE, 'p1', 'u-leader');
    expect(created).toHaveLength(0);
  });
});

describe('PlanEventType / PLAN_EVENT_META — 扩展性约束', () => {
  test('⭐ 每个事件类型都在 META 里登记(新增事件忘了登记就失败)', () => {
    for (const t of Object.values(PlanEventType)) {
      expect(PLAN_EVENT_META[t]).toBeDefined();
      expect(PLAN_EVENT_META[t].labelZh.length).toBeGreaterThan(0);
    }
    // 反向:META 里不能有 enum 之外的孤儿键
    expect(Object.keys(PLAN_EVENT_META).sort()).toEqual([...Object.values(PlanEventType)].sort());
  });

  test('⭐ auto_release 不算人工 —— 统计「客服处理过」不能把系统回收算进去', () => {
    expect(PLAN_EVENT_META.auto_release.byHuman).toBe(false);
    expect(HUMAN_TOUCH_EVENTS).not.toContain(PlanEventType.AUTO_RELEASE);
    expect(HUMAN_TOUCH_EVENTS).toEqual(
      expect.arrayContaining([PlanEventType.CLAIM, PlanEventType.ASSIGN, PlanEventType.FEEDBACK]),
    );
  });

  test('⭐⭐ 复制话术不算「处理过患者」—— 复制 ≠ 联系了患者', () => {
    // 复制完可能压根没发出去。算进 HUMAN_TOUCH_EVENTS 的话,「处理过的患者」
    // 会静默膨胀成"复制一下也算" —— 与 view 同一类错误,而它是 filter 派生的,
    // 改错**不会有任何编译错误**。
    expect(PLAN_EVENT_META.script_copy.byHuman).toBe(false);
    expect(HUMAN_TOUCH_EVENTS).not.toContain(PlanEventType.SCRIPT_COPY);
  });

  test('⭐ 复制话术不参与归属区间(可能发生在未认领的单上)', () => {
    expect(PLAN_EVENT_META.script_copy.holdsPatient).toBe(false);
  });

  test('归属类事件的 holdsPatient 口径正确(算归属区间用)', () => {
    expect(PLAN_EVENT_META.claim.holdsPatient).toBe(true);
    expect(PLAN_EVENT_META.assign.holdsPatient).toBe(true);
    expect(PLAN_EVENT_META.release.holdsPatient).toBe(false);
    expect(PLAN_EVENT_META.auto_release.holdsPatient).toBe(false);
  });

  // ── 主管重排(调整各客服在手的单)—— 2026-08-20 ──────────────────
  test('⭐⭐ 重排三件事都不算「客服处理过」—— 是主管在调度,不是有人跟进了患者', () => {
    for (const t of [
      PlanEventType.TRANSFER,
      PlanEventType.EXPIRY_CHANGED,
      PlanEventType.REARRANGE_REMOVE,
    ]) {
      expect(PLAN_EVENT_META[t].byHuman).toBe(false);
      expect(HUMAN_TOUCH_EVENTS).not.toContain(t);
    }
  });

  test('⭐ 改派/改时限之后单子还在人手上,移出之后不在', () => {
    expect(PLAN_EVENT_META.transfer.holdsPatient).toBe(true);
    expect(PLAN_EVENT_META.expiry_changed.holdsPatient).toBe(true);
    expect(PLAN_EVENT_META.rearrange_remove.holdsPatient).toBe(false);
  });

  test('⭐⭐ 重排移出**不是** release —— 复用会污染批次的退回原因分布', () => {
    // 批次详情按 `event === 'release'` 聚合退回原因,且**只按 assignment_id 过滤、
    // 不看是谁干的**(plan-assignment.service 的 detail())。重排移出若挂 release:
    //   ① 会进那一批的退回原因分布(污染主管调下一批的输入);
    //   ② 会给**原客服**记一笔「退回」—— 他没退,是主管收走的。
    // 两个错都不抛异常,所以在这里钉死。
    expect(PlanEventType.REARRANGE_REMOVE).not.toBe(PlanEventType.RELEASE);
    expect(PLAN_EVENT_META.rearrange_remove.labelZh).not.toBe(PLAN_EVENT_META.release.labelZh);
  });
});

describe('computeHeldSeconds — 必须在清空 assignedAt 前算', () => {
  test('无 assignedAt → null(不是 0,0 会被误读成"秒放")', () => {
    expect(computeHeldSeconds(null, new Date())).toBeNull();
    expect(computeHeldSeconds(undefined, new Date())).toBeNull();
  });

  test('正常时长按秒取整', () => {
    const now = new Date('2026-07-24T12:00:00Z');
    expect(computeHeldSeconds(new Date('2026-07-24T11:00:00Z'), now)).toBe(3600);
  });

  test('时钟回拨等异常 → 夹到 0,不出负数', () => {
    const now = new Date('2026-07-24T12:00:00Z');
    expect(computeHeldSeconds(new Date('2026-07-24T13:00:00Z'), now)).toBe(0);
  });
});

describe('RecycleSchedulerService — 自动回收默认关闭', () => {
  const OLD = process.env.PAC_PLAN_AUTO_RECYCLE;
  afterEach(() => {
    if (OLD === undefined) delete process.env.PAC_PLAN_AUTO_RECYCLE;
    else process.env.PAC_PLAN_AUTO_RECYCLE = OLD;
  });

  test('⭐ 未设开关 → 一行数据都不碰(连查询都不发)', async () => {
    delete process.env.PAC_PLAN_AUTO_RECYCLE;
    const { prisma } = makePrisma();
    await new RecycleSchedulerService(prisma).runRecycle();
    expect((prisma as unknown as { followupPlan: { findMany: jest.Mock } }).followupPlan.findMany)
      .not.toHaveBeenCalled();
  });

  test('开关为其他值(true/1/yes)也不算开 —— 只认显式 on,避免误开', async () => {
    const { prisma } = makePrisma();
    for (const v of ['true', '1', 'yes', 'ON ']) {
      process.env.PAC_PLAN_AUTO_RECYCLE = v;
      await new RecycleSchedulerService(prisma).runRecycle();
    }
    const findMany = (prisma as unknown as { followupPlan: { findMany: jest.Mock } }).followupPlan.findMany;
    // 'ON ' trim+lower 后 = 'on' → 应被认为开启,其余三个不开
    expect(findMany).toHaveBeenCalledTimes(1);
  });

  test('显式 on → 才会扫描', async () => {
    process.env.PAC_PLAN_AUTO_RECYCLE = 'on';
    const { prisma } = makePrisma();
    await new RecycleSchedulerService(prisma).runRecycle();
    expect((prisma as unknown as { followupPlan: { findMany: jest.Mock } }).followupPlan.findMany)
      .toHaveBeenCalled();
  });
});

// =============================================================
// 2026-07 加 view 事件(plan 详情页 PV/UV)—— 锁死"浏览 ≠ 处理"
// =============================================================
describe('PlanEventType.VIEW — PV/UV 埋点', () => {
  it('⭐ 红线:view 绝不能进 HUMAN_TOUCH_EVENTS', () => {
    // byHuman 是**口径开关**不是"是不是人做的"。给 view 写 true 会让
    // 「客服处理过的患者」静默膨胀成"看一眼也算处理",且**没有任何编译错误**。
    expect(PLAN_EVENT_META.view.byHuman).toBe(false);
    expect(HUMAN_TOUCH_EVENTS).not.toContain(PlanEventType.VIEW);
  });

  it('HUMAN_TOUCH_EVENTS 精确等于四个处理类事件(加了新事件要显式决策)', () => {
    expect([...HUMAN_TOUCH_EVENTS].sort()).toEqual(
      [
        PlanEventType.ASSIGN,
        PlanEventType.CLAIM,
        PlanEventType.FEEDBACK,
        PlanEventType.RELEASE,
      ].sort(),
    );
  });

  it('view 不改变归属:holdsPatient=false,独立 group', () => {
    expect(PLAN_EVENT_META.view.holdsPatient).toBe(false);
    expect(PLAN_EVENT_META.view.group).toBe('view');
  });

  it('view 已在枚举登记(元数据完整)', () => {
    expect(PlanEventType.VIEW).toBe('view');
    expect(PLAN_EVENT_META.view.labelZh.length).toBeGreaterThan(0);
  });
});
