import { Test } from '@nestjs/testing';
import { PERSONA_FEATURE_SPECS, PERSONA_TAG_FILTER_DIMS, PERSONA_FEATURE_META } from '@pac/types';
import {
  FeatureRegistry,
  FEATURE_EXTRACTOR_PROVIDERS,
} from '../src/modules/persona/features/feature.registry';

/**
 * 标签注册表防漂移。
 *
 * 由来:同一个「这个标签怎么算的」历史上有四份 —— PERSONA_FEATURE_SPECS.algorithm、
 * 前端 persona-feature-hover.tsx 的 ALGORITHMS、extractor 顶部 JSDoc、extractor 代码。
 * 前端那份靠人肉同步,已经漂了:
 *   · 权益身份 hover 只列 2 类,extractor 实际产 5 类,还承诺了数据里没有的「保司 + 日期」
 *   · 转介绍达人 hover 写「社交型 = 推外人 ≥3 人」,真实门槛是「推荐 ≥3 人且带来成交」
 * 客服照着 hover 跟患者讲,说明写错比不写更糟。
 *
 * 现在说明收进标签卡(spec.display),前端只渲染不编内容。本文件守住"注册表 ↔ 实现"的一致:
 * 光收拢不设闸,过两个月照样会漂。
 */

/** extractor 都是无依赖纯函数类 → 不用起 PersonaModule(那会拽进 Prisma / ClinicalGap) */
async function registryKeys(): Promise<string[]> {
  const mod = await Test.createTestingModule({
    providers: [FeatureRegistry, ...FEATURE_EXTRACTOR_PROVIDERS],
  }).compile();
  return mod.get(FeatureRegistry).extractors.map((e) => e.key as string);
}

describe('PERSONA_FEATURE_SPECS ↔ FeatureRegistry', () => {
  test('⭐ 每个在跑的 extractor 都必须在注册表里登记(新特征上线不许漏登记)', async () => {
    const missing = (await registryKeys()).filter((k) => !PERSONA_FEATURE_SPECS[k]);
    expect(missing).toEqual([]);
  });

  test('⭐ 注册表里不留已摘除的标签(摘 extractor 时要一并清掉,否则覆盖率日报会永远报 0%)', async () => {
    const live = new Set(await registryKeys());
    const stale = Object.keys(PERSONA_FEATURE_SPECS).filter((k) => !live.has(k));
    expect(stale).toEqual([]);
  });
});

describe('spec.display —— 详情页说明', () => {
  test('⭐ 副标题 + 至少一条取值条目,且每条都带加粗取值', () => {
    // 结构即可读性:卡片靠「取值 → 说明」两列对照来扫。曾试过把自明的标签 rules 清空,
    // 卡片退化成三段平铺文字,没有层次也无从对照 —— 要精简就精简 body,不是砍结构。
    for (const [key, spec] of Object.entries(PERSONA_FEATURE_SPECS)) {
      expect(spec.display?.subtitle?.length ?? 0).toBeGreaterThan(0);
      expect(spec.display?.rules?.length ?? 0).toBeGreaterThan(0);
      for (const r of spec.display.rules) {
        expect(`${key} 缺 label`).toBe(r.label ? `${key} 缺 label` : '');
        expect(r.body.length).toBeGreaterThan(0);
      }
      expect(`${key}:${spec.display.subtitle}`).not.toContain('undefined');
    }
  });

  test('⭐ 说明是给客服看的,不许出现实现术语', () => {
    // 这些词一旦出现,说明写的是"程序怎么实现的"而不是"这个标签什么意思"
    const JARGON = ['extractor', 'fact', 'jsonb', '字段', '接口', '口径与召回完全一致', 'K0', 'data.'];
    const offenders: string[] = [];
    for (const [key, spec] of Object.entries(PERSONA_FEATURE_SPECS)) {
      const text = [spec.display.subtitle,
                    ...spec.display.rules.map((r) => `${r.label}${r.body}`)].join(' ');
      for (const w of JARGON) if (text.includes(w)) offenders.push(`${key}: ${w}`);
    }
    expect(offenders).toEqual([]);
  });

  test('每个标签都有中文展示名(前端 hover 标题取它)', async () => {
    for (const key of await registryKeys()) expect(PERSONA_FEATURE_META[key]).toBeDefined();
  });

  test('⭐ 展示元数据里不留已摘除 / 只在路线图上的 key —— 否则"看着 30 个标签实际只出 16 个"', () => {
    const stale = Object.keys(PERSONA_FEATURE_META).filter((k) => !PERSONA_FEATURE_SPECS[k]);
    expect(stale).toEqual([]);
  });

  test('⭐ 红色是稀缺资源:只有警示类标签能用 rose(客服要扫一眼分轻重)', () => {
    const WARNING = new Set(['urgency_level', 'special_attention', 'contraindication', 'treatment_sensitivity']);
    const wrong = Object.entries(PERSONA_FEATURE_META)
      .filter(([k, m]) => (m.tone === 'rose') !== WARNING.has(k))
      .map(([k]) => k);
    expect(wrong).toEqual([]);
  });
});

describe('PERSONA_TAG_FILTER_DIMS —— 圈人字典', () => {
  test('⭐ 可筛维度的 key 必须是在跑的标签(筛一个不产出的标签 = 永远 0 结果)', async () => {
    const live = new Set(await registryKeys());
    const orphan = PERSONA_TAG_FILTER_DIMS.filter((d) => !live.has(d.key)).map((d) => d.key);
    expect(orphan).toEqual([]);
  });

  test('圈人维度的取值不能为空', () => {
    for (const d of PERSONA_TAG_FILTER_DIMS) {
      expect(d.options.length).toBeGreaterThan(0);
      for (const o of d.options) expect(o.value.length).toBeGreaterThan(0);
    }
  });
});
