import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import * as yaml from 'js-yaml';
import { CanonicalResourceMeta, PatientReturnVisitCanonicalSchema } from '@pac/types';

/**
 * 回访执行客服(patient_return_visits.task_director_id / _name)契约回归。
 *
 * 【为什么要这个字段】主管指派工单要选人,而 host **不提供「客服归属诊所」字段** ——
 * 只能按行为反推「该诊所近 N 月有过回访记录的客服」。DW fact_returnvisit_out 一直带
 * task_director_id/name,PAC 侧此前没映射,花名册只能靠离线快照 data/jvs-dw/users.json
 * (已陈旧且无刷新机制)。
 *
 * 【最容易搞错的一点】它**不是**患者的"专属客服":
 *   - patients.preferences.dedicatedCs ← fact_client_out.current_task_director(患者挂在谁名下)
 *   - patient_return_visits.task_director_* ← 本次回访是谁做的
 *   两者可以是不同人,口径也差 2.7 倍(回访表 5,110 个 distinct vs 患者表 1,865 个)。
 *   谁要是把两处合并成一列,名册就会少掉一大半人 —— 故这组测试显式锁住"各存各的"。
 */

const RV_YAML = join(__dirname, '../data/jvs-dw/assemblers/patient_return_visit.yaml');
const MANIFEST = join(__dirname, '../data/jvs-dw/manifest.yaml');

describe('canonical schema', () => {
  test('⭐ host 侧数字 id → 字符串(PAC 的 external 标识一律字符串,同 externalId 口径)', () => {
    const out = PatientReturnVisitCanonicalSchema.parse({
      externalId: 'rv1',
      patientExternalId: 'p1',
      taskDirectorId: 5679, // DW 是 Int64
      taskDirectorName: '李欣',
    });
    expect(out.taskDirectorId).toBe('5679');
    expect(out.taskDirectorName).toBe('李欣');
  });

  test('宿主侧时间被识别为 datetime 字段(要走无时区补 offset 的归一)', () => {
    const meta = CanonicalResourceMeta.patient_return_visit;
    expect(meta.datetimeFields).toContain('sourceCreatedAt');
    expect(meta.datetimeFields).toContain('sourceUpdatedAt');
    // taskDate 是 @db.Date(纯日期),补 offset 会跨日 —— 刻意不进这个列表
    expect(meta.datetimeFields).not.toContain('taskDate');
  });

  test('未提供 → undefined(不是空串);不因缺这两列而拒收整条回访', () => {
    const out = PatientReturnVisitCanonicalSchema.parse({
      externalId: 'rv2',
      patientExternalId: 'p2',
    });
    expect(out.taskDirectorId).toBeUndefined();
    expect(out.externalId).toBe('rv2'); // 主数据完好
  });
});

describe('yaml 契约', () => {
  const rv = yaml.load(readFileSync(RV_YAML, 'utf-8')) as {
    canonical: string;
    field_mapping: Record<string, string>;
  };

  test('assembler 映射到 host 的 task_director_id / task_director', () => {
    expect(rv.canonical).toBe('patient_return_visit');
    expect(rv.field_mapping.taskDirectorId).toBe('task_director_id');
    expect(rv.field_mapping.taskDirectorName).toBe('task_director');
    expect(rv.field_mapping.sourceCreatedAt).toBe('created_date');
    expect(rv.field_mapping.sourceUpdatedAt).toBe('updated_date');
  });

  test('⭐ 源 query 必须 SELECT 这两列 —— 漏了则映射静默失效(canonical 无此键 → 落 null)', () => {
    const manifest = yaml.load(readFileSync(MANIFEST, 'utf-8')) as {
      sql_source?: { queries?: Record<string, string> };
    };
    const sql = manifest.sql_source?.queries?.['fact_returnvisit_out'] ?? '';
    expect(sql).toMatch(/\btask_director_id\b/);
    expect(sql).toMatch(/\btask_director\b/);
    expect(sql).toMatch(/\bcreated_date\b/);
    expect(sql).toMatch(/\bupdated_date\b/);
  });

  test('⭐ 患者主档的「专属客服」仍走自己的字段,没被本次改动挪用', () => {
    const p = yaml.load(
      readFileSync(join(__dirname, '../data/jvs-dw/assemblers/patient.yaml'), 'utf-8'),
    ) as { field_mapping: Record<string, string> };
    expect(p.field_mapping.dedicatedCsName).toBe('current_task_director');
    expect(p.field_mapping.dedicatedCsId).toBe('current_task_director_id');
    // 两处映射的 host 列不同 —— 合并会让名册少掉 2/3 的人
    expect(p.field_mapping.dedicatedCsId).not.toBe(rv.field_mapping.taskDirectorId);
  });
});

describe('schema.prisma', () => {
  const schema = readFileSync(join(__dirname, '../prisma/schema.prisma'), 'utf-8');
  const model = schema.slice(
    schema.indexOf('model PatientReturnVisit'),
    schema.indexOf('@@map("patient_return_visits")'),
  );

  test('两列都是可空(存量 166 万行加列不重写表,回填前保持 NULL)', () => {
    expect(model).toMatch(/taskDirectorId\s+String\?\s+@map\("task_director_id"\)/);
    expect(model).toMatch(/taskDirectorName\s+String\?\s+@map\("task_director_name"\)/);
    expect(model).toMatch(/sourceCreatedAt\s+DateTime\?\s+@map\("source_created_at"\)/);
    expect(model).toMatch(/sourceUpdatedAt\s+DateTime\?\s+@map\("source_updated_at"\)/);
  });

  test('⭐ 宿主时间没有占用 PAC 自己的入库时间列(那两个重摄会被刷新)', () => {
    expect(model).toMatch(/createdAt\s+DateTime\s+@default\(now\(\)\)/);
    expect(model).toMatch(/updatedAt\s+DateTime\s+@updatedAt/);
  });

  test('⭐ 名册查询的复合索引存在 —— 单列索引不够(clinic 基数仅 64,选择性差)', () => {
    expect(model).toMatch(/@@index\(\[hostId, tenantId, clinicId, taskDirectorId, taskDate/);
  });
});
