import { Test } from '@nestjs/testing';
import { Permission, PlanEventType, REVOKE_WINDOW_MINUTES } from '@pac/types';
import { PlanAssignmentService } from '../src/modules/plan/plan-assignment.service';
import { PrismaService } from '../src/prisma/prisma.service';

/**
 * 撤销整批回归。
 *
 * ⚠️ **撤销 ≠ 退回**(T21):主管收整批 vs 客服退单条。
 * 三条红线错一条都会造成静默损失:
 *   ① 已被打开过的单被收走 → 客服刚打完的电话永久蒸发,他第二天才发现单子没了
 *   ② 写了 release_reason  → 退回率的分子分母一起虚高
 *   ③ 清了批次归因         → 这次误操作影响了多少人,事后再也说不清
 */

const SCOPE = {
  hostId: 'h1', tenantId: 't1', sourceUnits: [] as string[], clinicIds: [] as string[], userId: 'leader-1',
} as never;
const LEADER = { userId: 'leader-1', permissions: [Permission.PLAN_DISPATCH] };

function makePrisma(opts: {
  createdBy?: string;
  ageMinutes?: number;
  status?: string;
  plans?: Array<{ id: string; status: string; assigneeUserId: string | null }>;
  viewedPlanIds?: string[];
}) {
  const captured: { data?: Record<string, unknown>; ids?: string[] } = {};
  const events: Array<Record<string, unknown>> = [];
  const headUpdates: Array<Record<string, unknown>> = [];
  const plans = opts.plans ?? [
    { id: 'p1', status: 'assigned', assigneeUserId: 'a' },
    { id: 'p2', status: 'assigned', assigneeUserId: 'a' },
  ];
  const tx = {
    followupPlan: {
      updateMany: jest.fn(async (a: { where: { id: { in: string[] } }; data: Record<string, unknown> }) => {
        captured.data = a.data;
        captured.ids = a.where.id.in;
        return { count: a.where.id.in.length };
      }),
    },
    planEventLog: {
      createMany: jest.fn(async ({ data }: { data: Array<Record<string, unknown>> }) => {
        events.push(...data);
        return { count: data.length };
      }),
    },
    planAssignment: {
      update: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
        headUpdates.push(data);
        return {};
      }),
    },
  };
  const prisma = {
    planAssignment: {
      findFirst: jest.fn(async () => ({
        id: 'b1', hostId: 'h1', tenantId: 't1', clinicId: 'c1',
        createdBy: opts.createdBy ?? 'leader-1',
        status: opts.status ?? 'confirmed',
        createdAt: new Date(Date.now() - (opts.ageMinutes ?? 5) * 60_000),
      })),
    },
    followupPlan: {
      findMany: jest.fn(async () =>
        plans.map((p) => ({
          ...p, hostId: 'h1', tenantId: 't1', patientId: `pat-${p.id}`,
          assignedAt: new Date(Date.now() - 3600_000),
        })),
      ),
    },
    planEventLog: {
      groupBy: jest.fn(async () => (opts.viewedPlanIds ?? []).map((planId) => ({ planId }))),
    },
    $transaction: jest.fn(async (fn: (t: typeof tx) => Promise<unknown>) => fn(tx)),
  } as unknown as PrismaService;
  return { prisma, captured, events, headUpdates, tx };
}

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

describe('撤销整批 —— 「已动过」的判据', () => {
  test('⭐⭐ 红线①:客服**打开过**的单不收,并如实回报', async () => {
    // 判据只能用 view 事件:plan_executions 回写率仅 11%,拿它判会把 89%
    // 已经打过电话的单当成"没动过"收走,那通电话永久蒸发。
    const { prisma, captured } = makePrisma({ viewedPlanIds: ['p1'] });
    const svc = await build(prisma);
    const r = await svc.revoke(SCOPE, LEADER, 'b1');
    expect(r.revoked).toBe(1);
    expect(r.skippedTouched).toBe(1);
    expect(captured.ids).toEqual(['p2']); // p1 被跳过
    expect(r.note).toContain('已经打开过');
  });

  test('全都没打开过 → 全收', async () => {
    const { prisma } = makePrisma({});
    const svc = await build(prisma);
    const r = await svc.revoke(SCOPE, LEADER, 'b1');
    expect(r.revoked).toBe(2);
    expect(r.skippedTouched).toBe(0);
  });

  test('已退回 / 已到期的不计入(本就不在人手上)', async () => {
    const { prisma } = makePrisma({
      plans: [
        { id: 'p1', status: 'assigned', assigneeUserId: 'a' },
        { id: 'p2', status: 'active', assigneeUserId: null },
      ],
    });
    const svc = await build(prisma);
    const r = await svc.revoke(SCOPE, LEADER, 'b1');
    expect(r.revoked).toBe(1);
    expect(r.alreadyReleased).toBe(1);
  });
});

describe('撤销整批 —— 写入纪律', () => {
  test('⭐⭐ 红线②:**不写 release_reason**(那是客服的处置,不是主管的收回)', async () => {
    const { prisma, captured } = makePrisma({});
    const svc = await build(prisma);
    await svc.revoke(SCOPE, LEADER, 'b1');
    // 混进去会让退回率的分子分母一起虚高
    expect(captured.data).not.toHaveProperty('releaseReason');
    expect(captured.data).not.toHaveProperty('releaseNote');
  });

  test('⭐⭐ 红线③:**不清批次归因三列**(否则这次误操作影响了多少人再也说不清)', async () => {
    const { prisma, captured } = makePrisma({});
    const svc = await build(prisma);
    await svc.revoke(SCOPE, LEADER, 'b1');
    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 { prisma, captured } = makePrisma({});
    const svc = await build(prisma);
    await svc.revoke(SCOPE, LEADER, 'b1');
    expect(captured.data).not.toHaveProperty('snoozedUntil');
  });

  test('⭐ 账本记 actor = **主管**(不像到期回收那样为 null)—— 撤销是人做的决定,要能追责', async () => {
    const { prisma, events } = makePrisma({});
    const svc = await build(prisma);
    await svc.revoke(SCOPE, LEADER, 'b1');
    expect(events[0]).toMatchObject({
      event: PlanEventType.AUTO_RELEASE,
      reason: 'revoked',
      actorUserId: 'leader-1',
      assigneeUserId: null,
    });
  });

  test('批次头标 revoked + 记录撤销人与时刻', async () => {
    const { prisma, headUpdates } = makePrisma({});
    const svc = await build(prisma);
    await svc.revoke(SCOPE, LEADER, 'b1');
    expect(headUpdates[0]).toMatchObject({ status: 'revoked', revokedBy: 'leader-1' });
  });
});

describe('撤销整批 —— 授权与时限', () => {
  test('⭐ 别人分的批次 → 拒(除非有 PLAN_VIEW_ALL)', async () => {
    const { prisma } = makePrisma({ createdBy: 'other-leader' });
    const svc = await build(prisma);
    await expect(svc.revoke(SCOPE, LEADER, 'b1')).rejects.toThrow(/只能撤销自己发起的批次/);
  });

  test('有 PLAN_VIEW_ALL → 可撤别人的批次', async () => {
    const { prisma } = makePrisma({ createdBy: 'other-leader' });
    const svc = await build(prisma);
    const r = await svc.revoke(
      SCOPE,
      { userId: 'leader-1', permissions: [Permission.PLAN_DISPATCH, Permission.PLAN_VIEW_ALL] },
      'b1',
    );
    expect(r.revoked).toBe(2);
  });

  test('⭐ 超出撤销窗口 → 拒,并指路到逐条退回(那条路会留下原因)', async () => {
    const { prisma } = makePrisma({ ageMinutes: REVOKE_WINDOW_MINUTES + 1 });
    const svc = await build(prisma);
    await expect(svc.revoke(SCOPE, LEADER, 'b1')).rejects.toThrow(/撤销窗口/);
  });

  test('⭐ 幂等:重复撤销不报错(主管手抖点两下很正常)', async () => {
    const { prisma, tx } = makePrisma({ status: 'revoked' });
    const svc = await build(prisma);
    const r = await svc.revoke(SCOPE, LEADER, 'b1');
    expect(r.revoked).toBe(0);
    expect(tx.followupPlan.updateMany).not.toHaveBeenCalled();
  });

  test('⭐ 合成身份(企微)不能撤销', async () => {
    const { prisma } = makePrisma({});
    const svc = await build(prisma);
    await expect(
      svc.revoke(SCOPE, { userId: 'wx:someone', permissions: [Permission.PLAN_DISPATCH] }, 'b1'),
    ).rejects.toThrow(/合成身份/);
  });
});
