'use strict';
/*
 * Полностраничная jsdom-проверка глав «Химия 8» (SPA на chem8_engine.js):
 * выполняем реальный HTML + движок + виджеты, даём таймерам отработать, проверяем
 * para-selector, активный §, монтаж виджетов — без ошибок скриптов.
 */
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));

function buildPage(file, widgetsSrc) {
  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/chem8_mol.js': readF('frontend/js/chem8_mol.js'),
    [widgetsSrc]: readF('frontend/js' + widgetsSrc.replace('/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, widgetsSrc) {
  const errors = [];
  const vc = new VirtualConsole();
  vc.on('jsdomError', e => errors.push(e.message));
  const dom = new JSDOM(buildPage(file, widgetsSrc), {
    runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
    beforeParse(w) { w.scrollTo = function () {}; }
  });
  await wait(180);
  return { dom, errors, doc: dom.window.document };
}

/* ── Вводный раздел ── */
test('intro: SPA без ошибок, 11 карточек, §1 активен, виджеты', async () => {
  const { doc, errors } = await loadDom('chemistry_8_intro.html', '/js/chem8_intro_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 11, '11 карточек');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p1', '§1 активен');
  assert.ok(doc.querySelectorAll('#p1-el .el-cell').length > 10, 'карта элементов');
  doc.defaultView.goTo('p6'); await wait(120);
  assert.ok(doc.querySelector('#p6-mount .mtri'), 'треугольник §6');
});

/* ── Глава 1 ── */
test('ch1: SPA без ошибок, 15 карточек, §10 активен', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 15, '14 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p10', '§10 активен');
  assert.ok(doc.querySelector('#p10-body .para-hero'), 'para-hero §10');
});

test('ch1: флагман-виджеты монтируются (классификатор, растворимость, ряд активности)', async () => {
  const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
  doc.defaultView.goTo('p10'); await wait(120);
  assert.ok(doc.querySelector('#c-ox-cls .cls-chip'), 'классификатор оксидов §10');
  doc.defaultView.goTo('p13'); await wait(120);
  assert.ok(doc.querySelector('#c-acid-ind .ind-strip'), 'индикатор §13');
  doc.defaultView.goTo('p19'); await wait(120);
  assert.ok(doc.querySelector('#c-salt-sol .sol-tab'), 'таблица растворимости §19');
  doc.defaultView.goTo('p14'); await wait(120);
  assert.ok(doc.querySelector('#c-acid-act .act-cell'), 'ряд активности §14');
});

test('ch1: тренажёр задач отрисован для §10', async () => {
  const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
  await wait(150);
  assert.ok(doc.querySelectorAll('#navDotsp10 .nav-dot').length >= 4, 'навигация по задачам §10');
});

test('ch1: генетическая карта §22 монтируется (U3)', async () => {
  const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
  doc.defaultView.goTo('p22'); await wait(120);
  assert.ok(doc.querySelectorAll('#c-genetic .gm-edge').length >= 6, 'граф классов §22');
});

test('ch1: карта связей в финале главы монтируется (U6)', async () => {
  const { doc } = await loadDom('chemistry_8_ch1.html', '/js/chem8_ch1_widgets.js');
  doc.defaultView.goTo('final1'); await wait(120);
  assert.ok(doc.querySelectorAll('#c-concept .gm-edge').length >= 3, 'карта связей понятий финала');
});

/* ── Глава 2 ── */
test('ch2: SPA без ошибок, 6 карточек, §24 активен, ПСХЭ', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 6, '5 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p24', '§24 активен');
  await wait(120);
  assert.ok(doc.querySelectorAll('#c-pt-metals .pt-cell').length > 80, 'ПСХЭ §24 (90 элементов)');
});

test('ch2: амфотерность §25 и семейства §26 монтируются', async () => {
  const { doc } = await loadDom('chemistry_8_ch2.html', '/js/chem8_ch2_widgets.js');
  doc.defaultView.goTo('p25'); await wait(120);
  assert.ok(doc.querySelector('#c-amph .amph-btn'), 'амфотерность §25');
  doc.defaultView.goTo('p26'); await wait(120);
  assert.ok(doc.querySelectorAll('#c-pt-fam .pt-cell').length > 80, 'ПСХЭ семейства §26');
});

/* ── Глава 3 ── */
test('ch3: SPA без ошибок, 8 карточек, §29 активен, модель атома', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 8, '7 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p29', '§29 активен');
  await wait(120);
  assert.ok(doc.querySelector('#c-atom .as-svg'), 'модель атома §29');
});

test('ch3: нуклид §30 и паспорт §35 монтируются', async () => {
  const { doc } = await loadDom('chemistry_8_ch3.html', '/js/chem8_ch3_widgets.js');
  doc.defaultView.goTo('p30'); await wait(120);
  assert.ok(doc.querySelector('#c-nuclide #nz'), 'калькулятор нуклида §30');
  doc.defaultView.goTo('p35'); await wait(120);
  assert.ok(doc.querySelectorAll('#c-passport .pt-cell').length > 80, 'ПСХЭ паспорта §35');
});

/* ── Глава 4 ── */
test('ch4: SPA без ошибок, 7 карточек, §36 активен, тип связи', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 7, '6 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p36', '§36 активен');
  doc.defaultView.goTo('p37'); await wait(120);
  assert.ok(doc.querySelector('#c-bond1 .bt-svg'), 'виджет типа связи §37');
  doc.defaultView.goTo('p38'); await wait(120);
  assert.ok(doc.querySelector('#c-bond2 .bt-out'), 'виджет полярности §38');
});

test('ch4: 3D-модели молекул §38 и решётки §41 монтируются (U4)', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch4.html', '/js/chem8_ch4_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  doc.defaultView.goTo('p38'); await wait(140);
  assert.ok(doc.querySelector('#c-mol .mol-sel'), 'выбор молекулы §38');
  assert.ok(doc.querySelector('#c-mol canvas'), 'canvas 3D-модели §38');
  assert.ok(doc.querySelector('#c-mol .mol-info'), 'инфо о молекуле §38');
  doc.defaultView.goTo('p41'); await wait(140);
  assert.ok(doc.querySelector('#c-lattice .lat-sel'), 'выбор решётки §41');
  assert.ok(doc.querySelector('#c-lattice canvas'), 'canvas решётки §41');
});

/* ── Глава 5 ── */
test('ch5: SPA без ошибок, 5 карточек, §42 активен, с.о. и баланс', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch5.html', '/js/chem8_ch5_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 5, '4 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p42', '§42 активен');
  await wait(120);
  assert.ok(doc.querySelector('#c-ox .ox-out'), 'калькулятор с.о. §42');
  doc.defaultView.goTo('p44'); await wait(120);
  assert.ok(doc.querySelector('#c-redox-pick option'), 'электронный баланс §44');
});

/* ── Глоссарий (U2/Phase 8) ── */
test('glossary: кнопка, модалка, авто-подсветка терминов', async () => {
  const src = readF('frontend/js/chem8_glossary.js');
  const dom = new JSDOM('<!DOCTYPE html><body><div class="card-body"><p>Оксид — это сложное вещество. Кислота реагирует с основанием в реакции нейтрализации.</p></div></body>',
    { runScripts: 'outside-only', pretendToBeVisual: true, url: 'http://localhost/' });
  new Function('window', src)(dom.window);
  await wait(20);
  const doc = dom.window.document;
  assert.ok(dom.window.Chem8Glossary, 'window.Chem8Glossary определён');
  assert.ok(Object.keys(dom.window.Chem8Glossary.terms).length > 40, '>40 терминов');
  assert.ok(doc.querySelector('.gl-fab'), 'плавающая кнопка глоссария');
  // авто-подсветка терминов в .card-body
  assert.ok(doc.querySelectorAll('.card-body .gloss').length >= 2, 'термины подсвечены: ' + doc.querySelectorAll('.gloss').length);
  // открытие модалки
  dom.window.Chem8Glossary.open();
  assert.ok(doc.querySelector('.gl-modal.show'), 'модалка открыта');
  assert.ok(doc.querySelectorAll('.gl-modal .gl-item').length > 40, 'список терминов в модалке');
});

/* ── Хаб: финал курса (Phase 7) ── */
function buildHub() {
  let html = readF('frontend/textbooks/chemistry_8_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: финал курса — 10 боссов рендерятся при раскрытии, босс решается', async () => {
  const { doc, errors } = await loadHub();
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 7, '7 карточек глав');
  // раскрыть финал
  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, 10, '10 боссов');
  // решить босс 1 (Mr Ca(OH)2 = 74)
  const inp = doc.getElementById('fb-1-inp'), go = doc.getElementById('fb-1-go');
  inp.value = '74'; go.dispatchEvent(new doc.defaultView.Event('click', { bubbles: true }));
  assert.ok(doc.getElementById('fb-1-card').classList.contains('solved'), 'босс 1 повержен');
});

/* ── Глава 6 ── */
test('ch6: SPA без ошибок, 8 карточек, §46 активен, w/c калькуляторы', async () => {
  const { doc, errors } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, 8, '7 § + финал');
  assert.ok(doc.querySelector('.sec.active') && doc.querySelector('.sec.active').id === 'sec-p46', '§46 активен');
  await wait(120);
  assert.ok(doc.querySelector('#c-mix .cls-chip'), 'классификатор смесей §46');
  doc.defaultView.goTo('p50'); await wait(120);
  assert.ok(doc.querySelector('#c-wcalc #w-go'), 'калькулятор w §50');
  doc.defaultView.goTo('p51'); await wait(120);
  assert.ok(doc.querySelector('#c-ccalc #c-go'), 'калькулятор c §51');
});

test('ch6: анимация растворения §47 монтируется (U3)', async () => {
  const { doc } = await loadDom('chemistry_8_ch6.html', '/js/chem8_ch6_widgets.js');
  doc.defaultView.goTo('p47'); await wait(120);
  assert.ok(doc.querySelector('#c-dissoc .ds-svg'), 'анимация диссоциации §47');
});
