import { ApiCode } from '@pac/types';
import { assertPlanClaimedBy, assertCanRecycle } from '../src/modules/plan/claim-guard';
import { Permission, ROLE_PERMISSIONS, UserRole } from '@pac/types';
import { BizError } from '../src/common/errors/biz-error';

/**
 * 认领闸回归 —— 「未认领只能看,不能操作」。
 *
 * 背景(2026-07 生产实证):submitExecution 当时只校验 status、**完全没看 assignee**,
 * 结果缺牙召回有两条 plan 在 `assignee IS NULL` 状态下被直接关闭。
 * 危害不止"撞单":execution.service 特意保留 assigneeUserId 作**业绩归属**
 * (那里的注释写明清空会让「我的已完成」/ 转化率永远为 0)—— 未认领就提交执行,
 * 这条执行没有任何归属人,统计里直接蒸发。所以闸必须在服务端,前端提示替代不了。
 *
 * 三条红线(下面各有用例锁住):
 *   1. 没人认领 → 拒绝,且码是 PLAN_NOT_CLAIMED(引导去认领)
 *   2. 别人认领 → 拒绝,且码**不同**(PLAN_CLAIMED_BY_OTHER),否则组员以为自己没点对
 *   3. 自己认领 → 放行(闸不能把正常作业也拦了)
 */

const ME = 'u-me';

function catchErr(fn: () => void): BizError {
  try {
    fn();
  } catch (e) {
    return e as BizError;
  }
  throw new Error('期望抛错,但没抛');
}

describe('assertPlanClaimedBy — 认领闸', () => {
  test('⭐ 红线③:自己认领 → 放行', () => {
    expect(() => assertPlanClaimedBy({ assigneeUserId: ME }, ME)).not.toThrow();
  });

  test('⭐ 红线①:没人认领 → PLAN_NOT_CLAIMED', () => {
    const err = catchErr(() => assertPlanClaimedBy({ assigneeUserId: null }, ME));
    expect(err).toBeInstanceOf(BizError);
    expect(err.code).toBe(ApiCode.PLAN_NOT_CLAIMED);
    expect(err.msg).toContain('认领');
  });

  test('⭐ 红线②:别人认领 → PLAN_CLAIMED_BY_OTHER(跟"没人认领"必须是不同的码)', () => {
    const err = catchErr(() => assertPlanClaimedBy({ assigneeUserId: 'u-other' }, ME));
    expect(err.code).toBe(ApiCode.PLAN_CLAIMED_BY_OTHER);
    expect(err.code).not.toBe(ApiCode.PLAN_NOT_CLAIMED); // 两态文案不能合并
    // details 带占用人 —— 前端据此查字典换姓名展示("已被 张三 认领")
    expect(err.details).toEqual({ assigneeUserId: 'u-other' });
  });

  test('空串 assignee 当作未认领(不是"被叫空串的人认领了")', () => {
    const err = catchErr(() => assertPlanClaimedBy({ assigneeUserId: '' }, ME));
    expect(err.code).toBe(ApiCode.PLAN_NOT_CLAIMED);
  });

  test('用户 id 区分大小写 / 不做模糊匹配 —— 不是自己就是不行', () => {
    expect(() => assertPlanClaimedBy({ assigneeUserId: 'U-ME' }, ME)).toThrow();
    expect(() => assertPlanClaimedBy({ assigneeUserId: 'u-me-2' }, ME)).toThrow();
  });
});

/**
 * 返池归属闸 —— 2026-07-29 把 PLAN_RECYCLE 放给 staff 时加的。
 *
 * 放权限这一步本身是安全的(@RequirePermission 只管"能不能调这个接口"),
 * **危险的是没有第二道**:客服 A 拿到 recycle 权限后,能把客服 B 手里的单退回池、
 * 再自己认领 —— 认领闸挡的是"没认领就作业",挡不住"先把别人的单退了"。
 * 下面四条锁住这道闸,别在重构 recycle 时把它顺手删了。
 */
describe('assertCanRecycle — 返池归属闸', () => {
  const ME = 'u-me';

  test('⭐ staff 退自己的单 → 放行', () => {
    expect(() => assertCanRecycle({ assigneeUserId: ME }, ME, false)).not.toThrow();
  });

  test('⭐ staff 退别人的单 → PLAN_CLAIMED_BY_OTHER(带上占用人,前端能提示是谁)', () => {
    const err = catchErr(() => assertCanRecycle({ assigneeUserId: 'u-other' }, ME, false));
    expect(err).toBeInstanceOf(BizError);
    expect(err.code).toBe(ApiCode.PLAN_CLAIMED_BY_OTHER);
    expect((err.details as { assigneeUserId?: string } | undefined)?.assigneeUserId).toBe('u-other');
  });

  test('leader(canRecycleOthers)退任意人的单 → 放行,这正是"组长回收"的原义', () => {
    expect(() => assertCanRecycle({ assigneeUserId: 'u-other' }, ME, true)).not.toThrow();
  });

  test('无人认领 → 放行(那种返池本就是空操作,拦它只是多一种没意义的报错)', () => {
    expect(() => assertCanRecycle({ assigneeUserId: null }, ME, false)).not.toThrow();
  });
});

describe('ROLE_PERMISSIONS — 返池权限矩阵', () => {
  test('⭐ staff 有 PLAN_RECYCLE(自助返池),但没有 PLAN_VIEW_ALL —— 后者正是"能不能退别人的"的判据', () => {
    const staff = ROLE_PERMISSIONS[UserRole.STAFF];
    expect(staff).toContain(Permission.PLAN_RECYCLE);
    expect(staff).not.toContain(Permission.PLAN_VIEW_ALL);
  });

  test('leader 两个都有 → 可退任意人的单', () => {
    const leader = ROLE_PERMISSIONS[UserRole.LEADER];
    expect(leader).toContain(Permission.PLAN_RECYCLE);
    expect(leader).toContain(Permission.PLAN_VIEW_ALL);
  });
});
