'use strict';
/*
 * Phase 0 jsdom-каркас «Математика 6»: хаб и 6 глав выполняются на движке
 * math6_engine.js без ошибок скриптов; 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));

/* Инлайним внешние скрипты (CDN убираем, api/xp заменяем заглушками). */
function buildPage(file) {
  let html = readF('frontend/textbooks/' + file);
  const inl = {
    '/js/math6_svg.js': readF('frontend/js/math6_svg.js'),
    '/js/math6_anim.js': readF('frontend/js/math6_anim.js'),
    '/js/math6_engine.js': readF('frontend/js/math6_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: 'math_6_ch1.html', cards: 12 },
  { file: 'math_6_ch2.html', cards: 9 },
  { file: 'math_6_ch3.html', cards: 5 },
  { file: 'math_6_ch4.html', cards: 11 },
  { file: 'math_6_ch5.html', cards: 5 },
  { file: 'math_6_ch6.html', cards: 6 }
];

test('engine: init() вызывается ПОСЛЕ экспортов (guard от sync-defer бага makeCard)', () => {
  const src = readF('frontend/js/math6_engine.js');
  const exportIdx = src.indexOf('window.makeCard = makeCard');
  const initCallIdx = src.lastIndexOf('else init();');
  assert.ok(exportIdx > 0, 'есть экспорт window.makeCard');
  assert.ok(initCallIdx > exportIdx, 'else init() должен идти ПОСЛЕ window.makeCard = makeCard (иначе билдеры упадут с ReferenceError при defer-старте)');
});

for (const ch of CHAPTERS) {
  test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен § 1`, 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 === 'sec-p1', 'активен sec-p1');
    const body = doc.querySelector('#p1-body');
    assert.ok(body && body.children.length > 0, 'тело § 1 заполнено');
    assert.ok(doc.querySelector('#p1-body [data-read]'), 'кнопка прочтения § 1');
    /* финал помечен и присутствует */
    assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
  });
}

test('ch2: проценты и пропорции — интерактивы + финал', async () => {
  const { doc, errors } = await loadDom('math_6_ch2.html');
  const win = doc.defaultView;
  assert.ok(doc.querySelector('#p1-fig svg rect') && doc.querySelector('#p1-q'), 'сетка 100 и конвертер §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelectorAll('#p2-iv1 [data-t]').length === 3 && doc.querySelector('#p2-cq'), 'типы задач §2');
  win.goTo('p3'); await wait(80);
  assert.ok(doc.querySelector('#p3-q') && doc.querySelectorAll('#p3-iv2 [data-v]').length === 2, 'пропорция §3');
  win.goTo('p7'); await wait(80);
  assert.ok(doc.querySelector('#p7-fig svg') && doc.querySelector('#p7-pick [data-k]'), 'круговая диаграмма §7');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-go'), 'арена боссов §2');
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch2_done'), 'достижение «Глава 2 пройдена»');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch3: множество — интерактивы + финал', async () => {
  const { doc, errors } = await loadDom('math_6_ch3.html');
  const win = doc.defaultView;
  assert.ok(doc.querySelectorAll('#p1-iv1 [data-v]').length === 2 && doc.querySelector('#p1-cq'), '∈/∉ и счёт §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelector('#p2-opts') && doc.querySelectorAll('#p2-iv2 [data-v]').length === 2, 'способы задания §2');
  win.goTo('p3'); await wait(80);
  assert.ok(doc.querySelector('#p3-fig svg circle') && doc.querySelectorAll('#p3-iv1 [data-op]').length === 2, 'круги Эйлера/операции §3');
  win.goTo('p4'); await wait(80);
  assert.ok(doc.querySelector('#p4-fig svg') && doc.querySelector('#p4-fq'), 'задача с кругами Эйлера §4');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-go'), 'арена боссов §3');
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch3_done'), 'достижение «Глава 3 пройдена»');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch4: рациональные числа — интерактивы + финал', async () => {
  const { doc, errors } = await loadDom('math_6_ch4.html');
  const win = doc.defaultView;
  assert.ok(doc.querySelector('#p1-fig svg') && doc.querySelectorAll('#p1-iv2 [data-v]').length === 3, 'координатная прямая §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelector('#p2-q') && doc.querySelector('#p2-oq'), 'модуль и противоположные §2');
  win.goTo('p4'); await wait(80);
  assert.ok(doc.querySelector('#p4-asl') && doc.querySelector('#p4-fig svg'), 'сложение на прямой §4');
  win.goTo('p7'); await wait(80);
  assert.ok(doc.querySelectorAll('#p7-iv1 [data-s]').length === 4 && doc.querySelector('#p7-q'), 'знаки умножения §7');
  win.goTo('p9'); await wait(80);
  assert.ok(doc.querySelector('#p9-q'), 'порядок действий §9');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-go'), 'арена боссов §4');
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch4_done'), 'достижение «Глава 4 пройдена»');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch5: координатная плоскость — интерактивы §1–§3 + финал', async () => {
  const { doc, errors } = await loadDom('math_6_ch5.html');
  const win = doc.defaultView;
  assert.ok(doc.querySelector('#p1-fig svg'), 'плоскость с точкой §1');
  assert.ok(doc.querySelectorAll('#p1-iv2 [data-q]').length === 4, 'кнопки четвертей §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelector('#p2-fig svg polyline'), 'график процесса §2');
  win.goTo('p3'); await wait(80);
  assert.ok(doc.querySelector('#p3-k'), 'слайдер k §3');
  assert.ok(doc.querySelector('#p3-fig svg path'), 'график y=kx §3');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-go'), 'арена боссов §5');
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch5_done'), 'достижение «Глава 5 пройдена»');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch6: наглядная геометрия — интерактивы §1–§5 + финал', async () => {
  const { doc, errors } = await loadDom('math_6_ch6.html');
  const win = doc.defaultView;
  assert.ok(doc.querySelector('#p1-fig svg') && doc.querySelector('#p1-q'), 'тела §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelector('#p2-r') && doc.querySelector('#p2-fig svg circle'), 'окружность §2');
  assert.ok(doc.querySelector('#p2-out').textContent.indexOf('=') >= 0, 'формулы C, S §2');
  win.goTo('p3'); await wait(80);
  assert.ok(doc.querySelector('#p3-fig svg polygon'), 'треугольник §3');
  assert.ok(doc.querySelectorAll('#p3-iv1 [data-v]').length === 3, 'виды по сторонам §3');
  win.goTo('p4'); await wait(80);
  assert.ok(doc.querySelector('#p4-fig svg'), 'плоскость симметрии §4');
  win.goTo('p5'); await wait(80);
  assert.ok(doc.querySelector('#p5-fig svg'), 'плоскость симметрии §5');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-go'), 'арена боссов §6');
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch6_done'), 'достижение «Глава 6 пройдена»');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('анимации: canvas-демо монтируются (headless-safe)', async () => {
  // Глава 6 §2: колесо + заметание площади
  const r6 = await loadDom('math_6_ch6.html');
  r6.doc.defaultView.goTo('p2'); await wait(100);
  assert.ok(r6.doc.querySelector('#p2-roll canvas'), 'canvas «колесо» §6.2');
  assert.ok(r6.doc.querySelector('#p2-sweep canvas'), 'canvas «заметание площади» §6.2');
  assert.deepEqual(r6.errors, [], 'ch6 без ошибок: ' + r6.errors.join(' | '));
  // Глава 1 §6: площадная модель умножения
  const r1 = await loadDom('math_6_ch1.html');
  r1.doc.defaultView.goTo('p6'); await wait(100);
  assert.ok(r1.doc.querySelector('#p6-area canvas'), 'canvas «площадная модель» §1.6');
  assert.deepEqual(r1.errors, [], 'ch1 без ошибок: ' + r1.errors.join(' | '));
  // Глава 4 §4: прыжки по числовой прямой
  const r4 = await loadDom('math_6_ch4.html');
  r4.doc.defaultView.goTo('p4'); await wait(100);
  assert.ok(r4.doc.querySelector('#p4-walk canvas'), 'canvas «прыжки по прямой» §4.4');
  assert.deepEqual(r4.errors, [], 'ch4 без ошибок: ' + r4.errors.join(' | '));
  // Глава 5 §2: машинка + график
  const r5 = await loadDom('math_6_ch5.html');
  r5.doc.defaultView.goTo('p2'); await wait(100);
  assert.ok(r5.doc.querySelector('#p2-car canvas'), 'canvas «машинка + график» §5.2');
  r5.doc.defaultView.goTo('p3'); await wait(100);
  assert.ok(r5.doc.querySelector('#p3-livefig canvas'), 'canvas «живой график y=kx/k÷x» §5.3');
  assert.ok(r5.doc.querySelectorAll('#p3-live [data-m]').length === 2, 'переключатель прямая/обратная §5.3');
  assert.deepEqual(r5.errors, [], 'ch5 без ошибок: ' + r5.errors.join(' | '));
});

test('hub: 6 карточек глав + курсовой финал', async () => {
  const { doc, errors } = await loadDom('math_6_hub.html');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
  assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 6, '6 глав');
  assert.ok(doc.querySelector('#cf-wrap') && doc.querySelector('#cf-go'), 'секция курсового финала');
  assert.ok(doc.querySelector('#cf-q').textContent.length > 0, 'первое испытание показано');
});

test('ch1 Волна 1: интерактивы §1–§3 монтируются без ошибок', async () => {
  const { doc, errors } = await loadDom('math_6_ch1.html');
  const win = doc.defaultView;
  // §1 строится при загрузке
  assert.ok(doc.querySelector('#p1-iv1 #p1-c'), 'разрядный конструктор §1');
  assert.ok(doc.querySelector('#p1-iv2 #p1-qq'), 'разряд-квиз §1');
  win.goTo('p2'); await wait(80);
  assert.ok(doc.querySelector('#p2-cfig svg'), 'числовая прямая сравнения §2');
  assert.ok(doc.querySelector('#p2-iv2 #p2-rq'), 'тренажёр округления §2');
  win.goTo('p3'); await wait(80);
  assert.ok(doc.querySelector('#p3-afig svg'), 'координатный луч §3');
  assert.ok(doc.querySelectorAll('#p3-iv2 [data-pt]').length === 4, 'выбор точки A–D §3');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 2: интерактивы §4–§6 монтируются без ошибок', async () => {
  const { doc, errors } = await loadDom('math_6_ch1.html');
  const win = doc.defaultView;
  win.goTo('p4'); await wait(80);
  assert.ok(doc.querySelector('#p4-fig'), 'столбик §4');
  assert.ok(doc.querySelectorAll('#p4-eopts').length === 1, 'варианты §4');
  win.goTo('p5'); await wait(80);
  assert.ok(doc.querySelector('#p5-iv1 [data-op]'), 'кнопки сдвига запятой §5');
  assert.ok(doc.querySelector('#p5-out').textContent.length > 0 || doc.querySelector('#p5-out'), 'демонстратор §5');
  win.goTo('p6'); await wait(80);
  assert.ok(doc.querySelector('#p6-asl') && doc.querySelector('#p6-bsl'), 'ползунки множителей §6');
  assert.ok(doc.querySelector('#p6-q'), 'тренажёр умножения §6');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 3: интерактивы §7–§10 монтируются без ошибок', async () => {
  const { doc, errors } = await loadDom('math_6_ch1.html');
  const win = doc.defaultView;
  win.goTo('p7'); await wait(80);
  assert.ok(doc.querySelector('#p7-q') && doc.querySelector('#p7-rq'), 'тренажёры деления §7');
  win.goTo('p8'); await wait(80);
  assert.ok(doc.querySelector('#p8-pick [data-k]'), 'выбор примеров §8');
  assert.ok(doc.querySelector('#p8-out').textContent.indexOf('=') >= 0, 'демонстратор переноса запятой §8');
  win.goTo('p9'); await wait(80);
  assert.ok(doc.querySelector('#p9-iv1 [data-fin]'), 'классификатор §9');
  assert.ok(doc.querySelectorAll('#p9-dopts [data-o]').length === 3, 'варианты десятичной §9');
  win.goTo('p10'); await wait(80);
  assert.ok(doc.querySelectorAll('#p10-pool .dnd-chip').length === 5, 'сопоставление дробей §10');
  assert.ok(doc.querySelector('#p10-q'), 'выражения §10');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('ch1 Волна 4: §12 прикладной и финал-боссы', async () => {
  const { doc, errors } = await loadDom('math_6_ch1.html');
  const win = doc.defaultView;
  win.goTo('app'); await wait(80);
  assert.ok(doc.querySelector('#app-q') && doc.querySelector('#app-aq'), 'задачи §12');
  win.goTo('final'); await wait(80);
  assert.ok(doc.querySelector('#fin-q') && doc.querySelector('#fin-go'), 'арена боссов');
  // финал на 100% → достижение ch1_done (finalAch)
  win.bumpProgress('final', 100); await wait(20);
  assert.ok(win.M6STATE.achievements.has('ch1_done'), 'достижение «Глава 1 пройдена» при финале');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});

test('навигация и прогресс: переход на § и отметка прочтения', async () => {
  const { doc, errors } = await loadDom('math_6_ch1.html');
  const win = doc.defaultView;
  win.goTo('p4'); await wait(60);
  assert.ok(doc.querySelector('#sec-p4.active'), 'перешли на § 4');
  /* отметка прочтения начисляет XP */
  const btn = doc.querySelector('#p4-body [data-read]');
  assert.ok(btn, 'кнопка прочтения § 4');
  btn.click(); await wait(20);
  assert.ok((win.M6STATE.progress.p4 || 0) >= 30, 'прогресс § 4 вырос после прочтения');
  assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
});
