/**
 * selftest.mjs — gdi-hwpx 스킬 헤드리스 셀프테스트
 *   node selftest.mjs
 *
 * 검사: 엔진 로드 → 픽스처 마크다운 + 번들 양식 빌드 → 출력 .hwpx 의
 *   ① mimetype ② XML 태그 균형 ③ 내용→스타일 연결 정확성(위계 기호별 styleIDRef)
 *   ④ dangling styleIDRef 없음(한컴 "파일 손상" 주원인) ⑤ 표/캡션/자료주 매핑.
 * 엔진 스냅샷을 갱신(refresh)한 뒤 회귀 확인용으로도 사용.
 */
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadEngine } from './loader.mjs';
import { resolveMapping } from './automap.mjs';

const HERE = path.dirname(fileURLToPath(import.meta.url));
let pass = 0, fail = 0;
const ok = (cond, name) => { if (cond) { pass++; console.log('PASS', name); } else { fail++; console.error('FAIL', name); } };

const FIXTURE = [
  '# 제1장 연구개요', '',
  '## 제1절 연구 배경 및 목적', '',
  '### 1. 연구 배경', '',
  '#### 1) 세부 방향', '',
  '■ 킬러 메시지 한 줄 요약임', '',
  '❍ 구체 설명 문장으로 인과와 맥락을 담고 있음', '',
  '- 세부 통계·실행방안을 제시함', '',
  '※ 참고·예외 사항을 표기함', '',
  '[표 2-1] 경북 농지 현황', '',
  '| 구분 | 면적 | 비중 |', '| --- | --- | --- |', '| 노지 | 100 | 80 |', '',
  '자료 : 경상북도, 「2025 농업통계」, 2025.',
].join('\n');

const tmplPath = path.join(HERE, 'templates', 'gdi-default.hwpx');

async function main() {
  ok(fs.existsSync(tmplPath), '번들 양식 templates/gdi-default.hwpx 존재');

  const { MdParser, PresetManager, HwpxBuilder, JSZip } = loadEngine();
  ok(typeof MdParser.parse === 'function' && typeof HwpxBuilder.buildHwpx === 'function', '엔진 3모듈 로드');

  const ir = MdParser.parse(FIXTURE, {});
  ok(ir.patterns.h1 >= 1 && ir.patterns['bullet:■'] >= 1 && ir.patterns.table >= 1, '파싱: 위계·표 패턴 감지');

  const buf = fs.readFileSync(tmplPath);
  const ab = () => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
  const analysis = await PresetManager.analyzeHwpx(ab());

  const overrides = JSON.parse(fs.readFileSync(path.join(HERE, 'mapping.gdi.json'), 'utf8'));
  delete overrides._comment;
  const { mapping } = resolveMapping(analysis.styles, ir.patterns, overrides);
  ok(mapping.h1 === '4' && mapping.h2 === '5' && mapping.h3 === '6' && mapping.h4 === '7', '매핑: 헤딩 위계(장/절/1./1))');
  ok(mapping['bullet:■'] === '8' && mapping['bullet:❍'] === '1' && mapping['bullet:-'] === '2' && mapping['bullet:※'] === '3', '매핑: 글머리(▮/❍/-/※)');
  ok(mapping.tableCell === '11' && mapping['caption-table'] === '10' && mapping.source === '19', '매핑: 표내용/표제목/자료주');

  const blob = await HwpxBuilder.buildHwpx({ ir, analysis, mapping, hwpxArrayBuffer: ab(), imageRegistry: null });
  const out = Buffer.from(await blob.arrayBuffer());
  ok(out.length > 100000, `출력 생성 (${out.length.toLocaleString()} bytes)`);

  const z = await JSZip.loadAsync(out);
  ok((await z.file('mimetype').async('string')) === 'application/hwp+zip', 'mimetype application/hwp+zip');
  const header = await z.file('Contents/header.xml').async('string');
  const sec = await z.file('Contents/section0.xml').async('string');

  // XML 태그 균형
  const cnt = (s, re) => (s.match(re) || []).length;
  ok(cnt(sec, /<hp:p\b/g) === cnt(sec, /<\/hp:p>/g), 'XML: hp:p 여닫이 균형');
  ok(cnt(sec, /<hp:tbl\b/g) === cnt(sec, /<\/hp:tbl>/g), 'XML: hp:tbl 여닫이 균형');
  ok(cnt(sec, /<hp:tc\b/g) === cnt(sec, /<\/hp:tc>/g), 'XML: hp:tc 여닫이 균형');

  // 내용 → styleIDRef 연결 정확성 (해당 텍스트를 담은 hp:p 의 styleIDRef 확인)
  const styleOf = (text) => {
    const i = sec.indexOf(text);
    if (i < 0) return null;
    const open = sec.lastIndexOf('<hp:p ', i);
    const m = /styleIDRef="(\d+)"/.exec(sec.slice(open, i));
    return m ? m[1] : null;
  };
  ok(styleOf('제1장 연구개요') === '4', '연결: "제1장…" → 장(4)');
  ok(styleOf('제1절 연구 배경') === '5', '연결: "제1절…" → 제1절(5)');
  ok(styleOf('1. 연구 배경') === '6' || styleOf('연구 배경') === '6', '연결: "1. 연구 배경" → 1.(6)');
  ok(styleOf('세부 방향') === '7', '연결: "1) 세부 방향" → 1)(7)');
  ok(styleOf('킬러 메시지') === '8', '연결: "■ …" → ▮(8)');
  ok(styleOf('구체 설명') === '1', '연결: "❍ …" → 본문○(1)');
  ok(styleOf('세부 통계') === '2', '연결: "- …" → 본문-(2)');
  ok(styleOf('참고·예외') === '3', '연결: "※ …" → ※(3)');
  ok(styleOf('2025 농업통계') === '19', '연결: "자료 :…" → 자료,주(19)');

  // dangling styleIDRef 없음 — 본문이 참조하는 모든 styleIDRef 가 header 에 존재
  const headerStyleIds = new Set([...header.matchAll(/<hh:style\b[^>]*\bid="(\d+)"/g)].map((m) => m[1]));
  const usedStyleIds = new Set([...sec.matchAll(/styleIDRef="(\d+)"/g)].map((m) => m[1]));
  const dangling = [...usedStyleIds].filter((id) => !headerStyleIds.has(id));
  ok(dangling.length === 0, `dangling styleIDRef 없음 (header 스타일 ${headerStyleIds.size}개, 사용 ${usedStyleIds.size}개)`);

  // dangling charPrIDRef 없음 — 엔진이 동적 추가한 charPr 포함 itemCnt 정합
  const headerCharIds = new Set([...header.matchAll(/<hh:charPr\b[^>]*\bid="(\d+)"/g)].map((m) => m[1]));
  const usedCharIds = new Set([...sec.matchAll(/charPrIDRef="(\d+)"/g)].map((m) => m[1]));
  const danglingChar = [...usedCharIds].filter((id) => !headerCharIds.has(id));
  ok(danglingChar.length === 0, `dangling charPrIDRef 없음 (charPr ${headerCharIds.size}개)`);

  console.log(`\n${fail === 0 ? '✅' : '❌'} ${pass} PASS, ${fail} FAIL`);
  process.exitCode = fail === 0 ? 0 : 1;
}

main().catch((e) => { console.error('셀프테스트 오류:', e); process.exitCode = 1; });
