// Автопроверки интерактивного графа связей (dev-only).
//
// Запускаются ТОЛЬКО в лаборатории при наличии ?fgtest в URL (см. lab.js). Используют детерминированные
// dev-хелперы движка (graph.debugState / graph.pumpForTest) — поэтому проходят стабильно даже когда
// requestAnimationFrame троттлится в фоновой вкладке (pumpForTest синхронно докручивает кадры до покоя).
//
// Результат печатается в консоль и кладётся в window.__fgTestResults = { pass, total, results[] }.

const DEEP_FAN_HALF_DEG = 110; // допустимое отклонение детей от направления «наружу» (полукруг ~±99° + запас)

export async function runNetworkSelfTest(graph, deepChipEl) {
  const wait = (ms) => new Promise((r) => setTimeout(r, ms));
  const results = [];
  const check = (name, pass, detail) => { results.push({ name, pass: !!pass, detail }); };
  const st = () => graph.debugState();

  // 1) Включаем режим «Вселенная» и ждём, пока завершится bloom-перестроение (его закрывает setTimeout).
  if (deepChipEl && !deepChipEl.classList.contains('is-active')) deepChipEl.click();
  await wait(1700);

  let s = st();
  const focusId = s.focusId;
  const tier1 = s.nodes.filter((n) => n.tier === 1 && n.id !== focusId);
  const parent = tier1.find((n) => n.id === 'nina') || tier1[0];
  if (!parent) { check('есть узлы 1-го уровня', false, 'tier-1 не найдены'); return finish(results); }

  // === Тест A: погружение в узел 1-го уровня (камера-наезд + расталкивание + полукруг) ===
  graph.diveTo({ id: parent.id });
  const framesA = graph.pumpForTest();
  s = st();
  const p = s.nodes.find((n) => n.id === parent.id);
  const kids = s.nodes.filter((n) => n.tier === 2 && String(n.id).startsWith(parent.id + '__d2_'));

  check('A1 анимация погружения завершается (freeze)', framesA < 1190, `кадров: ${framesA}`);
  check('A2 камера зумит (zoom≈DIVE)', s.zoom >= 1.5, `zoom=${s.zoom}`);
  check('A3 узел центрируется камерой', Math.abs(s.camX + p.x * s.zoom) < 36 && Math.abs(s.camY + p.y * s.zoom) < 36,
    `offset=(${Math.round(s.camX + p.x * s.zoom)},${Math.round(s.camY + p.y * s.zoom)})`);
  check('A4 узел вырос (герой)', p.depthScale > 1.2, `depthScale=${p.depthScale}`);

  // расталкивание: дети не слипаются
  let minD = Infinity;
  for (let i = 0; i < kids.length; i += 1) for (let j = i + 1; j < kids.length; j += 1) {
    minD = Math.min(minD, Math.hypot(kids[i].x - kids[j].x, kids[i].y - kids[j].y));
  }
  check('A5 дети не слипаются (collision)', kids.length >= 2 ? minD > 40 : true, `мин.дистанция=${Math.round(minD)}px`);

  // полукруг наружу: все дети в секторе вокруг направления от центра к родителю
  const outward = Math.atan2(p.y, p.x);
  const maxDev = kids.reduce((mx, k) => {
    let d = Math.abs(Math.atan2(k.y - p.y, k.x - p.x) - outward);
    if (d > Math.PI) d = 2 * Math.PI - d;
    return Math.max(mx, d * 180 / Math.PI);
  }, 0);
  check('A6 веер полукругом наружу', kids.length ? maxDev <= DEEP_FAN_HALF_DEG : true, `maxDev=${Math.round(maxDev)}°`);

  // === Тест B: Spotlight открыт — путь горит, остальное тускнеет ===
  const offPath = tier1.filter((n) => n.id !== parent.id);
  const offDim = offPath.every((n) => { const x = s.nodes.find((m) => m.id === n.id); return x && x.spotCur < 0.4; });
  const pathLit = (s.nodes.find((n) => n.id === parent.id).spotCur > 0.9) && (s.nodes.find((n) => n.id === focusId).spotCur > 0.9);
  check('B1 путь горит на 100%', pathLit, 'фокус+цель spotCur>0.9');
  check('B2 остальные ветки затемнены (~0.25)', offDim, 'все вне пути spotCur<0.4');

  // === Тест C: переключение веток сбрасывает прежнюю (нет накопления) ===
  if (offPath.length) {
    graph.diveTo({ id: offPath[0].id });
    graph.pumpForTest();
    s = st();
    const prev = s.nodes.find((n) => n.id === parent.id);
    check('C1 прежняя ветка сброшена при переключении', prev.spotCur < 0.4, `прежняя spotCur=${prev.spotCur}`);
    check('C2 новая цель — активна', s.diveTargetId === offPath[0].id, `dive=${s.diveTargetId}`);
  }

  // === Тест D: LOD — дети 3-го уровня становятся аватарками при сильном зуме ===
  const t2withKids = st().nodes.find((n) => n.tier === 2);
  if (t2withKids) {
    graph.diveTo({ id: t2withKids.id });
    graph.pumpForTest();
    s = st();
    const t3 = s.nodes.filter((n) => n.tier === 3 && String(n.id).startsWith(t2withKids.id + '_d3_'));
    const allFull = t3.length ? t3.every((n) => n.lod === 'full') : true;
    check('D1 точки 3-го уровня → аватарки (LOD)', allFull, `full: ${t3.filter((n) => n.lod === 'full').length}/${t3.length}`);
  }

  // === Тест F: поиск по имени находит узел (для строки поиска + телепорта) ===
  const named = st().nodes.find((n) => n.tier === 1 && n.id !== st().focusId);
  if (named && typeof graph.findNode === 'function') {
    const byId = graph.findNode(named.id);
    check('F1 поиск находит узел по логину', byId && byId.id === named.id, `найдено: ${byId && byId.id}`);
  }

  // === Тест G: хлебные крошки — путь focus → … → цель (мы сейчас в t2withKids) ===
  if (typeof graph.getDivePath === 'function' && t2withKids) {
    const path = graph.getDivePath();
    const okPath = path.length >= 2 && path[0].isFocus && path[path.length - 1].id === t2withKids.id;
    check('G1 хлебные крошки строят путь к цели', okPath, `путь: ${path.map((p) => p.name).join(' › ')}`);
  }

  // === Тест H: бейдж числа связей виден и числовой (DOM) ===
  if (typeof document !== 'undefined') {
    const fb = document.querySelector('.fg-node.is-focus .fg-node-badge');
    const fbOk = fb && !fb.hidden && Number(fb.textContent) > 0;
    check('H1 бейдж числа связей на центре', !!fbOk, `центр: ${fb ? fb.textContent : 'нет'}`);
  }

  // === Тест I: общие связи — есть узлы с золотым ободком ★ (общий друг) ===
  if (typeof document !== 'undefined') {
    const commonCount = document.querySelectorAll('.fg-node.is-common').length;
    check('I1 общие связи помечены (★)', commonCount >= 1, `узлов «общая связь»: ${commonCount}`);
  }

  // === Тест J: доступность — текстовый список графа для скринридеров ===
  if (typeof document !== 'undefined') {
    const a11y = document.querySelector('.fg-a11y');
    const liCount = a11y ? a11y.querySelectorAll('li').length : 0;
    check('J1 sr-only список графа заполнен', !!a11y && liCount >= 1, `пунктов списка: ${liCount}`);
  }

  // === Тест E: выход — ВСЕ узлы возвращают 100% яркости, зум отъезжает ===
  graph.exitDive();
  graph.pumpForTest();
  s = st();
  const allBright = s.nodes.filter((n) => n.tier === 1).every((n) => n.spotCur > 0.95);
  check('E1 выход: все узлы 100% яркости', allBright, 'tier-1 spotCur>0.95');
  check('E2 выход: зум вернулся (~1)', Math.abs(s.zoom - 1) < 0.05, `zoom=${s.zoom}`);
  check('E3 выход: погружение снято', s.diveTargetId === null, `dive=${s.diveTargetId}`);

  // === Тест K: сияющие линии — плазма из 3 слоёв на ОДНОМ S-пути (одинаковый d) ===
  if (typeof document !== 'undefined') {
    const flare = document.querySelectorAll('.fg-plasma-flare');
    const tube = document.querySelectorAll('.fg-plasma-tube');
    const core = document.querySelectorAll('.fg-plasma-core');
    const equalLayers = flare.length >= 1 && flare.length === tube.length && tube.length === core.length;
    const sameD = flare[0] && flare[0].getAttribute('d') === tube[0].getAttribute('d')
      && tube[0].getAttribute('d') === core[0].getAttribute('d');
    check('K1 плазма: 3 слоя на ОДНОМ S-пути', equalLayers && !!sameD, `поле:${flare.length} трубка:${tube.length} ядро:${core.length} sameD:${!!sameD}`);
  }

  return finish(results);
}

function finish(results) {
  const pass = results.filter((r) => r.pass).length;
  const out = { pass, total: results.length, results };
  if (typeof window !== 'undefined') window.__fgTestResults = out;
  const tag = pass === results.length ? '✅ PASS' : '❌ FAIL';
  // eslint-disable-next-line no-console
  console.log(`[fg-selftest] ${tag} ${pass}/${results.length}`);
  results.forEach((r) => console.log(`  ${r.pass ? '✓' : '✗'} ${r.name} — ${r.detail}`));
  return out;
}
