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

/**
 * runAllForHost 批量化(2026-06)回归:
 *   bulk 预取 latest/snoozed/persona + 分块并发 upsert + PlanGenerationLog createMany。
 * 目标:证明批量路径在全部 5 种结局(create / unchanged / superseded / suppressed / stale-close)
 *      下与逐患者语义**等价**。用忠实内存 mock(按 where/orderBy/include 真实建模)端到端跑。
 */

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

type Reason = { scenario: string; subKey: string | null };
interface Plan {
  id: string;
  hostId: string;
  tenantId: string;
  patientId: string;
  version: number;
  status: string;
  priorityScore: number;
  snoozedUntil: Date | null;
  reasons: Reason[];
  assigneeUserId: string | null;
  assignedAt: Date | null;
  recycleAt: Date | null;
  contactAttempts: number;
}

const inArr = (v: unknown): unknown[] | null =>
  v && typeof v === 'object' && Array.isArray((v as { in?: unknown[] }).in)
    ? (v as { in: unknown[] }).in
    : null;

/// 忠实内存 mock:findFirst/findMany 真的按 where(patientId/status 支持 in,snoozedUntil.gt)过滤、
/// 按 version desc 排序;update/updateMany/create/$transaction 真的改同一个 plans 数组。
function makeStore(seed: { plans?: Partial<Plan>[]; personas?: { patientId: string; id: string }[] } = {}) {
  let idc = 1000;
  const plans: Plan[] = (seed.plans ?? []).map((p, i) => ({
    id: p.id ?? `seed-${i}`,
    hostId: HOST,
    tenantId: TENANT,
    patientId: p.patientId!,
    version: p.version ?? 1,
    status: p.status ?? 'active',
    priorityScore: p.priorityScore ?? 50,
    snoozedUntil: p.snoozedUntil ?? null,
    reasons: p.reasons ?? [],
    assigneeUserId: p.assigneeUserId ?? null,
    assignedAt: p.assignedAt ?? null,
    recycleAt: p.recycleAt ?? null,
    contactAttempts: p.contactAttempts ?? 0,
  }));
  const personas = seed.personas ?? [];
  const logs: Array<Record<string, unknown>> = [];

  const matchPlan = (pl: Plan, where: Record<string, unknown>): boolean => {
    if (where.hostId && pl.hostId !== where.hostId) return false;
    if (where.tenantId && pl.tenantId !== where.tenantId) return false;
    if (where.patientId !== undefined) {
      const ins = inArr(where.patientId);
      if (ins) {
        if (!ins.includes(pl.patientId)) return false;
      } else if (pl.patientId !== where.patientId) return false;
    }
    if (where.status !== undefined) {
      const ins = inArr(where.status);
      if (ins) {
        if (!ins.includes(pl.status)) return false;
      } else if (pl.status !== where.status) return false;
    }
    const sn = where.snoozedUntil as { gt?: Date } | undefined;
    if (sn?.gt !== undefined) {
      if (!pl.snoozedUntil || !(pl.snoozedUntil > sn.gt)) return false;
    }
    return true;
  };

  const followupPlan = {
    findFirst: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
      const m = plans.filter((p) => matchPlan(p, where)).sort((a, b) => b.version - a.version);
      return m[0] ?? null;
    }),
    findMany: jest.fn(async ({ where }: { where: Record<string, unknown> }) =>
      plans
        .filter((p) => matchPlan(p, where))
        .sort((a, b) => a.patientId.localeCompare(b.patientId) || b.version - a.version),
    ),
    update: jest.fn(async ({ where, data }: { where: { id: string }; data: Partial<Plan> }) => {
      const p = plans.find((x) => x.id === where.id)!;
      Object.assign(p, data);
      return p;
    }),
    updateMany: jest.fn(async ({ where, data }: { where: { id: { in: string[] } }; data: Partial<Plan> }) => {
      const ids = where.id?.in ?? [];
      let count = 0;
      for (const p of plans)
        if (ids.includes(p.id)) {
          Object.assign(p, data);
          count++;
        }
      return { count };
    }),
    create: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
      const rdata = (data.reasons as { createMany?: { data?: Array<{ scenario: string; subKey?: string }> } })
        ?.createMany?.data ?? [];
      const p: Plan = {
        id: `new-${idc++}`,
        hostId: data.hostId as string,
        tenantId: data.tenantId as string,
        patientId: data.patientId as string,
        version: data.version as number,
        status: data.status as string,
        priorityScore: data.priorityScore as number,
        snoozedUntil: null,
        reasons: rdata.map((r) => ({ scenario: r.scenario, subKey: r.subKey ?? null })),
        assigneeUserId: (data.assigneeUserId as string) ?? null,
        assignedAt: (data.assignedAt as Date) ?? null,
        recycleAt: (data.recycleAt as Date) ?? null,
        contactAttempts: (data.contactAttempts as number) ?? 0,
      };
      plans.push(p);
      return p;
    }),
  };

  const persona = {
    findFirst: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
      const ins = inArr(where.patientId);
      const cand = personas.filter((pe) =>
        ins ? ins.includes(pe.patientId) : pe.patientId === where.patientId,
      );
      return cand[0] ?? null;
    }),
    findMany: jest.fn(async ({ where }: { where: Record<string, unknown> }) => {
      const ins = (inArr(where.patientId) as string[]) ?? [];
      return personas.filter((pe) => ins.includes(pe.patientId));
    }),
  };

  const planGenerationLog = {
    create: jest.fn(async ({ data }: { data: Record<string, unknown> }) => {
      const r = { id: `log-${idc++}`, ...data };
      logs.push(r);
      return r;
    }),
    update: jest.fn(async () => ({})),
    createMany: jest.fn(async ({ data }: { data: Array<Record<string, unknown>> }) => {
      logs.push(...data);
      return { count: data.length };
    }),
  };

  const prisma = {
    followupPlan,
    persona,
    planGenerationLog,
    $transaction: jest.fn(async (cb: (tx: unknown) => Promise<unknown>) =>
      cb({ followupPlan: { update: followupPlan.update, create: followupPlan.create } }),
    ),
  };
  return { prisma, plans, logs };
}

function makeScenario(hits: ScenarioHit[]) {
  return { key: SCEN, selectHits: jest.fn(async () => hits) };
}
function hit(patientId: string, subKey: string, priorityScore = 50): ScenarioHit {
  return {
    patientId,
    patientExternalId: `ext-${patientId}`,
    reason: 'r',
    priorityScore,
    subKey,
    evidence: { factIds: ['f1'] },
  } as ScenarioHit;
}
function engine(prisma: unknown, scenario: unknown) {
  return new PlanEngineService(prisma as never, scenario as never);
}
const NOW = new Date('2026-06-02T00:00:00Z');

describe('runAllForHost 批量路径 — 5 种结局等价', () => {
  test('create:有命中无既有 plan → plansCreated=1 + 新 active plan(reason 正确)', async () => {
    const { prisma, plans } = makeStore({});
    const res = await engine(prisma, makeScenario([hit('pat-a', 'missing_tooth@whole')])).runAllForHost({
      hostId: HOST,
      tenantId: TENANT,
      now: NOW,
    });
    expect(res.plansCreated).toBe(1);
    const active = plans.filter((p) => p.patientId === 'pat-a' && p.status === 'active');
    expect(active).toHaveLength(1);
    expect(active[0]!.reasons.map((r) => r.subKey)).toEqual(['missing_tooth@whole']);
  });

  test('unchanged:既有 active 同 (scenario,subKey) → plansUnchanged=1,不升版本,仅刷分', async () => {
    const { prisma, plans } = makeStore({
      plans: [
        {
          id: 'p-old',
          patientId: 'pat-b',
          version: 1,
          status: 'active',
          priorityScore: 40,
          reasons: [{ scenario: SCEN, subKey: 'missing_tooth@whole' }],
        },
      ],
    });
    const res = await engine(prisma, makeScenario([hit('pat-b', 'missing_tooth@whole', 55)])).runAllForHost({
      hostId: HOST,
      tenantId: TENANT,
      now: NOW,
    });
    expect(res.plansUnchanged).toBe(1);
    expect(res.plansSuperseded).toBe(0);
    expect(plans.filter((p) => p.patientId === 'pat-b')).toHaveLength(1); // 没升版本
    expect(plans.find((p) => p.id === 'p-old')!.priorityScore).toBe(55); // 刷分
  });

  test('superseded:既有 active 不同 subKey → plansSuperseded=1,旧 superseded + 新 v2 active', async () => {
    const { prisma, plans } = makeStore({
      plans: [
        {
          id: 'p-old',
          patientId: 'pat-c',
          version: 1,
          status: 'active',
          reasons: [{ scenario: SCEN, subKey: 'caries@16' }],
        },
      ],
    });
    const res = await engine(prisma, makeScenario([hit('pat-c', 'missing_tooth@36')])).runAllForHost({
      hostId: HOST,
      tenantId: TENANT,
      now: NOW,
    });
    expect(res.plansSuperseded).toBe(1);
    expect(plans.find((p) => p.id === 'p-old')!.status).toBe('superseded');
    const v2 = plans.find((p) => p.patientId === 'pat-c' && p.version === 2);
    expect(v2?.status).toBe('active');
    expect(v2?.reasons.map((r) => r.subKey)).toEqual(['missing_tooth@36']);
  });

  test('suppressed:命中信号在 终态+未到期 抑制集 → plansSuppressed=1,不建新', async () => {
    const { prisma, plans } = makeStore({
      plans: [
        {
          id: 'p-term',
          patientId: 'pat-d',
          version: 1,
          status: 'completed',
          snoozedUntil: new Date('2026-12-01T00:00:00Z'),
          reasons: [{ scenario: SCEN, subKey: 'missing_tooth@whole' }],
        },
      ],
    });
    const res = await engine(prisma, makeScenario([hit('pat-d', 'missing_tooth@whole')])).runAllForHost({
      hostId: HOST,
      tenantId: TENANT,
      now: NOW,
    });
    expect(res.plansSuppressed).toBe(1);
    expect(res.plansCreated).toBe(0);
    expect(plans.filter((p) => p.patientId === 'pat-d' && p.status === 'active')).toHaveLength(0);
  });

  test('stale-close:有 active 但本轮 0 命中 → plansClosed=1', async () => {
    const { prisma, plans } = makeStore({
      plans: [
        {
          id: 'p-stale',
          patientId: 'pat-e',
          version: 1,
          status: 'active',
          reasons: [{ scenario: SCEN, subKey: 'caries@16' }],
        },
      ],
    });
    const res = await engine(prisma, makeScenario([])).runAllForHost({
      hostId: HOST,
      tenantId: TENANT,
      now: NOW,
    });
    expect(res.plansClosed).toBe(1);
    expect(plans.find((p) => p.id === 'p-stale')!.status).toBe('superseded');
  });

  test('混合一批:create+unchanged+superseded+suppressed 并发跑 → 计数正确 + 每患一条 createMany 日志', async () => {
    const { prisma, plans, logs } = makeStore({
      plans: [
        { id: 'u-old', patientId: 'p2', status: 'active', priorityScore: 50, reasons: [{ scenario: SCEN, subKey: 'k@1' }] },
        { id: 's-old', patientId: 'p3', status: 'active', reasons: [{ scenario: SCEN, subKey: 'k@old' }] },
        {
          id: 't-term',
          patientId: 'p4',
          status: 'abandoned',
          snoozedUntil: new Date('2026-12-01T00:00:00Z'),
          reasons: [{ scenario: SCEN, subKey: 'k@4' }],
        },
      ],
    });
    const res = await engine(
      prisma,
      makeScenario([
        hit('p1', 'k@1'), // create
        hit('p2', 'k@1', 50), // unchanged
        hit('p3', 'k@new'), // superseded
        hit('p4', 'k@4'), // suppressed
      ]),
    ).runAllForHost({ hostId: HOST, tenantId: TENANT, now: NOW });

    expect({ c: res.plansCreated, u: res.plansUnchanged, s: res.plansSuperseded, sup: res.plansSuppressed }).toEqual({
      c: 1,
      u: 1,
      s: 1,
      sup: 1,
    });
    expect(res.patientsHit).toBe(4);
    // PlanGenerationLog 走批量 createMany,每患一行(口径不变)
    expect(prisma.planGenerationLog.createMany).toHaveBeenCalled();
    expect(logs.filter((l) => ['p1', 'p2', 'p3', 'p4'].includes(l.patientId as string))).toHaveLength(4);
    // 无既有 active 且在 hitsByPatient 的患者不应被 stale-close 误关
    expect(res.plansClosed).toBe(0);
  });
});
