/**
 * FactWriter 时间锚变更检测 — occurredAt/plannedFor/validFrom/validUntil 纳入 supersede 判定
 *
 * 背景 bug:变更检测原来只比 content hash + status,宿主纠正事件时间
 * (如 FRIDAY Mongo 伪 UTC 修复,occurredAt 整体 -8h)时 content 不变 →
 * 重摄永远 evidence_appended,occurred_at 无法更新。
 *
 * 覆盖:
 *   - 回归:content+status+时间锚全同 → evidence_appended / unchanged(幂等语义不变)
 *   - occurredAt 平移(-8h)→ superseded,新版本携带纠正后时间
 *   - plannedFor / null↔值 变化 → superseded(planned 事实的时间锚在 plannedFor)
 *   - stale gate 优先:sourceUpdatedAt 更旧 → stale_skipped,时间锚差异不放行
 *   - 终态版本不动:latest 是 fulfilled + 时间差 → 追新版本,终态行保持原状
 *   - bulkWrite 与 writeDraft 同语义(混合批 + batch 内链式)
 *
 * 跑:pnpm test -- fact-writer-temporal
 */
import { FactKind, FactStatus, FactType } from '@pac/types';
import { FactWriter } from '../src/modules/sync/pipeline/fact-writer.service';
import type { FactDraft } from '../src/modules/sync/pipeline/parsers/parser.interface';

// ─────────────────────────────────────────────
// 忠实内存 mock:findFirst/findMany 真按 where 过滤、orderBy version desc、
// $transaction 直接跑 callback,update/create/createMany/updateMany 真改内存行
// ─────────────────────────────────────────────

interface Row {
  id: string;
  hostId: string;
  tenantId: string;
  sourceUnit: string;
  patientId: string;
  subjectId: string;
  kind: string;
  type: string;
  status: string;
  version: number;
  clinicId: string | null;
  occurredAt: Date | null;
  sourceUpdatedAt: Date | null;
  plannedFor: Date | null;
  validFrom: Date | null;
  validUntil: Date | null;
  title: string | null;
  summary: string | null;
  content: unknown;
  transactionIds: string[];
  supersededAt: Date | null;
}

function makeStore(seed: Partial<Row>[] = []) {
  let nextId = 1;
  const rows: Row[] = [];
  const insert = (data: Record<string, unknown>): Row => {
    const row: Row = {
      id: `fact-${nextId++}`,
      hostId: data.hostId as string,
      tenantId: data.tenantId as string,
      sourceUnit: (data.sourceUnit as string) ?? '',
      patientId: data.patientId as string,
      subjectId: data.subjectId as string,
      kind: data.kind as string,
      type: data.type as string,
      status: data.status as string,
      version: data.version as number,
      clinicId: (data.clinicId as string | null) ?? null,
      occurredAt: (data.occurredAt as Date | null) ?? null,
      sourceUpdatedAt: (data.sourceUpdatedAt as Date | null) ?? null,
      plannedFor: (data.plannedFor as Date | null) ?? null,
      validFrom: (data.validFrom as Date | null) ?? null,
      validUntil: (data.validUntil as Date | null) ?? null,
      title: (data.title as string | null) ?? null,
      summary: (data.summary as string | null) ?? null,
      content: data.content,
      transactionIds: (data.transactionIds as string[]) ?? [],
      supersededAt: null,
    };
    rows.push(row);
    return row;
  };
  for (const s of seed) insert(s as Record<string, unknown>);

  const matches = (row: Row, where: Record<string, unknown>): boolean => {
    for (const [k, v] of Object.entries(where)) {
      const rv = (row as unknown as Record<string, unknown>)[k];
      if (v !== null && typeof v === 'object' && 'in' in (v as object)) {
        if (!(v as { in: unknown[] }).in.includes(rv)) return false;
      } else if (rv !== v) {
        return false;
      }
    }
    return true;
  };

  const model = {
    findFirst: async (q: { where: Record<string, unknown> }) => {
      const hit = rows
        .filter((r) => matches(r, q.where))
        .sort((a, b) => b.version - a.version)[0];
      return hit ? { ...hit } : null;
    },
    findMany: async (q: { where: Record<string, unknown> }) => {
      return rows
        .filter((r) => matches(r, q.where))
        .sort((a, b) => a.subjectId.localeCompare(b.subjectId) || b.version - a.version)
        .map((r) => ({ ...r }));
    },
    update: async (q: { where: { id: string }; data: Record<string, unknown> }) => {
      const row = rows.find((r) => r.id === q.where.id)!;
      Object.assign(row, q.data);
      return { ...row };
    },
    updateMany: async (q: { where: { id: { in: string[] } }; data: Record<string, unknown> }) => {
      for (const r of rows) if (q.where.id.in.includes(r.id)) Object.assign(r, q.data);
    },
    create: async (q: { data: Record<string, unknown> }) => ({ ...insert(q.data) }),
    createMany: async (q: { data: Record<string, unknown>[] }) => {
      for (const d of q.data) insert(d);
    },
  };

  const tx = {
    patientFact: model,
    // bulkWrite 的 evidence append 走 raw SQL(array_append 去重);内存 mock 等价实现
    $executeRaw: async (strings: TemplateStringsArray, ...values: unknown[]) => {
      const [transactionId, factId] = values as [string, string];
      const row = rows.find((r) => r.id === factId);
      if (row && !row.transactionIds.includes(transactionId)) {
        row.transactionIds = [...row.transactionIds, transactionId];
      }
    },
  };

  const prisma = {
    patientFact: model,
    $transaction: async (cb: (t: typeof tx) => Promise<unknown>) => cb(tx),
  };

  return { prisma, rows };
}

// ─────────────────────────────────────────────
// 夹具
// ─────────────────────────────────────────────

const HOST = 'host-1';
const TENANT = 'tenant-1';
const UNIT = 'unit-a';
const PATIENT = 'patient-1';

const T0 = new Date('2026-07-01T10:00:00.000Z');
const T0_MINUS_8H = new Date('2026-07-01T02:00:00.000Z');

/// 库里存的是 zod 校验后的 content(nullableString().default(null) 会补齐 null 字段)
/// — 种子必须用同一形态,否则 hash 恒不等,测不出幂等路径
const VALIDATED_CONTENT = {
  encounter_external_id: 'enc-1',
  doctor_id: null,
  doctor_name: null,
  chief_complaint: null,
  encounter_type: null,
  notes: null,
};

function encounterDraft(overrides: Partial<FactDraft> = {}): FactDraft {
  return {
    subjectId: 'enc-1',
    kind: FactKind.ACTUAL,
    type: FactType.ENCOUNTER_RECORD,
    occurredAt: T0,
    content: { encounter_external_id: 'enc-1' },
    ...overrides,
  };
}

function writer(prisma: unknown): FactWriter {
  return new FactWriter(prisma as never);
}

function seedActive(overrides: Partial<Row> = {}): Partial<Row> {
  return {
    hostId: HOST,
    tenantId: TENANT,
    sourceUnit: UNIT,
    patientId: PATIENT,
    subjectId: 'enc-1',
    kind: FactKind.ACTUAL,
    type: FactType.ENCOUNTER_RECORD,
    status: FactStatus.ACTIVE,
    version: 1,
    occurredAt: T0,
    content: { ...VALIDATED_CONTENT },
    transactionIds: ['tx-1'],
    ...overrides,
  };
}

const baseInput = {
  hostId: HOST,
  tenantId: TENANT,
  sourceUnit: UNIT,
  patientId: PATIENT,
};

// ─────────────────────────────────────────────
// writeDraft
// ─────────────────────────────────────────────

describe('FactWriter.writeDraft | 时间锚变更检测', () => {
  test('回归:content+status+时间锚全同 → evidence_appended(新 tx)/ unchanged(同 tx)', async () => {
    const { prisma, rows } = makeStore([seedActive()]);
    const w = writer(prisma);

    const r1 = await w.writeDraft({ ...baseInput, draft: encounterDraft(), transactionId: 'tx-2' });
    expect(r1.action).toBe('evidence_appended');
    expect(rows).toHaveLength(1);
    expect(rows[0]!.transactionIds).toEqual(['tx-1', 'tx-2']);

    const r2 = await w.writeDraft({ ...baseInput, draft: encounterDraft(), transactionId: 'tx-2' });
    expect(r2.action).toBe('unchanged');
    expect(rows).toHaveLength(1);
  });

  test('occurredAt 平移 -8h(content 不变)→ superseded,新版本带纠正后时间', async () => {
    const { prisma, rows } = makeStore([seedActive()]);
    const w = writer(prisma);

    const r = await w.writeDraft({
      ...baseInput,
      draft: encounterDraft({ occurredAt: T0_MINUS_8H }),
      transactionId: 'tx-2',
    });

    expect(r.action).toBe('superseded');
    expect(r.version).toBe(2);
    expect(rows).toHaveLength(2);
    const [old, next] = rows;
    expect(old!.status).toBe(FactStatus.SUPERSEDED);
    expect(old!.supersededAt).toBeInstanceOf(Date);
    expect(next!.status).toBe(FactStatus.ACTIVE);
    expect(next!.occurredAt?.getTime()).toBe(T0_MINUS_8H.getTime());
  });

  test('plannedFor 变化(occurredAt 双 null)→ superseded — planned 事实的时间锚', async () => {
    const { prisma, rows } = makeStore([
      seedActive({ occurredAt: null, plannedFor: T0 }),
    ]);
    const w = writer(prisma);

    const r = await w.writeDraft({
      ...baseInput,
      draft: encounterDraft({ occurredAt: null, plannedFor: T0_MINUS_8H }),
      transactionId: 'tx-2',
    });

    expect(r.action).toBe('superseded');
    expect(rows[1]!.plannedFor?.getTime()).toBe(T0_MINUS_8H.getTime());
  });

  test('occurredAt null → 值 / 值 → null 都算变化 → superseded', async () => {
    const s1 = makeStore([seedActive({ occurredAt: null })]);
    const r1 = await writer(s1.prisma).writeDraft({
      ...baseInput,
      draft: encounterDraft({ occurredAt: T0 }),
      transactionId: 'tx-2',
    });
    expect(r1.action).toBe('superseded');

    const s2 = makeStore([seedActive({ occurredAt: T0 })]);
    const r2 = await writer(s2.prisma).writeDraft({
      ...baseInput,
      draft: encounterDraft({ occurredAt: null }),
      transactionId: 'tx-2',
    });
    expect(r2.action).toBe('superseded');
  });

  test('stale gate 优先:sourceUpdatedAt 更旧 → stale_skipped,时间锚差异不放行', async () => {
    const { prisma, rows } = makeStore([
      seedActive({ sourceUpdatedAt: new Date('2026-07-02T00:00:00Z') }),
    ]);
    const r = await writer(prisma).writeDraft({
      ...baseInput,
      draft: encounterDraft({ occurredAt: T0_MINUS_8H }),
      transactionId: 'tx-2',
      sourceUpdatedAt: new Date('2026-07-01T00:00:00Z'),
    });
    expect(r.action).toBe('stale_skipped');
    expect(rows).toHaveLength(1);
    expect(rows[0]!.occurredAt?.getTime()).toBe(T0.getTime());
  });

  test('终态 latest(fulfilled)+ 时间差 → 追新版本,终态行不动', async () => {
    const { prisma, rows } = makeStore([
      seedActive({ status: FactStatus.FULFILLED }),
    ]);
    const r = await writer(prisma).writeDraft({
      ...baseInput,
      draft: encounterDraft({ status: FactStatus.FULFILLED, occurredAt: T0_MINUS_8H }),
      transactionId: 'tx-2',
    });
    expect(r.action).toBe('superseded');
    expect(rows[0]!.status).toBe(FactStatus.FULFILLED); // 终态版本保持原状
    expect(rows[1]!.version).toBe(2);
    expect(rows[1]!.occurredAt?.getTime()).toBe(T0_MINUS_8H.getTime());
  });
});

// ─────────────────────────────────────────────
// bulkWrite — 与 writeDraft 同语义
// ─────────────────────────────────────────────

describe('FactWriter.bulkWrite | 时间锚变更检测', () => {
  test('混合批:全同 → evidence_appended;occurredAt 平移 → superseded', async () => {
    const { prisma, rows } = makeStore([
      seedActive({ subjectId: 'enc-1' }),
      seedActive({ subjectId: 'enc-2', version: 3 }),
    ]);
    const w = writer(prisma);

    const { results } = await w.bulkWrite([
      { ...baseInput, draft: encounterDraft({ subjectId: 'enc-1', content: { encounter_external_id: 'enc-1' } }), transactionId: 'tx-9' },
      { ...baseInput, draft: encounterDraft({ subjectId: 'enc-2', content: { encounter_external_id: 'enc-1' }, occurredAt: T0_MINUS_8H }), transactionId: 'tx-9' },
    ]);

    expect(results.map((r) => r.action)).toEqual(['evidence_appended', 'superseded']);

    const enc1 = rows.filter((r) => r.subjectId === 'enc-1');
    expect(enc1).toHaveLength(1);
    expect(enc1[0]!.transactionIds).toContain('tx-9');

    const enc2 = rows.filter((r) => r.subjectId === 'enc-2').sort((a, b) => a.version - b.version);
    expect(enc2).toHaveLength(2);
    expect(enc2[0]!.status).toBe(FactStatus.SUPERSEDED);
    expect(enc2[1]!.version).toBe(4);
    expect(enc2[1]!.occurredAt?.getTime()).toBe(T0_MINUS_8H.getTime());
  });

  test('batch 内链式:同 subject 两 draft 仅时间不同 → 连升两版', async () => {
    const { prisma, rows } = makeStore([seedActive()]);
    const w = writer(prisma);

    const { results } = await w.bulkWrite([
      { ...baseInput, draft: encounterDraft({ occurredAt: T0_MINUS_8H }), transactionId: 'tx-2' },
      { ...baseInput, draft: encounterDraft({ occurredAt: new Date('2026-07-01T03:00:00Z') }), transactionId: 'tx-3' },
    ]);

    expect(results.map((r) => r.action)).toEqual(['superseded', 'superseded']);
    expect(results.map((r) => r.version)).toEqual([2, 3]);
    expect(rows).toHaveLength(3);
  });

  test('bulk stale gate 优先于时间锚差异', async () => {
    const { prisma, rows } = makeStore([
      seedActive({ sourceUpdatedAt: new Date('2026-07-02T00:00:00Z') }),
    ]);
    const { results } = await writer(prisma).bulkWrite([
      {
        ...baseInput,
        draft: encounterDraft({ occurredAt: T0_MINUS_8H }),
        transactionId: 'tx-2',
        sourceUpdatedAt: new Date('2026-07-01T00:00:00Z'),
      },
    ]);
    expect(results[0]!.action).toBe('stale_skipped');
    expect(rows).toHaveLength(1);
  });
});

// ─────────────────────────────────────────────
// bulkWrite — schema 违规隔离(2026-07-29 线上事故回归)
// ─────────────────────────────────────────────

/**
 * 事故复盘:FRIDAY 推的结算里有 4 行「实收为负」的红冲单,manifest 当时没把它归到退费,
 * 落进 payment_record 后 amount_cents<0 被 zod 拒 —— 而当时 bulkWrite 是**整批抛错**,
 * ParserPipeline 因此降级成每 fact 一个事务,单批 200 行从 3 秒涨到 63.5 秒,
 * 超过宿主 60s 读超时 → 对方永远收不到成功回执、无限重推同一批,卡了 13 小时一条没进。
 *
 * 这里锁住修复后的语义:违规的那条被隔离进 rejected,**同批其余照常写**,批不抛错。
 */
describe('FactWriter.bulkWrite | schema 违规隔离', () => {
  const paymentDraft = (subjectId: string, amountCents: number): FactDraft => ({
    subjectId,
    kind: FactKind.ACTUAL,
    type: FactType.PAYMENT_RECORD,
    occurredAt: T0,
    content: { payment_external_id: subjectId, amount_cents: amountCents },
  });

  test('一条负金额不连坐 —— 其余照常写入,违规条目进 rejected', async () => {
    const { prisma, rows } = makeStore();
    const { results, rejected } = await writer(prisma).bulkWrite([
      { ...baseInput, draft: paymentDraft('pay-ok-1', 12_000), transactionId: 'tx-1' },
      { ...baseInput, draft: paymentDraft('pay-bad', -50_000), transactionId: 'tx-2' },
      { ...baseInput, draft: paymentDraft('pay-ok-2', 6_000), transactionId: 'tx-3' },
    ]);

    // 好行照常落库,批没有抛错
    expect(results.map((r) => r.action)).toEqual(['created', 'created']);
    expect(rows.map((r) => r.subjectId).sort()).toEqual(['pay-ok-1', 'pay-ok-2']);

    // 坏行被隔离,且带够宿主定位所需的信息
    expect(rejected).toHaveLength(1);
    expect(rejected[0]!.subjectId).toBe('pay-bad');
    expect(rejected[0]!.type).toBe(FactType.PAYMENT_RECORD);
    expect(rejected[0]!.transactionId).toBe('tx-2');
    expect(rejected[0]!.issues.join(' ')).toContain('amount_cents');
    // 不回传 content 原文(患者信息不随回执外流)
    expect(JSON.stringify(rejected[0])).not.toContain('payment_external_id');
  });

  test('整批全违规 → 返回空 results + 全部 rejected,仍不抛错', async () => {
    const { prisma, rows } = makeStore();
    const { results, rejected } = await writer(prisma).bulkWrite([
      { ...baseInput, draft: paymentDraft('bad-1', -1), transactionId: 'tx-1' },
      { ...baseInput, draft: paymentDraft('bad-2', -2), transactionId: 'tx-2' },
    ]);
    expect(results).toHaveLength(0);
    expect(rejected).toHaveLength(2);
    expect(rows).toHaveLength(0);
  });

  test('全部合法 → rejected 为空(回归:不改动正常路径)', async () => {
    const { prisma } = makeStore();
    const { results, rejected } = await writer(prisma).bulkWrite([
      { ...baseInput, draft: paymentDraft('pay-1', 100), transactionId: 'tx-1' },
    ]);
    expect(results.map((r) => r.action)).toEqual(['created']);
    expect(rejected).toEqual([]);
  });
});
