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

/**
 * 「确认之后再补挂福利」回归。
 *
 * ── 由来(2026-08-06 实测的事故)──────────────────────────────────
 * 福利原本**只在确认那一刻**随 `create` 落库。主管点完「确认分配」才想起来
 * 「这批带上八折」时,助手照样把 `set_benefit` 推给确认单卡片 —— 卡片改了自己的
 * `benefit` state、亮出绿色角标、还往对话里回了一句「本批福利设为…」,
 * 而那个 state **只有 `confirm()` 会读**,批次早就落库了,DB 里一个字没变。
 * 界面说做了、话术里没有,主管无从察觉(违 T14「口径对数」)。
 *
 * ⚠️ 这个 bug 的形状值得记:**没有任何报错**,而且"失败路径"看起来比成功路径还顺 ——
 *   角标亮了、话也回了。只有去查库或等客服念话术才会发现。
 *
 * 这里锁四件做错了不会报错的事:
 *   ① 真的写进 attributes.benefit(不是只改界面)
 *   ② 福利变了必须**作废话术缓存** —— 否则福利段永远不会出现 / 撤掉的福利还被念出去
 *   ③ 空串 = 撤掉,⛔ 不许落一个 `{text:''}` 的空壳(读侧会把它当"有福利")
 *   ④ 已被客服**打开过**的条数要如实回报 —— 那些人手里是旧话术
 */

const BATCH = 'c02e1b80-1111-4222-8333-444455556666';

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: {
  attributes?: unknown;
  status?: string;
  createdBy?: string;
  planIds?: string[];
  viewedPlanIds?: string[];
}) {
  const headUpdates: Array<Record<string, unknown>> = [];
  const deletedScriptPlanIds: string[][] = [];
  const planIds = opts.planIds ?? ['p1', 'p2', 'p3'];
  const tx = {
    planAssignment: {
      update: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
        headUpdates.push(data);
        return {};
      }),
    },
    planScript: {
      deleteMany: jest.fn(async (a: { where: { planId: { in: string[] } } }) => {
        deletedScriptPlanIds.push(a.where.planId.in);
        return { count: a.where.planId.in.length };
      }),
    },
  };
  const prisma = {
    planAssignment: {
      findFirst: jest.fn(async () => ({
        id: BATCH, hostId: 'h1', tenantId: 't1', clinicId: 'c1',
        createdBy: opts.createdBy ?? 'leader-1',
        status: opts.status ?? 'confirmed',
        attributes: opts.attributes ?? null,
        createdAt: new Date(),
      })),
    },
    followupPlan: { findMany: jest.fn(async () => planIds.map((id) => ({ id }))) },
    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, headUpdates, deletedScriptPlanIds };
}

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('⭐⭐ 红线①:福利写进 attributes.benefit.text(界面亮角标不算数)', async () => {
    const { prisma, headUpdates } = makePrisma({});
    const svc = await build(prisma);
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '看牙打八折优惠');
    expect(r.benefitText).toBe('看牙打八折优惠');
    expect(headUpdates).toHaveLength(1);
    expect(headUpdates[0]!.attributes).toEqual({ benefit: { text: '看牙打八折优惠' } });
  });

  test('⭐⭐ 红线②:福利变了必须作废本批话术缓存,否则福利段永远不会出现', async () => {
    const { prisma, deletedScriptPlanIds } = makePrisma({});
    const svc = await build(prisma);
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '老客户复查免挂号费');
    expect(deletedScriptPlanIds).toEqual([['p1', 'p2', 'p3']]);
    expect(r.scriptsInvalidated).toBe(3);
    expect(r.note).toContain('重新生成');
  });

  test('⭐ 红线③:空串 = 撤掉福利,⛔ 不许留一个 {text:""} 的空壳', async () => {
    const { prisma, headUpdates } = makePrisma({ attributes: { benefit: { text: '八折' } } });
    const svc = await build(prisma);
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '');
    expect(r.benefitText).toBeNull();
    // 读侧 readBenefitText 只看 benefit.text 存不存在 —— 留个空壳会被当成"有福利"
    expect(headUpdates[0]!.attributes).not.toHaveProperty('benefit');
    expect(r.note).toContain('不再带福利');
  });

  test('⭐⭐ 红线④:已被客服打开过的条数要如实回报(他手里是旧话术)', async () => {
    const { prisma } = makePrisma({ viewedPlanIds: ['p1', 'p2'] });
    const svc = await build(prisma);
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '八折');
    expect(r.touched).toBe(2);
    expect(r.note).toContain('已经打开过');
  });

  test('attributes 里原有的别的键要留着(⛔ 别整个覆盖掉)', async () => {
    const { prisma, headUpdates } = makePrisma({ attributes: { note: 'x', benefit: { text: '旧' } } });
    const svc = await build(prisma);
    await svc.setBenefit(SCOPE, LEADER, BATCH, '新');
    expect(headUpdates[0]!.attributes).toEqual({ note: 'x', benefit: { text: '新' } });
  });

  test('值没变 → 不写库、不作废话术(⛔ 别让重复指令白白炸掉一批缓存)', async () => {
    const { prisma, headUpdates, deletedScriptPlanIds } = makePrisma({
      attributes: { benefit: { text: '八折' } },
    });
    const svc = await build(prisma);
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '八折');
    expect(headUpdates).toHaveLength(0);
    expect(deletedScriptPlanIds).toHaveLength(0);
    expect(r.note).toContain('没有变化');
  });
});

describe('确认后补挂福利 —— 闸', () => {
  test('已撤销的批次不许再挂福利(那批人已经回池子了,挂上去谁也读不到)', async () => {
    const { prisma } = makePrisma({ status: 'revoked' });
    const svc = await build(prisma);
    await expect(svc.setBenefit(SCOPE, LEADER, BATCH, '八折')).rejects.toThrow('已撤销');
  });

  test('别人发起的批次改不了(与撤销同一条授权:自己分的,或有看全池权限)', async () => {
    const { prisma } = makePrisma({ createdBy: 'other-leader' });
    const svc = await build(prisma);
    await expect(svc.setBenefit(SCOPE, LEADER, BATCH, '八折')).rejects.toThrow('自己发起');
  });

  test('有 PLAN_VIEW_ALL 的人可以改别人的批次', async () => {
    const { prisma } = makePrisma({ createdBy: 'other-leader' });
    const svc = await build(prisma);
    const r = await svc.setBenefit(
      SCOPE,
      { userId: 'leader-1', permissions: [Permission.PLAN_DISPATCH, Permission.PLAN_VIEW_ALL] },
      BATCH,
      '八折',
    );
    expect(r.benefitText).toBe('八折');
  });

  test('没有 PLAN_DISPATCH 权限直接拒', async () => {
    const { prisma } = makePrisma({});
    const svc = await build(prisma);
    await expect(
      svc.setBenefit(SCOPE, { userId: 'leader-1', permissions: [] }, BATCH, '八折'),
    ).rejects.toThrow();
  });

  test('⚠️ 不设时间窗 —— 与撤销不同:改福利只影响之后生成的话术,越晚只是越没用,不会伤到谁', async () => {
    const { prisma } = makePrisma({});
    const svc = await build(prisma);
    // makePrisma 的 createdAt 是"刚刚",这里直接验证服务端没有读 createdAt 做拒绝
    const r = await svc.setBenefit(SCOPE, LEADER, BATCH, '八折');
    expect(r.benefitText).toBe('八折');
  });
});
