import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';

/**
 * 「画像必须按患者取当前版」的**全仓**护栏。
 *
 * 🔴 拦的是一个**静默归零**的写法:按 `pe.id = fp.persona_id AND pe.superseded_at IS NULL`
 * 关联画像看着天经地义,实际上 `followup_plans.persona_id` 指向的是**建 plan 那一刻**的
 * 画像版本,画像一升版本那行就被 supersede,而 plan 的外键不跟着走 —— 条件恒为假,
 * 筛选变成 0 条,**不报错、不告警**,主管只看到"这个格子没人了"。
 *
 * 2026-08-02 本地实测(跑完一次全量 persona 重算之后):
 * ```
 * 池子里还指向"活着的画像版本"的 plan     97 / 2,724
 * 同一个「潜在种植」条件 · 按 persona_id     0 条
 * 同一个「潜在种植」条件 · 按 patient_id   376 条   ← 列表页一直是这个口径
 * ```
 * 两处不一致 = T6a「同源保证」明令要防的「矩阵上有人、点进去列表没有」。
 *
 * ⚠️ 故意**扫整个 src**,不是盯某一个文件:这个写法太顺手,下一个人会在别处再写一遍。
 *   生成出来的 SQL 长什么样另有 `cohort-filter.spec.ts` 逐条断言,那一层比文本匹配强;
 *   这一层只负责"别处也不许冒出来"。
 */

const SRC_ROOT = join(__dirname, '../src');

function walk(dir: string): string[] {
  return readdirSync(dir).flatMap((name) => {
    const p = join(dir, name);
    if (statSync(p).isDirectory()) return walk(p);
    return p.endsWith('.ts') ? [p] : [];
  });
}

/** 只看代码不看注释 —— 注释里正写着这条反面教材,不剥掉护栏会先抓住它自己 */
const stripComments = (s: string) =>
  s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');

describe('圈人取数 —— 画像必须按患者取当前版', () => {
  test('⭐⭐ 全仓 ⛔ 不许出现 `pe.id = fp.persona_id`(升一次画像版本就静默归零)', () => {
    const offenders = walk(SRC_ROOT).filter((f) =>
      /\bpe\.id\s*=\s*fp\.persona_id/.test(stripComments(readFileSync(f, 'utf-8'))),
    );
    expect(offenders.map((f) => f.replace(SRC_ROOT, 'src'))).toEqual([]);
  });

  test('⭐ 人群取数的关联口径收口在 cohort-filter 一处 —— 出确认单与看分布共用', () => {
    // 各写一遍就会出现「属性分布说商保 32 人,提案只有 28 人」,主管当场不信,
    // 而且分不清是哪一边错。
    const proposal = readFileSync(join(SRC_ROOT, 'modules/plan/assignment-proposal.service.ts'), 'utf-8');
    const attrs = readFileSync(join(SRC_ROOT, 'modules/plan/cohort-attributes.service.ts'), 'utf-8');
    expect(proposal).toContain('cohortWhereSql');
    expect(attrs).toContain('cohortWhereSql');
  });
});
