'use strict';
/*
 * Phase 0 jsdom-каркас «Химия 7»: проверяем, что хаб и 4 главы реально
 * выполняются на движке chem8_engine.js без ошибок скриптов, строится
 * para-selector с нужным числом карточек, активен первый §, заглушки-builder'ы
 * рисуют para-hero и кнопку прочтения, а финал-боссы хаба решаются.
 * Содержание § наполняется в фазах 1–4 — здесь проверяется только каркас.
 */
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { JSDOM, VirtualConsole } = require('jsdom');

const ROOT = path.join(__dirname, '..', '..');
const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8');
const wait = ms => new Promise(r => setTimeout(r, ms));

/* Инлайним внешние скрипты главы (CDN убираем, api/xp заменяем заглушками). */
function buildPage(file) {
  let html = readF('frontend/textbooks/' + file);
  const inl = {
    '/js/biochem-core.js': readF('frontend/js/biochem-core.js'),
    '/js/chem8_svg.js': readF('frontend/js/chem8_svg.js'),
    '/js/chem7_svg.js': readF('frontend/js/chem7_svg.js'),
    '/js/chem7_anim.js': readF('frontend/js/chem7_anim.js'),
    '/js/chem7_ch1_widgets.js': readF('frontend/js/chem7_ch1_widgets.js'),
    '/js/chem7_ch2_widgets.js': readF('frontend/js/chem7_ch2_widgets.js'),
    '/js/chem7_ch3_widgets.js': readF('frontend/js/chem7_ch3_widgets.js'),
    '/js/chem7_ch4_widgets.js': readF('frontend/js/chem7_ch4_widgets.js'),
    '/js/chem8_engine.js': readF('frontend/js/chem8_engine.js')
  };
  html = html
    .replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
    .replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
    .replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
  Object.keys(inl).forEach(src => {
    html = html.replace(new RegExp('<script src="' + src + '" defer><\\/script>'), () => '<script>\n' + inl[src] + '\n</script>');
  });
  return html;
}

async function loadDom(file) {
  const errors = [];
  const vc = new VirtualConsole();
  vc.on('jsdomError', e => errors.push(e.message));
  const dom = new JSDOM(buildPage(file), {
    runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
    beforeParse(w) { w.scrollTo = function () {}; }
  });
  await wait(160);
  return { dom, errors, doc: dom.window.document };
}

const CHAPTERS = [
  { file: 'chemistry_7_ch1.html', cards: 15, first: 'sec-p1' },
  { file: 'chemistry_7_ch2.html', cards: 8,  first: 'sec-p13' },
  { file: 'chemistry_7_ch3.html', cards: 9,  first: 'sec-p18' },
  { file: 'chemistry_7_ch4.html', cards: 7,  first: 'sec-p23' }
];

for (const ch of CHAPTERS) {
  test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен ${ch.first}`, async () => {
    const { doc, errors } = await loadDom(ch.file);
    assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
    assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, ch.cards, ch.cards + ' карточек');
    const active = doc.querySelector('.sec.active');
    assert.ok(active && active.id === ch.first, 'активен ' + ch.first);
    const firstId = ch.first.replace('sec-', '');
    assert.ok(doc.querySelector('#' + firstId + '-body .para-hero'), 'para-hero первого §');
    assert.ok(doc.querySelector('#' + firstId + '-body .read-wrap'), 'кнопка прочтения первого §');
  });
}

test('ch1 Волна 1: интерактивы §1–§3 + ПР1 монтируются без ошибок', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  // §1 строится при загрузке (первый §) — классификатор «тело/вещество»
  assert.ok(doc.querySelector('#p1-cls .c7-chip'), 'классификатор §1');
  doc.defaultView.goTo('p2'); await wait(100);
  assert.ok(doc.querySelector('#p2-sep .c7-m'), 'разделитель смесей §2');
  doc.defaultView.goTo('pr1'); await wait(100);
  assert.ok(doc.querySelector('#pr1-sep .c7-m'), 'разделитель смесей ПР1');
  doc.defaultView.goTo('p3'); await wait(100);
  assert.ok(doc.querySelectorAll('#p3-el .el-cell').length > 10, 'каталог элементов §3');
  assert.ok(doc.querySelector('#p3-drill .c7-d'), 'тренажёр символов §3');
  assert.ok(doc.querySelectorAll('#navDotsp3 .nav-dot').length >= 4, 'тренажёр задач §3');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  doc.defaultView.goTo('p4'); await wait(100);
  assert.ok(doc.querySelector('#p4-bal #p4-a'), 'весы атомов §4');
  doc.defaultView.goTo('p5'); await wait(100);
  assert.ok(doc.querySelector('#p5-gal svg'), 'галерея молекул §5');
  doc.defaultView.goTo('p6'); await wait(100);
  assert.ok(doc.querySelector('#p6-cls .c7-chip'), 'классификатор простое/сложное §6');
  assert.ok(doc.querySelector('#p6-gal svg'), 'галерея сложных веществ §6');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 V-пилот: 3D-молекулы §5/§6 + анимация разделения §2', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  doc.defaultView.goTo('p5'); await wait(120);
  assert.ok(doc.querySelector('#p5-gal .mv-b'), 'переключатель молекул §5');
  assert.ok(doc.querySelector('#p5-gal-stage svg circle'), '3D-молекула §5 (SVG)');
  doc.defaultView.goTo('p6'); await wait(120);
  assert.ok(doc.querySelector('#p6-gal-stage svg circle'), '3D-молекула §6 (SVG)');
  doc.defaultView.goTo('p2'); await wait(120);
  const btn = [...doc.querySelectorAll('#p2-sep .c7-m')].find(b => b.dataset.m === 'Фильтрование');
  assert.ok(btn, 'кнопка верного метода §2 найдена');
  btn.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(50);
  assert.ok(doc.querySelector('#p2-sep-anim canvas'), 'сцена разделения §2 (canvas)');
  // §10: анимация признаков реакции после «Провести опыт»
  doc.defaultView.goTo('p10'); await wait(120);
  doc.getElementById('p10-signs-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40);
  assert.ok(doc.querySelector('#p10-signs-stage div'), 'анимация признаков реакции §10');
  // §11: осадок появляется при «Смешать»
  doc.defaultView.goTo('p11'); await wait(120);
  doc.getElementById('p11-mix').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40);
  assert.ok(doc.querySelector('#p11-stage div'), 'анимация осадка §11');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 3: интерактивы §7–§9 монтируются и считают', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  doc.defaultView.goTo('p7'); await wait(100);
  assert.ok(doc.querySelector('#p7-out'), 'парсер формулы §7');
  assert.match(doc.querySelector('#p7-out').textContent, /4/, 'H2SO4 → 4 атома O в разборе');
  doc.defaultView.goTo('p8'); await wait(100);
  assert.match(doc.querySelector('#p8-out').textContent, /100/, 'M_r(CaCO3)=100');
  doc.defaultView.goTo('p9'); await wait(100);
  assert.ok(doc.querySelector('#p9-bld #p9-a'), 'конструктор валентности §9');
  assert.ok(doc.querySelector('#p9-vis svg circle'), 'схема валентных связей §9');
  assert.match(doc.querySelector('#p9-bout').textContent, /Al/, 'формула по валентности построена');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 4: §10–§12 + ЛО1 + финал главы монтируются', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  doc.defaultView.goTo('p10'); await wait(100);
  assert.ok(doc.querySelector('#p10-signs #p10-signs-go'), 'детектор признаков §10');
  doc.defaultView.goTo('lo1'); await wait(100);
  assert.ok(doc.querySelector('#lo1-signs #lo1-signs-go'), 'детектор признаков ЛО1');
  doc.defaultView.goTo('p11'); await wait(100);
  assert.ok(doc.querySelector('#p11-bal svg'), 'весы сохранения массы §11');
  doc.defaultView.goTo('p12'); await wait(120);
  assert.ok(doc.querySelector('#p12-mount').childElementCount > 0, 'балансировщик §12');
  assert.ok(doc.querySelector('#p12-tally .c7-atom'), 'подсчёт атомов §12 (летящие атомы)');
  doc.defaultView.goTo('final1'); await wait(120);
  assert.ok(doc.querySelectorAll('#navDotsfinal1 .nav-dot').length >= 6, 'боссы финала главы');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1: переход к §9 и финалу строит без ошибок', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch1.html');
  doc.defaultView.goTo('p9'); await wait(80);
  assert.ok(doc.querySelector('#p9-body .para-hero'), 'para-hero §9');
  doc.defaultView.goTo('final1'); await wait(80);
  assert.ok(doc.querySelector('#final1-body .para-hero'), 'para-hero финала');
  assert.equal(doc.querySelector('#final1-body .read-wrap'), null, 'у финала нет кнопки прочтения');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch2 Волна 1: интерактивы §13 + ЛО2 + §14 + §15 монтируются', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch2.html');
  assert.ok(doc.querySelector('#p13-air .air-seg'), 'диаграмма состава воздуха §13');
  doc.defaultView.goTo('lo2'); await wait(100);
  assert.ok(doc.querySelector('#lo2-coll #lo2-pick'), 'выбор собирания газа ЛО2');
  doc.defaultView.goTo('p14'); await wait(100);
  assert.ok(doc.querySelector('#p14-tog #p14-o2'), 'переключатель элемент/вещество §14');
  doc.defaultView.goTo('p15'); await wait(100);
  assert.ok(doc.querySelector('#p15-burn #p15-go'), 'симулятор горения §15');
  doc.defaultView.goTo('p15'); doc.getElementById('p15-go').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
  assert.match(doc.querySelector('#p15-out').textContent, /оксид/, 'горение даёт оксид');
  assert.ok(doc.querySelector('#p15-stage div'), 'анимация пламени §15');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch2 Волна 2: §16 + §17 + ПР2 + финал главы монтируются', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch2.html');
  doc.defaultView.goTo('p16'); await wait(100);
  assert.ok(doc.querySelector('#p16-bld #p16-el'), 'конструктор оксида §16');
  assert.ok(doc.querySelector('#p16-cls .c7-chip'), 'классификатор оксид/не оксид §16');
  doc.defaultView.goTo('p17'); await wait(100);
  assert.ok(doc.querySelector('#p17-prod #p17-pick'), 'схема получения O2 §17');
  doc.defaultView.goTo('pr2'); await wait(100);
  assert.ok(doc.querySelector('#pr2-test #pr2-go'), 'проверка кислорода ПР2');
  doc.defaultView.goTo('final2'); await wait(120);
  assert.ok(doc.querySelectorAll('#navDotsfinal2 .nav-dot').length >= 6, 'боссы финала главы 2');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch3 Волна 1: §18 + §19 + §20 + ЛО3 монтируются', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch3.html');
  assert.ok(doc.querySelector('#p18-card svg'), 'паспорт водорода §18');
  doc.defaultView.goTo('p19'); await wait(100);
  assert.ok(doc.querySelector('#p19-rx #p19-pick'), 'реакции водорода §19');
  assert.ok(doc.querySelector('#p19-stage div'), 'анимация реакции §19');
  doc.defaultView.goTo('p20'); await wait(100);
  assert.ok(doc.querySelector('#p20-ind #p20-ind-ind'), 'индикаторы §20');
  assert.ok(doc.querySelector('#p20-ind-drop div'), 'анимация индикатора §20');
  assert.ok(doc.querySelector('#p20-acids table'), 'таблица кислот §20');
  doc.defaultView.goTo('lo3'); await wait(100);
  assert.ok(doc.querySelector('#lo3-ind #lo3-ind-ind'), 'индикаторы ЛО3');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch3 Волна 2: §21 + ЛО4 + §22 + ПР3 + финал главы монтируются', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch3.html');
  doc.defaultView.goTo('p21'); await wait(100);
  assert.ok(doc.querySelector('#p21-act .act-cell'), 'ряд активности §21');
  // клик по Zn (левее H₂) → пузырьки H₂
  doc.querySelector('#p21-act .act-cell[data-i="5"]').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true })); await wait(40);
  assert.ok(doc.querySelector('#p21-tube div'), 'пузырьки H₂ при реакции металла с кислотой §21');
  doc.defaultView.goTo('lo4'); await wait(100);
  assert.ok(doc.querySelector('#lo4-rx #lo4-go'), 'опыт металл+кислота ЛО4');
  doc.defaultView.goTo('p22'); await wait(100);
  assert.ok(doc.querySelector('#p22-salt #p22-m'), 'конструктор солей §22');
  doc.defaultView.goTo('pr3'); await wait(100);
  assert.ok(doc.querySelector('#pr3-test #pr3-mix'), 'проверка чистоты H2 ПР3');
  doc.defaultView.goTo('final3'); await wait(120);
  assert.ok(doc.querySelectorAll('#navDotsfinal3 .nav-dot').length >= 6, 'боссы финала главы 3');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch4: вся глава 4 (§23–§26 + ЛО5 + ПР4 + финал) монтируется', async () => {
  const { doc, errors } = await loadDom('chemistry_7_ch4.html');
  assert.ok(doc.querySelector('#p23-water #p23-pick'), 'разложение/реакции воды §23');
  assert.ok(doc.querySelector('#p23-bub-h div'), 'пузырьки электролиза 2:1 §23');
  doc.defaultView.goTo('p24'); await wait(100);
  assert.ok(doc.querySelector('#p24-bld #p24-m'), 'конструктор оснований §24');
  assert.ok(doc.querySelector('#p24-ind #p24-ind-sel'), 'индикаторы щёлочи §24');
  assert.ok(doc.querySelector('#p24-ind-drop div'), 'анимация индикатора §24');
  doc.defaultView.goTo('lo5'); await wait(100);
  assert.ok(doc.querySelector('#lo5-ind #lo5-ind-sel'), 'индикаторы ЛО5');
  doc.defaultView.goTo('p25'); await wait(100);
  assert.ok(doc.querySelector('#p25-neu #p25-neu-go'), 'нейтрализация §25');
  assert.ok(doc.querySelector('#p25-neu-cup div'), 'анимация раствора §25');
  doc.defaultView.goTo('pr4'); await wait(100);
  assert.ok(doc.querySelector('#pr4-neu #pr4-neu-go'), 'нейтрализация ПР4');
  doc.defaultView.goTo('p26'); await wait(100);
  assert.ok(doc.querySelector('#p26-eco .eco-it'), 'экология §26');
  doc.defaultView.goTo('final4'); await wait(120);
  assert.ok(doc.querySelectorAll('#navDotsfinal4 .nav-dot').length >= 6, 'боссы финала главы 4');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

/* ── Хаб: каталог глав + финал курса ── */
function buildHub() {
  let html = readF('frontend/textbooks/chemistry_7_hub.html');
  return html
    .replace(/<script defer src="https:\/\/cdn[^"]*"[^>]*><\/script>/g, '')
    .replace(/<script src="\/js\/api\.js" defer><\/script>/, '<script>window.renderMathInElement=function(){};</script>')
    .replace(/<script src="\/js\/xp\.js" defer><\/script>/, '');
}
async function loadHub() {
  const errors = []; const vc = new VirtualConsole(); vc.on('jsdomError', e => errors.push(e.message));
  const dom = new JSDOM(buildHub(), { runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/', beforeParse(w){ w.scrollTo=function(){}; } });
  await wait(60);
  return { dom, errors, doc: dom.window.document };
}

test('hub: 4 карточки глав, финал курса — 8 боссов, босс решается', async () => {
  const { doc, errors } = await loadHub();
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 4, '4 карточки глав');
  doc.getElementById('final-head').dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
  await wait(40);
  assert.equal(doc.querySelectorAll('#fin-bosses-container .boss-card').length, 8, '8 боссов');
  // решить босс 1 (Mr H2SO4 = 98)
  const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
  inp.value = '98'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
  assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
});
