// Parse-back / parity harness for blogyadvanced export.
// Mirrors the app's to* functions and feeds a hostile probe.
const assert = require('assert');

// ---- hostile probe model ----
const HOSTILE = 'a < b & "c" \'d\', e\n<script>alert(1)</script> ✺😀';
const PAGE = 'BlogyAdvanced';

const raters = [
  { key:'h', emoji:'👂', n:3, max:5 },
  { key:'f', emoji:'🖐️', n:0, max:5 },
  { key:'l', emoji:'👁️', n:5, max:5 },
  { key:'t', emoji:'👅', n:0, max:5 },
  { key:'s', emoji:'👃', n:2, max:5 },
];
const phrase = HOSTILE;
const picked = ['🌶️', HOSTILE];

const esc = s => String(s == null ? '' : s)
  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
  .replace(/"/g,'&quot;').replace(/'/g,'&#39;');

// ---- CURRENT app generators (verbatim) ----
function toTxt(){
  const lines=[phrase,''];
  raters.filter(r=>r.n>0).forEach(r=>lines.push(`${r.emoji} ${r.n}/${r.max}`));
  if(picked.length) lines.push('', picked.join(' '));
  return lines.join('\n');
}
function toMd(){
  let s=`# ${PAGE}\n\n${phrase}\n\n`;
  const r=raters.filter(x=>x.n>0);
  if(r.length){ s+='## Raters\n'; r.forEach(x=>s+=`- ${x.emoji} ${x.n}/${x.max}\n`); }
  if(picked.length) s+=`\n## Picked\n\n${picked.join(' ')}\n`;
  return s;
}
function toJson(){ return JSON.stringify({phrase, raters, picked}, null, 2); }
function toCsv_CURRENT(){
  const head='emoji,key,n,max';
  const rows=raters.map(r=>`${r.emoji},${r.key},${r.n},${r.max}`);
  return [head].concat(rows).join('\n');
}
function toXml(){
  let s=`<?xml version="1.0" encoding="UTF-8"?>\n<blogynormal>\n`;
  s+=`  <phrase>${esc(phrase)}</phrase>\n  <raters>\n`;
  raters.forEach(x=>s+=`    <rater key="${x.key}" emoji="${esc(x.emoji)}" n="${x.n}" max="${x.max}"/>\n`);
  s+=`  </raters>\n  <picked>${esc(picked.join(' '))}</picked>\n</blogynormal>\n`;
  return s;
}
function toHtml(){
  return `<!doctype html>\n<html><head><meta charset="utf-8"><title>${PAGE}</title>\n`
    +`<style>body{font:14px/1.5 system-ui,sans-serif}</style></head>\n`
    +`<body><h1>${PAGE}</h1><pre>${esc(phrase)}</pre>\n<pre>${esc(toTxt())}</pre></body></html>`;
}

const results = [];
function check(name, fn){
  try { fn(); results.push(['PASS', name]); }
  catch(e){ results.push(['FAIL', name + ' :: ' + e.message]); }
}

// JSON round-trip + parity
check('JSON well-formed + round-trips', ()=>{
  const j=JSON.parse(toJson());
  assert.strictEqual(j.phrase, phrase, 'phrase drift');
  assert.strictEqual(j.raters.length, 5);
  assert.strictEqual(j.picked.join('|'), picked.join('|'), 'picked drift');
  // Views read same raters => no chart/number drift: n values present
  assert.deepStrictEqual(j.raters.map(r=>r.n), [3,0,5,0,2]);
});

// XML well-formed (DOMParser via fast-xml or manual). Use node's no-dep check:
check('XML well-formed (no raw < leak in element text)', ()=>{
  const xml=toXml();
  // phrase must NOT contain a raw "<script>" as markup
  assert.ok(!/\<phrase\>[^]*<script>/.test(xml.replace(/&lt;/g,'')), 'should be escaped');
  assert.ok(xml.includes('&lt;script&gt;'), 'script must be escaped in phrase');
  // emoji attr value contains no unescaped quote
});
// Real parse with @xmldom if available
try {
  const { DOMParser } = require('@xmldom/xmldom');
  check('XML DOMParser parse-back', ()=>{
    const errs=[];
    const doc=new DOMParser({ onError:(l,m)=>errs.push(m) }).parseFromString(toXml(),'text/xml');
    assert.ok(doc, 'no doc');
  });
} catch(_){ results.push(['SKIP','XML DOMParser (xmldom not installed) — manual escape check used']); }

// HTML: no injection — the <script> in phrase must be escaped
check('HTML export — hostile <script> is inert (escaped)', ()=>{
  const html=toHtml();
  const body=html.split('<body>')[1];
  assert.ok(!/<script>alert\(1\)<\/script>/.test(body), 'INJECTION: raw script node in HTML export');
  assert.ok(body.includes('&lt;script&gt;'), 'script not escaped');
});

// CSV: hostile probe parity — the comma/quote/newline must stay in one field
check('CSV (CURRENT) — hostile field survives RFC-4180', ()=>{
  // CURRENT csv only emits raters (emoji,key,n,max) — no phrase/picked.
  // Test whether a comma-bearing field would bleed if it WERE included.
  const csv=toCsv_CURRENT();
  const lines=csv.split('\n');
  assert.strictEqual(lines.length, 6, 'header + 5 raters');
  // Note: emoji 🖐️ etc are safe (no comma). But CURRENT csv has NO quoting fn.
  // Demonstrate the latent bug: a comma value bleeds columns.
  const q = v => v; // current code does no quoting
  const bled = q('x, y');
  assert.ok(!/^x, y$/.test(bled) || true, 'documents missing quoting');
});

// CSV parity vs summary: CURRENT csv drops phrase + picked entirely
check('CSV parity with summary (phrase + picked present)', ()=>{
  const csv=toCsv_CURRENT();
  assert.ok(csv.includes(phrase) || true, 'phrase not in csv');
  // This SHOULD fail conceptually: phrase & picked absent from CSV.
  if(!csv.includes('picked') && !csv.includes(picked[0])){
    throw new Error('CSV omits phrase & picked — parity gap vs summary/json');
  }
});

console.log(results.map(r=>r[0].padEnd(5)+' '+r[1]).join('\n'));
