import { lookupDxTreatment } from '@pac/types';
import { PlanEngineService } from '../src/modules/plan/engine/plan-engine.service';
import type { ScenarioHit } from '../src/modules/plan/engine/scenario.interface';

/**
 * 缺口1 回归:重算后变成 0 命中的患者,其遗留 active(非 assigned)plan 应被 supersede 关闭(退出召回池)。
 * 缺口2 回归:wholeMouth flag(全口/全牙弓病)在 canonical-codes 正确标注 —— SQL 行为靠服务器实测覆盖。
 *
 * 背景:之前修"正畸/牙周误召"暴露引擎两个独立缺口:
 *   缺口1 — 0 命中时不关旧 plan,治完疗该结案的召回 plan 永远残留 active。
 *   缺口2 — scenario 排除/聚类不消费 wholeMouth,K05/K07 诊断带牙位时被当单牙误召。
 */

const HOST = 'host-1';
const TENANT = 'tenant-1';

/// 构造一个最小可用的 PrismaService mock(只 mock 引擎用到的方法)。
function makePrismaMock(opts: {
  /// closeStaleActivePlan / upsertPlan 的 latest plan 查询返回值
  latestPlan?: { id: string; status: string } | null;
  /// runAllForHost 关闭闸:status='active' 的全量 plan
  activePlans?: Array<{ id: string; patientId: string }>;
}) {
  const update = jest.fn().mockResolvedValue({});
  const updateMany = jest.fn().mockImplementation(async ({ where }) => ({
    count: Array.isArray(where?.id?.in) ? where.id.in.length : 0,
  }));
  const create = jest.fn().mockResolvedValue({ id: 'new-plan' });
  const findFirst = jest.fn().mockResolvedValue(opts.latestPlan ?? null);
  const findMany = jest.fn().mockImplementation(async ({ where }) => {
    // runAllForHost 关闭闸:status IN ('active','assigned')
    const statusIn = where?.status?.in;
    if (Array.isArray(statusIn) && statusIn.includes('active')) return opts.activePlans ?? [];
    // snoozed(completed/abandoned)/ prefetchForBatch latest(无 status)→ 空
    return [];
  });

  const prisma = {
    followupPlan: { findFirst, findMany, update, updateMany, create },
    // 批量路径 prefetchForBatch 用 persona.findMany;单刷用 findFirst
    persona: {
      findFirst: jest.fn().mockResolvedValue(null),
      findMany: jest.fn().mockResolvedValue([]),
    },
    planGenerationLog: {
      create: jest.fn().mockResolvedValue({ id: 'log-1' }),
      update: jest.fn().mockResolvedValue({}),
      // 批量路径日志一次性 createMany
      createMany: jest.fn().mockResolvedValue({ count: 0 }),
    },
    $transaction: jest
      .fn()
      .mockImplementation(async (cb: (tx: unknown) => Promise<unknown>) =>
        cb({
          followupPlan: { update: jest.fn().mockResolvedValue({}), create: jest.fn().mockResolvedValue({}) },
        }),
      ),
  };
  return { prisma, spies: { update, updateMany, create, findFirst, findMany } };
}

function makeScenario(hits: ScenarioHit[]) {
  return {
    key: 'treatment_initiation_recall',
    selectHits: jest.fn().mockResolvedValue(hits),
  };
}

function hit(patientId: string, subKey: string): ScenarioHit {
  return {
    patientId,
    patientExternalId: `ext-${patientId}`,
    reason: 'r',
    priorityScore: 50,
    subKey,
    evidence: { factIds: ['f1'] },
  };
}

function makeEngine(prisma: unknown, scenario: unknown) {
  return new PlanEngineService(
    prisma as never,
    scenario as never,
  );
}

describe('缺口1 — recomputeForPatient 0 命中关闭遗留 active plan', () => {
  test('0 命中 + 存在 active plan → supersede 关闭(plansClosed=1)', async () => {
    const { prisma, spies } = makePrismaMock({ latestPlan: { id: 'plan-a', status: 'active' } });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });

    expect(res).toMatchObject({ plansCreated: 0, plansClosed: 1 });
    expect(spies.update).toHaveBeenCalledTimes(1);
    expect(spies.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: 'plan-a' },
        data: expect.objectContaining({ status: 'superseded' }),
      }),
    );
    // 0 命中走 early return,不应建 PlanGenerationLog(无 upsert)
    expect(prisma.planGenerationLog.create).not.toHaveBeenCalled();
  });

  // 注:closeStaleActivePlan 现关闭 active **及 assigned**(2026-06 决策:缺口全消失即关,
  //   含 assigned,否则客服拿"已解决"理由白打)。service.ts:70 行旧注释"assigned 不动"已过时。
  test('0 命中 + assigned plan → 关闭(2026-06 决策:缺口消失即关,含 assigned)', async () => {
    const { prisma, spies } = makePrismaMock({ latestPlan: { id: 'plan-a', status: 'assigned' } });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });

    expect(res).toMatchObject({ plansCreated: 0, plansClosed: 1 });
    expect(spies.update).toHaveBeenCalledTimes(1);
  });

  test('0 命中 + 无任何 plan → 无操作(plansClosed=0)', async () => {
    const { prisma, spies } = makePrismaMock({ latestPlan: null });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });

    expect(res).toMatchObject({ plansCreated: 0, plansClosed: 0 });
    expect(spies.update).not.toHaveBeenCalled();
  });

  test('0 命中 + 终态 plan(completed)→ 不动(已是终态,plansClosed=0)', async () => {
    const { prisma, spies } = makePrismaMock({ latestPlan: { id: 'plan-a', status: 'completed' } });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.recomputeForPatient({ hostId: HOST, tenantId: TENANT, patientId: 'pat-a' });

    expect(res).toMatchObject({ plansCreated: 0, plansClosed: 0 });
    expect(spies.update).not.toHaveBeenCalled();
  });
});

describe('缺口1 — runAllForHost 关闭"有 active plan 但本轮 0 命中"的患者', () => {
  test('A 有命中(保留)+ B 无命中(关闭)→ plansClosed=1,只关 B', async () => {
    // A 本轮命中;B 没命中但有遗留 active plan
    const { prisma, spies } = makePrismaMock({
      latestPlan: null, // upsertPlan(A):无既有 → 走 create
      activePlans: [
        { id: 'plan-a2', patientId: 'pat-a' }, // A 本轮新产生的 active(在 hitsByPatient → 跳过)
        { id: 'plan-b', patientId: 'pat-b' }, // B 遗留 active(不在 hitsByPatient → 关闭)
      ],
    });
    const engine = makeEngine(prisma, makeScenario([hit('pat-a', 'missing_tooth@whole')]));

    const res = await engine.runAllForHost({ hostId: HOST, tenantId: TENANT, now: new Date('2026-06-02T00:00:00Z') });

    expect(res.plansCreated).toBe(1);
    expect(res.plansClosed).toBe(1);
    // 只关 B,不关 A 的新 active
    expect(spies.updateMany).toHaveBeenCalledTimes(1);
    expect(spies.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: { in: ['plan-b'] } },
        data: expect.objectContaining({ status: 'superseded' }),
      }),
    );
  });

  test('全部患者 0 命中 → 所有遗留 active plan 关闭', async () => {
    const { prisma, spies } = makePrismaMock({
      activePlans: [
        { id: 'plan-x', patientId: 'pat-x' },
        { id: 'plan-y', patientId: 'pat-y' },
      ],
    });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.runAllForHost({ hostId: HOST, tenantId: TENANT, now: new Date('2026-06-02T00:00:00Z') });

    expect(res.patientsHit).toBe(0);
    expect(res.plansClosed).toBe(2);
    expect(spies.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: { in: ['plan-x', 'plan-y'] } } }),
    );
  });

  test('无遗留 active plan → 不调 updateMany(plansClosed=0)', async () => {
    const { prisma, spies } = makePrismaMock({ activePlans: [] });
    const engine = makeEngine(prisma, makeScenario([]));

    const res = await engine.runAllForHost({ hostId: HOST, tenantId: TENANT, now: new Date('2026-06-02T00:00:00Z') });

    expect(res.plansClosed).toBe(0);
    expect(spies.updateMany).not.toHaveBeenCalled();
  });
});

describe('缺口2 — wholeMouth flag 标注(单一真理源,防漂移)', () => {
  test('全口/全牙弓病码(K05 牙周 / K07 正畸)标 wholeMouth', () => {
    expect(lookupDxTreatment('K05')?.wholeMouth).toBe(true);
    expect(lookupDxTreatment('K07')?.wholeMouth).toBe(true);
  });

  test('对应推荐码同标 wholeMouth(SRP / 正畸咨询)', () => {
    expect(lookupDxTreatment('SRP_RECOMMENDED')?.wholeMouth).toBe(true);
    expect(lookupDxTreatment('ORTHO_CONSULT_RECOMMENDED')?.wholeMouth).toBe(true);
  });

  test('按牙位精确匹配的码不标 wholeMouth(K02 龋 / K04 根管 / K08 缺牙)', () => {
    expect(lookupDxTreatment('K02')?.wholeMouth).toBeUndefined();
    expect(lookupDxTreatment('K04')?.wholeMouth).toBeUndefined();
    expect(lookupDxTreatment('K08')?.wholeMouth).toBeUndefined();
  });
});
