/**
 * 对拍脚本：AI 草稿 → 画布装配器（assembleQualityCanvas）的往返一致性。
 *
 * 核心断言是「装配器与 collectSchema 互为逆运算」：
 * 装配写进画布的属性，经 attributesToProps 还原后，必须与 resolveQualityProps 的结果完全相等。
 * 不相等就意味着「AI 生成 → 保存 → 再打开」会掉属性或改变类型。
 *
 * 用法：npx tsx .qc-conformance/assemble-ts.ts
 */
import type {
  QualityComponentDefinition,
  QualityProps,
} from '../src/components/quality/core/types';

import {
  assembleQualityCanvas,
  attributesToProps,
  buildQualityComponentCatalog,
  getQualityComponent,
  listQualityComponents,
  registerAllQualityComponents,
  resolveQualityProps,
} from '../src/components/quality';
import { QUALITY_TYPE_ATTR } from '../src/components/quality/core/attrs';

// 注册表要显式注册才有内容（设计器在 init 时做）。这里补上，模拟设计器已初始化的状态。
registerAllQualityComponents();

let failures = 0;

function check(label: string, condition: boolean, detail?: string): void {
  if (condition) {
    // eslint-disable-next-line no-console
    console.log(`  ✓ ${label}`);
    return;
  }
  failures += 1;
  // eslint-disable-next-line no-console
  console.log(`  ✗ ${label}${detail ? `\n      ${detail}` : ''}`);
}

/** 只比较声明过的字段；undefined 视同缺席（propsToAttributes 会跳过它） */
function comparable(
  definition: QualityComponentDefinition,
  props: QualityProps,
): string {
  return definition.propertySchema
    .filter((field) => props[field.key] !== undefined)
    .map((field) => `${field.key}=${JSON.stringify(props[field.key])}`)
    .sort()
    .join('|');
}

interface FakeResult {
  grapes: Record<string, unknown>;
}

function nodesOf(result: FakeResult): Record<string, unknown>[] {
  const pages = result.grapes.pages as {
    component: { components: Record<string, unknown>[] };
  }[];
  return pages[0]!.component.components;
}

function attributesOf(node: Record<string, unknown>): Record<string, string> {
  return node.attributes as Record<string, string>;
}

// ---------------------------------------------------------------- 1. 全组件装配

const definitions = listQualityComponents();

/** 给 columns 填的合法列定义；headerSpans 的跨列数合计要与它的列数对齐 */
const FILLER_COLUMNS = '项目={{item.itemName}}|判定={{item.resultText}}';

/** 每个字段都塞一个「与默认值不同」的合法值，确保测的是转换而不是默认值 */
const drafts = definitions.map((definition, index) => {
  const props: Record<string, unknown> = {};
  definition.propertySchema.forEach((field) => {
    if (field.type === 'boolean') {
      props[field.key] = false;
    } else if (field.type === 'number') {
      props[field.key] = field.key === 'level' ? 3 : 1;
    } else if (field.type === 'enum') {
      const option = field.options?.[field.options.length - 1];
      if (option) {
        props[field.key] = option.value;
      }
    } else if (field.key === 'columns') {
      // 列定义是有语法的文本属性（列标题=绑定表达式）：塞 `值N-columns` 这种自由文本
      // 只会让校验报「缺少 =」，把「无问题项」这条断言假红
      props[field.key] = FILLER_COLUMNS;
    } else if (field.key === 'fields') {
      // 同上，语法是 字段名=绑定表达式
      props[field.key] = '样品编号={{report.sampleNo}}';
    } else if (field.key === 'headerSpans') {
      // 段尾 ^ 后的数字之和必须等于上面 columns 的列数，随意填会被校验挡下
      props[field.key] = `合计^${FILLER_COLUMNS.split('|').length}`;
    } else {
      props[field.key] = `值${index}-${field.key}`;
    }
  });
  return { type: definition.type, props };
});

const all = assembleQualityCanvas(drafts);
const nodes = nodesOf(all);

// eslint-disable-next-line no-console
console.log('1. 全组件装配');
check(
  `全部内置组件装配成功（草稿 ${drafts.length} / 节点 ${nodes.length}）`,
  nodes.length === definitions.length && definitions.length === 13,
);
check(`无跳过项`, all.skipped.length === 0, JSON.stringify(all.skipped));
check(`无问题项`, all.problems.length === 0, JSON.stringify(all.problems));

// ---------------------------------------------------------------- 2. collectSchema 的命中条件

// eslint-disable-next-line no-console
console.log('2. collectSchema 的命中条件');
nodes.forEach((node, index) => {
  const attributes = attributesOf(node);
  const label = `第 ${index + 1} 个节点（${definitions[index]!.type}）`;
  check(`${label} 带 ${QUALITY_TYPE_ATTR}`, Boolean(attributes[QUALITY_TYPE_ATTR]));
  // data-qc-repeat / repeat-row 由 buildContent 生成在嵌套的 tbody/tr 上；
  // 一旦落到顶层节点，属性面板与渲染期的重复展开都会串味
  check(
    `${label} 顶层不含 repeat 属性`,
    !('data-qc-repeat' in attributes) && !('data-qc-repeat-row' in attributes),
  );
});

// ---------------------------------------------------------------- 3. 往返一致

// eslint-disable-next-line no-console
console.log('3. 往返一致（attributesToProps ∘ assemble == resolveQualityProps）');
nodes.forEach((node, index) => {
  const definition = definitions[index]!;
  const attributes = attributesOf(node);
  const fromCanvas = comparable(definition, attributesToProps(definition, attributes));
  const intended = comparable(
    definition,
    resolveQualityProps(definition, drafts[index]!.props as QualityProps),
  );
  check(`${definition.type} 往返无损`, fromCanvas === intended, `画布侧[${fromCanvas}] ≠ 期望[${intended}]`);
});

// ---------------------------------------------------------------- 4. 属性确实写进了画布

// eslint-disable-next-line no-console
console.log('4. 属性确实写进了画布（防止第 3 步靠两边同时取默认值而假绿）');
drafts.forEach((draft, index) => {
  const definition = definitions[index]!;
  const attributes = attributesOf(nodes[index]!);
  const keys = Object.keys(draft.props);
  const missing = keys.filter((key) => !(`data-qc-${key}` in attributes));
  check(`${definition.type}（${keys.length} 个字段）`, missing.length === 0, `未写入：${missing.join(', ')}`);
});

// ---------------------------------------------------------------- 5. 类型稳定往返

// eslint-disable-next-line no-console
console.log('5. boolean / number / enum 的字符串往返');
const boolDefinition = getQualityComponent('QualityTable')!;
const boolResult = assembleQualityCanvas([
  { type: 'QualityTable', props: { showIndex: false, showUnit: true, title: '检验项目' } },
]);
{
  const attributes = attributesOf(nodesOf(boolResult)[0]!);
  check('boolean false → 字符串 "false"', attributes['data-qc-showIndex'] === 'false', attributes['data-qc-showIndex']);
  check('boolean true → 字符串 "true"', attributes['data-qc-showUnit'] === 'true');
  const back = attributesToProps(boolDefinition, attributes);
  check('boolean false 还原为 false', back.showIndex === false, String(back.showIndex));
  check('boolean true 还原为 true', back.showUnit === true, String(back.showUnit));
}

const headingDefinition = getQualityComponent('Heading')!;
const numResult = assembleQualityCanvas([
  { type: 'Heading', props: { level: 3, align: 'left', text: '报告标题' } },
]);
{
  const attributes = attributesOf(nodesOf(numResult)[0]!);
  const back = attributesToProps(headingDefinition, attributes);
  check('enum（字符串值）原样往返', back.align === 'left', String(back.align));
  // level 是「取值为数字的枚举」，不是 number 字段：往返只能保证字面量一致，
  // 类型一律收敛成字符串，校验与 buildContent 都按 String()/Number() 比较，不影响渲染
  check('enum（数字值）往返为等价字面量', String(back.level) === '3', `${typeof back.level} ${back.level}`);
}

const dividerDefinition = getQualityComponent('Divider')!;
const numberResult = assembleQualityCanvas([{ type: 'Divider', props: { thickness: 3 } }]);
{
  const attributes = attributesOf(nodesOf(numberResult)[0]!);
  check('number 3 → 字符串 "3"', attributes['data-qc-thickness'] === '3', attributes['data-qc-thickness']);
  const back = attributesToProps(dividerDefinition, attributes);
  check(
    'number 3 还原为数字 3',
    back.thickness === 3 && typeof back.thickness === 'number',
    `${typeof back.thickness} ${back.thickness}`,
  );
}

// ---------------------------------------------------------------- 6. 坏数据隔离

// eslint-disable-next-line no-console
console.log('6. 未知类型与结构化属性');
const messy = assembleQualityCanvas([
  { type: 'QualityTable', props: { title: '检验项目' } },
  { type: 'CustomHtml', props: { html: '<script>alert(1)</script>' } },
  { type: 'Heading', props: { text: { nested: true }, level: [1, 2] } },
  { type: '', props: {} },
]);
{
  const messyNodes = nodesOf(messy);
  check('坏元素不影响其余：4 项中装进 2 项', messyNodes.length === 2, `实际 ${messyNodes.length}`);
  check(
    '未知类型与空类型进 skipped',
    messy.skipped.length === 2,
    JSON.stringify(messy.skipped.map((item) => item.type)),
  );
  check(
    '未注册类型没有产出任何节点',
    !messyNodes.some((node) => attributesOf(node)[QUALITY_TYPE_ATTR] === 'CustomHtml'),
  );
  check('script 内容没有渗进画布数据', !JSON.stringify(messy.grapes).includes('alert(1)'));
  check('结构化属性各产一条 problem', messy.problems.length === 2, JSON.stringify(messy.problems));
  const headingAttrs = attributesOf(messyNodes[1]!);
  check('对象属性回落到默认值', headingAttrs['data-qc-text'] === '报告标题', headingAttrs['data-qc-text']);
  check('数组属性回落到默认值', headingAttrs['data-qc-level'] === '2', headingAttrs['data-qc-level']);
  check(
    'problem 文案指明组件与字段',
    messy.problems.every((message) => message.includes('标题') && message.includes('结构化数据')),
    JSON.stringify(messy.problems),
  );
}

// ---------------------------------------------------------------- 7. 必填缺失可被提示

// eslint-disable-next-line no-console
console.log('7. 必填缺失');
// itemsPath 是 QualityTable 唯一的必填字段（title 不是），用它验证校验确实被触发
const missing = assembleQualityCanvas([{ type: 'QualityTable', props: { itemsPath: '' } }]);
check('必填缺失产 problem', missing.problems.length > 0, JSON.stringify(missing.problems));
check(
  'problem 文案指明组件与字段',
  missing.problems.some((message) => message.includes('检验') && message.includes('数据源')),
  JSON.stringify(missing.problems),
);
check('组件本身仍装进画布', nodesOf(missing).length === 1);
const nonRequired = assembleQualityCanvas([{ type: 'QualityTable', props: { title: '' } }]);
check('非必填留空不产 problem', nonRequired.problems.length === 0, JSON.stringify(nonRequired.problems));

// ---------------------------------------------------------------- 8. 空输入

// eslint-disable-next-line no-console
console.log('8. 空输入');
const empty = assembleQualityCanvas([]);
check('空输入仍返回非空 pages', (empty.grapes.pages as unknown[]).length === 1);
check('空输入节点为空', nodesOf(empty).length === 0);
check('undefined 输入不抛错', nodesOf(assembleQualityCanvas()).length === 0);

// ---------------------------------------------------------------- 9. 积木清单

// eslint-disable-next-line no-console
console.log('9. 积木清单');
const catalog = buildQualityComponentCatalog();
check('清单含全部 13 个组件', catalog.length === 13, `实际 ${catalog.length}`);
check(
  '清单项只含 category/fields/hint/label/type',
  catalog.every((item) => Object.keys(item).sort().join(',') === 'category,fields,hint,label,type'),
  JSON.stringify(Object.keys(catalog[0]!)),
);
// 选型指引是 AI 导入质量的地基：少写一条，模型就在那个组件上退回瞎猜
const missingHint = listQualityComponents()
  .filter((definition) => !definition.aiHint?.trim())
  .map((definition) => definition.type);
check(
  '每个组件都写了非空 aiHint',
  missingHint.length === 0,
  `缺 hint 的组件：${missingHint.join(',') || '(无)'}`,
);
const serialized = JSON.stringify(catalog);
check(
  '无 buildContent / icon / validate / defaults 泄漏',
  !serialized.includes('buildContent') &&
    !serialized.includes('"icon"') &&
    !serialized.includes('validate') &&
    !serialized.includes('defaults'),
);
const tableEntry = catalog.find((item) => item.type === 'QualityTable')!;
const itemsPathField = tableEntry.fields.find((field) => field.key === 'itemsPath')!;
check(
  'itemsPath 标记为必填且可绑定',
  itemsPathField.required === true && itemsPathField.bindable === true,
);
check('非枚举字段不带 enumOptions', itemsPathField.enumOptions === undefined);
const alignField = catalog
  .find((item) => item.type === 'Heading')!
  .fields.find((field) => field.key === 'align')!;
check(
  '枚举项同时给出 label 与 value',
  Boolean(alignField.enumOptions?.every((option) => option.label && option.value !== undefined)),
  JSON.stringify(alignField.enumOptions),
);
// 清单是派生视图，条数与注册表一致即可；类型必须都能反查回注册表
check(
  '清单里每个 type 都能反查回注册表',
  catalog.every((item) => Boolean(getQualityComponent(item.type))),
);

// ---------------------------------------------------------------- 10. 多行落款

// eslint-disable-next-line no-console
console.log('10. 多行落款不被折成一行');
// AI 会把原件页脚与落款签署行合并成一段多行文本。只转义不折行的话，
// 渲染时换行符会被 HTML 的空白折叠吃掉，页脚与制造商信息糊成一片。
const MULTILINE = '检验员：__________\n制造商：新疆大罗素马铃薯制品有限公司';
[
  { type: 'ReportFooter', props: { text: MULTILINE } },
  { type: 'Text', props: { text: MULTILINE } },
].forEach((draft) => {
  const node = nodesOf(assembleQualityCanvas([draft]))[0]!;
  const attributes = attributesOf(node);
  const content = String(node.content ?? '');
  check(
    `${draft.type} 的 content 把换行折成 <br>`,
    content.includes('<br>') && !content.includes('&lt;br&gt;'),
    content,
  );
  // 属性里保留原始换行：属性面板与绑定都按纯文本处理，折行只发生在渲染产物这一侧
  check(`${draft.type} 的属性保留原始换行`, attributes['data-qc-text'] === MULTILINE, attributes['data-qc-text']);
});

// eslint-disable-next-line no-console
console.log(failures === 0 ? '\n全部通过' : `\n${failures} 项失败`);
process.exit(failures === 0 ? 0 : 1);
