/**
 * Transform pipeline operator 单元测试(Layer A.5)
 *
 * 5 operator × 边界 case,确保:
 *   - 形态改造正确
 *   - 边界(空/null/缺字段/无效 JSON)不抛错,跳过 + log
 *   - 输入不可变(不污染上游)
 */
import { TransformEngine } from '../src/modules/sync/transforms/transform-engine';
import { runProject } from '../src/modules/sync/transforms/operators/project.op';
import { runSplitJsonArray } from '../src/modules/sync/transforms/operators/split-json-array.op';
import { runDerive } from '../src/modules/sync/transforms/operators/derive.op';
import { runRouteByPattern } from '../src/modules/sync/transforms/operators/route-by-pattern.op';
import { runPickFirstNonzero } from '../src/modules/sync/transforms/operators/pick-first-nonzero.op';
import { runFilter } from '../src/modules/sync/transforms/operators/filter.op';
import { runLookup } from '../src/modules/sync/transforms/operators/lookup.op';

describe('transforms | project', () => {
  test('keep + rename 选列改名', () => {
    const out = runProject(
      { kind: 'project', input: 'in', output: 'out', keep: ['id', 'name'], rename: { id: 'emr_id' } },
      [{ id: 'a', name: 'X', noise: 'drop' }],
    );
    expect(out).toEqual([{ emr_id: 'a', name: 'X' }]);
  });

  test('keep 不写 = 全保留', () => {
    const out = runProject(
      { kind: 'project', input: 'in', output: 'out' },
      [{ a: 1, b: 2 }],
    );
    expect(out).toEqual([{ a: 1, b: 2 }]);
  });

  test('空输入 → 空输出', () => {
    expect(runProject({ kind: 'project', input: 'in', output: 'out' }, [])).toEqual([]);
  });

  test('keep 中字段缺失 → 输出 undefined(对齐 SQL NULL 语义)', () => {
    const out = runProject(
      { kind: 'project', input: 'in', output: 'out', keep: ['a', 'missing'] },
      [{ a: 1 }],
    );
    expect(out[0]!).toEqual({ a: 1, missing: undefined });
  });
});

describe('transforms | split_json_array', () => {
  test('typical EMR.diag 拆行', () => {
    const { rows, skippedParents, skippedChildren } = runSplitJsonArray(
      {
        kind: 'split_json_array',
        input: 'emr',
        output: 'dx',
        array_field: 'diag',
        parent_keys: { emr_id: 'id', patient_id: 'patient_id' },
        element_fields: { std_code: 'stdCode', tooth_position: 'toothPosition' },
      },
      [
        {
          id: 'emr1',
          patient_id: '100',
          diag: '[{"stdCode":"K05.300","toothPosition":"36"},{"stdCode":"K02.9","toothPosition":"15"}]',
        },
      ],
    );
    expect(rows).toEqual([
      { emr_id: 'emr1', patient_id: '100', std_code: 'K05.300', tooth_position: '36' },
      { emr_id: 'emr1', patient_id: '100', std_code: 'K02.9', tooth_position: '15' },
    ]);
    expect(skippedParents).toBe(0);
    expect(skippedChildren).toBe(0);
  });

  test('array_field 已 parse 数组(非 string)', () => {
    const { rows } = runSplitJsonArray(
      {
        kind: 'split_json_array',
        input: 'emr',
        output: 'dx',
        array_field: 'diag',
        parent_keys: { emr_id: 'id' },
        element_fields: { code: 'code' },
      },
      [{ id: 'e1', diag: [{ code: 'K02' }, { code: 'K08' }] }],
    );
    expect(rows.map((r) => r.code)).toEqual(['K02', 'K08']);
  });

  test('array_field 为 "[]" / null → 跳过该父行,不产子行', () => {
    const { rows, skippedParents } = runSplitJsonArray(
      {
        kind: 'split_json_array',
        input: 'emr',
        output: 'dx',
        array_field: 'diag',
        parent_keys: {},
        element_fields: { code: 'code' },
      },
      [{ id: 'e1', diag: '[]' }, { id: 'e2', diag: null }, { id: 'e3', diag: '{"not": "array"}' }],
    );
    expect(rows).toEqual([]);
    // "[]" 不算 skipped(parse 成功是空数组);null / 非数组 JSON 算 skipped
    expect(skippedParents).toBeGreaterThan(0);
  });

  test('where not_empty 过滤空字段元素', () => {
    const { rows } = runSplitJsonArray(
      {
        kind: 'split_json_array',
        input: 'emr',
        output: 'dx',
        array_field: 'diag',
        parent_keys: { emr_id: 'id' },
        element_fields: { code: 'stdCode' },
        where: { code: { not_empty: true } },
      },
      [{ id: 'e1', diag: '[{"stdCode":""},{"stdCode":"K08"},{"stdCode":null}]' }],
    );
    expect(rows.map((r) => r.code)).toEqual(['K08']);
  });

  test('非对象元素跳过', () => {
    const { rows, skippedChildren } = runSplitJsonArray(
      {
        kind: 'split_json_array',
        input: 'emr',
        output: 'dx',
        array_field: 'arr',
        parent_keys: { id: 'id' },
        element_fields: { v: 'v' },
      },
      [{ id: 'e1', arr: '[1, "string", {"v": "ok"}]' }],
    );
    expect(rows).toHaveLength(1);
    expect(skippedChildren).toBe(2);
  });
});

describe('transforms | derive', () => {
  test('substring K05.300 → K05', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'dx',
        output: 'dx',
        fields: { pac_code: { op: 'substring', from: 'std_code', start: 0, end: 3 } },
      },
      [{ std_code: 'K05.300' }, { std_code: 'K02.9' }, { std_code: null }],
    );
    expect(out.map((r) => r.pac_code)).toEqual(['K05', 'K02', null]);
  });

  test('concat 字段引用 + 字面量', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'dx',
        output: 'dx',
        fields: {
          external_id: { op: 'concat', parts: ['${emr_id}', '|', '${code}', '|', '${tooth}'] },
        },
      },
      [{ emr_id: 'e1', code: 'K08', tooth: '36' }],
    );
    expect(out[0]!.external_id).toBe('e1|K08|36');
  });

  test('concat null/undefined → 空串(不抛错)', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'dx',
        output: 'dx',
        fields: { x: { op: 'concat', parts: ['${a}', '|', '${b}'] } },
      },
      [{ a: 'A', b: null }],
    );
    expect(out[0]!.x).toBe('A|');
  });

  test('default 原值空时填', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'x',
        output: 'x',
        fields: { y: { op: 'default', from: 'src', value: 'fallback' } },
      },
      [{ src: null }, { src: '' }, { src: 'real' }],
    );
    expect(out.map((r) => r.y)).toEqual(['fallback', 'fallback', 'real']);
  });

  test('trim 去首尾空白 + 尾部噪声标点(脏中文名归一)', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'x',
        output: 'x',
        fields: { m: { op: 'trim', from: 'msg' } },
      },
      [
        { msg: ' 牙龈炎' },
        { msg: '牙龈炎。' },
        { msg: '慢性牙周炎；' },
        { msg: '错颌畸形？' },
        { msg: '牙龈炎\r\n' },
        { msg: '牙周炎' },
        { msg: null },
      ],
    );
    expect(out.map((r) => r.m)).toEqual([
      '牙龈炎',
      '牙龈炎',
      '慢性牙周炎',
      '错颌畸形',
      '牙龈炎',
      '牙周炎',
      null,
    ]);
  });

  test('coalesce 取第一个非空字段(stdCode 优先,否则 message)', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'x',
        output: 'x',
        fields: { code_src: { op: 'coalesce', from: ['std_k', 'msg'] } },
      },
      [
        { std_k: 'K08', msg: '后天性牙齿缺失' }, // 有码 → K08
        { std_k: '', msg: '慢性牙周炎' }, //         无码 → 中文名
        { std_k: null, msg: '龋齿' }, //             无码 → 中文名
        { std_k: '', msg: '' }, //                   都空 → null
      ],
    );
    expect(out.map((r) => r.code_src)).toEqual(['K08', '慢性牙周炎', '龋齿', null]);
  });

  test('lower / upper', () => {
    const out = runDerive(
      {
        kind: 'derive',
        input: 'x',
        output: 'x',
        fields: {
          l: { op: 'lower', from: 'v' },
          u: { op: 'upper', from: 'v' },
        },
      },
      [{ v: 'AbC' }],
    );
    expect(out[0]!.l).toBe('abc');
    expect(out[0]!.u).toBe('ABC');
  });
});

describe('transforms | route_by_pattern', () => {
  test('3 路分流(建议→recommendation/暂观→observation/默认→treatment)', () => {
    const { outputs, dropped } = runRouteByPattern(
      {
        kind: 'route_by_pattern',
        input: 'tp',
        field: 'treat_name',
        routes: [
          { output: 'recommendation', when: { starts_with: ['建议', '推荐'] } },
          { output: 'observation', when: { equals: ['暂观', '观察', '无治疗'] } },
          { output: 'treatment', when: { default: true } },
        ],
      },
      [
        { treat_name: '建议种植' },
        { treat_name: '暂观' },
        { treat_name: '充填' },
        { treat_name: '根管治疗' },
        { treat_name: '观察' },
      ],
    );
    expect(outputs.recommendation!.map((r) => r.treat_name)).toEqual(['建议种植']);
    expect(outputs.observation!.map((r) => r.treat_name)).toEqual(['暂观', '观察']);
    expect(outputs.treatment!.map((r) => r.treat_name)).toEqual(['充填', '根管治疗']);
    expect(dropped).toBe(0);
  });

  test('无 default 兜底时 dropped 计数', () => {
    const { outputs, dropped } = runRouteByPattern(
      {
        kind: 'route_by_pattern',
        input: 'tp',
        field: 'name',
        routes: [{ output: 'a', when: { starts_with: ['X'] } }],
      },
      [{ name: 'XYZ' }, { name: 'ABC' }],
    );
    expect(outputs.a).toHaveLength(1);
    expect(dropped).toBe(1);
  });

  test('null 字段当空串处理', () => {
    const { outputs } = runRouteByPattern(
      {
        kind: 'route_by_pattern',
        input: 'tp',
        field: 'name',
        routes: [
          { output: 'empty', when: { equals: [''] } },
          { output: 'rest', when: { default: true } },
        ],
      },
      [{ name: null }, { name: 'X' }],
    );
    expect(outputs.empty).toHaveLength(1);
    expect(outputs.rest).toHaveLength(1);
  });
});

describe('transforms | pick_first_nonzero', () => {
  test('17 列推 channel', () => {
    const out = runPickFirstNonzero(
      {
        kind: 'pick_first_nonzero',
        input: 'pm',
        output: 'pm',
        output_field: 'payment_channel',
        default: 'other',
        candidates: [
          { column: 'pay_wechat', value: 'wechat' },
          { column: 'pay_alipay', value: 'alipay' },
          { column: 'pay_cash', value: 'cash' },
        ],
      },
      [
        { pay_wechat: 100, pay_alipay: 0 },
        { pay_wechat: '0', pay_alipay: '50' },
        { pay_wechat: 0, pay_alipay: 0, pay_cash: 0 },
      ],
    );
    expect(out.map((r) => r.payment_channel)).toEqual(['wechat', 'alipay', 'other']);
  });

  test('字符串数字识别', () => {
    const out = runPickFirstNonzero(
      {
        kind: 'pick_first_nonzero',
        input: 'pm',
        output: 'pm',
        output_field: 'channel',
        default: 'other',
        candidates: [{ column: 'a', value: 'a' }],
      },
      [{ a: '12.50' }, { a: 'NaN' }, { a: null }],
    );
    expect(out.map((r) => r.channel)).toEqual(['a', 'other', 'other']);
  });
});

describe('transforms | filter', () => {
  test('多字段 AND 过滤(not_empty)', () => {
    const { rows, dropped } = runFilter(
      {
        kind: 'filter',
        input: 'in',
        output: 'out',
        where: { in_time: { not_empty: true }, patient_id: { not_empty: true } },
      },
      [
        { in_time: '2026-01-01', patient_id: '100' },
        { in_time: null, patient_id: '101' },
        { in_time: '2026-01-02', patient_id: '' },
        { in_time: '2026-01-03', patient_id: '102' },
      ],
    );
    expect(rows).toHaveLength(2);
    expect(dropped).toBe(2);
  });

  test('in / equals 规则', () => {
    const { rows } = runFilter(
      {
        kind: 'filter',
        input: 'in',
        output: 'out',
        where: { status: { in: ['4', '5', '6'] } },
      },
      [{ status: 4 }, { status: '5' }, { status: 9 }, { status: 6 }],
    );
    expect(rows).toHaveLength(3);
  });
});

// ─────────────────────────────────────────────
// TransformEngine 集成
// ─────────────────────────────────────────────

describe('transforms | lookup', () => {
  const op = {
    kind: 'lookup' as const,
    input: 'patient',
    output: 'patient',
    from: 'contacts',
    left_key: 'id',
    right_key: 'customer_id',
    where: { contacts_tel: { not_empty: true as const } },
    order_by: [
      { field: 'is_default', dir: 'desc' as const },
      { field: 'contacts_type', dir: 'asc' as const },
      { field: 'id', dir: 'asc' as const },
    ],
    select: { phone: 'contacts_tel' },
  };

  test('多候选按 order_by 挑默认号(is_default desc)', () => {
    const { rows, matched, missed } = runLookup(op, [{ id: '1', name: 'A' }], [
      { id: '10', customer_id: '1', contacts_tel: '111', is_default: '0', contacts_type: '1' },
      { id: '11', customer_id: '1', contacts_tel: '222', is_default: '1', contacts_type: '2' },
    ]);
    expect(rows[0]).toEqual({ id: '1', name: 'A', phone: '222' }); // is_default=1 胜出
    expect(matched).toBe(1);
    expect(missed).toBe(0);
  });

  test('平手时按 contacts_type asc(本人=1 优先),再 id asc', () => {
    const { rows } = runLookup(op, [{ id: '1' }], [
      { id: '20', customer_id: '1', contacts_tel: '联系人', is_default: '0', contacts_type: '2' },
      { id: '21', customer_id: '1', contacts_tel: '本人', is_default: '0', contacts_type: '1' },
    ]);
    expect(rows[0]!.phone).toBe('本人');
  });

  test('未命中 → phone 填 null(左连接,行数不变)', () => {
    const { rows, matched, missed } = runLookup(op, [{ id: '99', name: 'Z' }], [
      { id: '10', customer_id: '1', contacts_tel: '111', is_default: '1', contacts_type: '1' },
    ]);
    expect(rows).toEqual([{ id: '99', name: 'Z', phone: null }]);
    expect(matched).toBe(0);
    expect(missed).toBe(1);
  });

  test('where 预过滤:空号码候选被排除', () => {
    const { rows } = runLookup(op, [{ id: '1' }], [
      { id: '10', customer_id: '1', contacts_tel: '', is_default: '1', contacts_type: '1' }, // 空号被 where 剔除
      { id: '11', customer_id: '1', contacts_tel: '333', is_default: '0', contacts_type: '1' },
    ]);
    expect(rows[0]!.phone).toBe('333');
  });

  test('输入不可变(不污染上游 input 行)', () => {
    const input = [{ id: '1', name: 'A' }];
    runLookup(op, input, [{ id: '10', customer_id: '1', contacts_tel: '111', is_default: '1', contacts_type: '1' }]);
    expect(input[0]).toEqual({ id: '1', name: 'A' }); // 原行没被加 phone
  });

  test('引擎集成:from 表缺失 → 全部 null,不抛错', () => {
    const engine = new TransformEngine();
    const result = engine.run({
      tables: { patient: [{ id: '1' }] },
      transforms: [{ ...op, where: undefined, order_by: undefined }],
    });
    expect(result.patient![0]).toMatchObject({ id: '1', phone: null });
  });
});

describe('transforms | __source_row 真原文留底', () => {
  test('TransformEngine 入口给每行打 __source_row=自身', () => {
    const engine = new TransformEngine();
    const original = { id: 'e1', diag: '[{"stdCode":"K08"}]' };
    const result = engine.run({
      tables: { fact_emr: [original] },
      transforms: [
        {
          kind: 'project',
          input: 'fact_emr',
          output: 'emr_rows',
          keep: ['id'],
        },
      ],
    });
    expect(result.emr_rows![0]!.__source_row).toBe(original);
    // project 把 diag 字段剥掉了,但 __source_row 仍指向原行
    expect((result.emr_rows![0]!.__source_row as Record<string, unknown>).diag).toBe(
      '[{"stdCode":"K08"}]',
    );
  });

  test('split_json_array 子行继承父 __source_row(关键场景)', () => {
    const engine = new TransformEngine();
    const emrRow = {
      id: 'emr1',
      patient_id: '100',
      illness_desc: '主诉xxx',
      diag: '[{"stdCode":"K08","toothPosition":"17"},{"stdCode":"K05","toothPosition":""}]',
      treat_plan: '[{"treatName":"种植"}]',
    };
    const result = engine.run({
      tables: { fact_emr: [emrRow] },
      transforms: [
        {
          kind: 'split_json_array',
          input: 'fact_emr',
          output: 'diagnosis_rows',
          array_field: 'diag',
          parent_keys: { emr_id: 'id', patient_id: 'patient_id' },
          element_fields: { std_code: 'stdCode', tooth_position: 'toothPosition' },
        },
      ],
    });
    // 拆出来 2 个诊断行,都指向同一父 EMR
    expect(result.diagnosis_rows).toHaveLength(2);
    for (const dx of result.diagnosis_rows!) {
      // 拆出来的诊断子行,raw_payload 应该是 EMR 完整原文(含 treat_plan / illness_desc)
      expect(dx.__source_row).toBe(emrRow);
      const src = dx.__source_row as Record<string, unknown>;
      expect(src.illness_desc).toBe('主诉xxx');
      expect(src.treat_plan).toBeDefined();
      expect(src.diag).toBeDefined();
    }
  });

  test('derive / route / pick / filter 透传 __source_row(自然 spread)', () => {
    const engine = new TransformEngine();
    const original = { name: '建议种植', amount: 100 };
    const result = engine.run({
      tables: { tp: [original] },
      transforms: [
        {
          kind: 'derive',
          input: 'tp',
          output: 'tp',
          fields: { x: { op: 'concat', parts: ['ok'] } },
        },
        {
          kind: 'route_by_pattern',
          input: 'tp',
          field: 'name',
          routes: [{ output: 'rec', when: { starts_with: ['建议'] } }],
        },
      ],
    });
    expect(result.rec![0]!.__source_row).toBe(original);
  });

  test('caller 提前注入 __source_row 时不覆盖(Push 路径场景)', () => {
    const engine = new TransformEngine();
    const customSource = { remote: 'event-payload' };
    const result = engine.run({
      tables: {
        events: [{ id: 'e1', __source_row: customSource }],
      },
      transforms: [{ kind: 'project', input: 'events', output: 'out' }],
    });
    expect(result.out![0]!.__source_row).toBe(customSource);
  });
});

describe('transforms | TransformEngine 集成', () => {
  test('完整 EMR pipeline:project + split + derive + route', () => {
    const engine = new TransformEngine();
    const result = engine.run({
      tables: {
        fact_emr: [
          {
            id: 'emr1',
            patient_id: '100',
            illness_desc: '主诉',
            diag: '[{"stdCode":"K05.300","toothPosition":"36"}]',
            treat_plan: '[{"treatName":"建议种植","toothPosition":"36"},{"treatName":"龈上洁治","toothPosition":""}]',
          },
        ],
      },
      transforms: [
        // emr 主表去掉 JSON 字段
        {
          kind: 'project',
          input: 'fact_emr',
          output: 'emr_rows',
          keep: ['id', 'patient_id', 'illness_desc'],
          rename: { id: 'emr_id' },
        },
        // diag 拆行 + 截短 K05.300 → K05
        {
          kind: 'split_json_array',
          input: 'fact_emr',
          output: '_diagnosis_raw',
          array_field: 'diag',
          parent_keys: { emr_id: 'id', patient_id: 'patient_id' },
          element_fields: { std_code: 'stdCode', tooth_position: 'toothPosition' },
        },
        {
          kind: 'derive',
          input: '_diagnosis_raw',
          output: 'diagnosis_rows',
          fields: {
            pac_code: { op: 'substring', from: 'std_code', start: 0, end: 3 },
          },
        },
        // treat_plan 拆 + 3 路
        {
          kind: 'split_json_array',
          input: 'fact_emr',
          output: '_treat_plan_raw',
          array_field: 'treat_plan',
          parent_keys: { emr_id: 'id', patient_id: 'patient_id' },
          element_fields: { treat_name: 'treatName', tooth_position: 'toothPosition' },
        },
        {
          kind: 'route_by_pattern',
          input: '_treat_plan_raw',
          field: 'treat_name',
          routes: [
            { output: 'recommendation_rows', when: { starts_with: ['建议', '推荐'] } },
            { output: 'observation_rows', when: { equals: ['暂观', '观察'] } },
            { output: 'treatment_rows', when: { default: true } },
          ],
        },
      ],
    });

    // 注:行还自带 __source_row 隐藏字段(合规留底),用 toMatchObject 跳过它
    expect(result.emr_rows).toHaveLength(1);
    expect(result.emr_rows![0]).toMatchObject({ emr_id: 'emr1', patient_id: '100', illness_desc: '主诉' });
    expect(result.diagnosis_rows).toHaveLength(1);
    expect(result.diagnosis_rows![0]).toMatchObject({
      emr_id: 'emr1', patient_id: '100', std_code: 'K05.300', tooth_position: '36', pac_code: 'K05',
    });
    expect(result.recommendation_rows!.map((r) => r.treat_name)).toEqual(['建议种植']);
    expect(result.observation_rows).toEqual([]);
    expect(result.treatment_rows!.map((r) => r.treat_name)).toEqual(['龈上洁治']);
  });

  test('输入 table 不存在 → 跳过 + warn(不抛)', () => {
    const engine = new TransformEngine();
    const result = engine.run({
      tables: {},
      transforms: [{ kind: 'project', input: 'nonexistent', output: 'out' }],
    });
    expect(result.out).toBeUndefined();
  });

  test('source tables 不被污染', () => {
    const engine = new TransformEngine();
    const src = [{ a: 1, b: 2 }];
    const result = engine.run({
      tables: { src },
      transforms: [
        { kind: 'derive', input: 'src', output: 'src', fields: { c: { op: 'concat', parts: ['ok'] } } },
      ],
    });
    expect(src[0]).toEqual({ a: 1, b: 2 }); // 原对象不变
    expect(result.src![0]).toMatchObject({ a: 1, b: 2, c: 'ok' });
  });
});
