#!/usr/bin/env node
/**
 * enduro-phase3.js — UI тесты Enduro Trails Фаза 3
 * 56 тест-кейсов из TEST_CASES_PHASE3.md
 *
 * Запуск:
 *   CHROME_BIN=/usr/bin/chromium-browser \
 *   TEST_URL=https://openclaw.mva154.duckdns.org/enduro/ \
 *   node enduro-phase3.js
 */

const puppeteer = require('puppeteer-core');
const fs = require('fs');
const path = require('path');

const CONFIG = {
  url: process.env.TEST_URL || 'https://openclaw.mva154.duckdns.org/enduro/',
  chromeBin: process.env.CHROME_BIN || '/usr/bin/chromium-browser',
  screenshotsDir: process.env.SCREENSHOTS_DIR || '/tmp/enduro-screenshots',
  resultsFile: process.env.RESULTS_FILE || '/tmp/enduro-results.json',
  viewportDesktop: { width: 1280, height: 800 },
  viewportMobile: { width: 375, height: 667 },
};

const results = [];
function pass(id, note) { results.push({ id, status: 'PASS', note }); console.log(`✅ ${id}: ${note}`); }
function fail(id, note, snap) { results.push({ id, status: 'FAIL', note, screenshot: snap }); console.log(`❌ ${id}: ${note}`); }
function blocked(id, note) { results.push({ id, status: 'BLOCKED', note }); console.log(`⚠️  ${id}: ${note}`); }

async function screenshot(page, name) {
  const p = path.join(CONFIG.screenshotsDir, `${name}.png`);
  await page.screenshot({ path: p });
  console.log(`   📸 ${name}.png`);
  return p;
}
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function isVisible(page, sel) {
  return page.$eval(sel, el => {
    const s = getComputedStyle(el);
    return s.display !== 'none' && s.visibility !== 'hidden';
  }).catch(() => false);
}
async function getText(page, sel) { return page.$eval(sel, el => el.textContent.trim()).catch(() => null); }

// ─── Координаты точек на карте (ЦФО) ─────────────────────────────────────────
// При zoom=7, center=[40.5,55.5] — кликаем в пиксели карты
// Точка A: ~Москва, Точка B: ~Тверь
function mapClick(mapBox, fracX, fracY) {
  return {
    x: mapBox.x + mapBox.width * fracX,
    y: mapBox.y + mapBox.height * fracY,
  };
}

(async () => {
  fs.mkdirSync(CONFIG.screenshotsDir, { recursive: true });
  console.log(`\n🧪 Enduro Trails — Фаза 3 UI Tests`);
  console.log(`   URL: ${CONFIG.url}`);
  console.log(`   Chrome: ${CONFIG.chromeBin}\n`);

  const browser = await puppeteer.launch({
    executablePath: CONFIG.chromeBin,
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage',
           '--disable-gpu', '--use-gl=swiftshader'],
  });

  const page = await browser.newPage();
  await page.setViewport(CONFIG.viewportDesktop);

  // ── Загрузка страницы ────────────────────────────────────────────────────────
  await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 30000 });
  await sleep(3000);
  const snap_initial = await screenshot(page, 'TC-PAGE-01-initial');

  const title = await page.title();
  title.includes('Enduro') ? pass('TC-PAGE-01', `title: "${title}"`) : fail('TC-PAGE-01', `неожиданный title: "${title}"`, snap_initial);

  // ── TC-F03: formatDuration — проверка через page.evaluate ───────────────────
  console.log('\n── TC-F03: formatDuration ──');
  const durTests = [
    [2700,  '45 мин'],
    [3600,  '1 ч'],
    [9300,  '2 ч 35 мин'],
    [86400, '1 дн'],
    [93600, '1 дн 2 ч'],
    [165000,'1 дн 21 ч 50 мин'],
    [0,     '0 мин'],
  ];
  for (const [secs, expected] of durTests) {
    const result = await page.evaluate((s) => {
      if (typeof formatDuration === 'function') return formatDuration(s);
      return null;
    }, secs);
    if (result === null) { blocked(`TC-F03-${secs}s`, 'formatDuration не найдена в window'); break; }
    result === expected
      ? pass(`TC-F03-${secs}s`, `${secs}s → "${result}"`)
      : fail(`TC-F03-${secs}s`, `${secs}s → "${result}", ожидалось "${expected}"`);
  }

  // ── UI элементы ──────────────────────────────────────────────────────────────
  console.log('\n── UI элементы ──');
  for (const [id, sel, label] of [
    ['TC-UI-BTN-ROUTE',    '#btn-route',        'кнопка маршрута'],
    ['TC-UI-BTN-MARKERS',  '#btn-markers',      'кнопка 🚩'],
    ['TC-UI-BTN-RULER',    '#btn-ruler',        'кнопка линейки'],
    ['TC-UI-BTN-COMPASS',  '#btn-compass',      'кнопка компаса'],
    ['TC-UI-BTN-LOCATE',   '#btn-locate',       'кнопка геолокации'],
    ['TC-UI-SEARCH',       '#search-input',     'поиск'],
    ['TC-UI-LEGEND',       '#legend',           'легенда'],
  ]) {
    const el = await page.$(sel);
    el ? pass(id, `${label} найдена`) : fail(id, `${label} не найдена (${sel})`, snap_initial);
  }

  // TC-F01: route-panel скрыт изначально
  const rpHidden = !(await isVisible(page, '#route-panel'));
  rpHidden ? pass('TC-F01-PANEL-HIDDEN', 'route-panel скрыт') : fail('TC-F01-PANEL-HIDDEN', 'route-panel виден без активации');

  // TC-F05-01: GPX кнопка — проверяем что нет активной кнопки GPX без маршрута
  const gpxBtnVisible = await page.$eval('button[onclick*="downloadGPX"]', el => !el.disabled).catch(() => false);
  !gpxBtnVisible ? pass('TC-F05-01', 'GPX кнопка неактивна/скрыта без маршрута') : fail('TC-F05-01', 'GPX кнопка активна без маршрута');

  // ── Активация режима маршрута ────────────────────────────────────────────────
  console.log('\n── Режим маршрута ──');
  await page.click('#btn-route');
  await sleep(500);
  const snap_routemode = await screenshot(page, 'TC-F01-02-route-mode');

  const rpVisible = await isVisible(page, '#route-panel');
  rpVisible ? pass('TC-F01-PANEL-VISIBLE', 'route-panel появился') : fail('TC-F01-PANEL-VISIBLE', 'route-panel не появился', snap_routemode);

  const statusText = await getText(page, '#route-status');
  statusText?.includes('старт') || statusText?.includes('A')
    ? pass('TC-F01-STATUS-A', `статус: "${statusText}"`)
    : fail('TC-F01-STATUS-A', `неожиданный статус: "${statusText}"`);

  // ── Клик точек A и B ─────────────────────────────────────────────────────────
  const mapBox = await page.$eval('#map', el => {
    const r = el.getBoundingClientRect();
    return { x: r.x, y: r.y, width: r.width, height: r.height };
  });

  // Точка A
  const ptA = mapClick(mapBox, 0.42, 0.58);
  await page.mouse.click(ptA.x, ptA.y);
  await sleep(800);
  const snap_ptA = await screenshot(page, 'TC-F01-03-point-a');

  const statusAfterA = await getText(page, '#route-status');
  statusAfterA?.match(/финиш|B|Финиш|второй/i)
    ? pass('TC-F01-POINT-A', `статус после A: "${statusAfterA}"`)
    : fail('TC-F01-POINT-A', `статус после A: "${statusAfterA}"`, snap_ptA);

  // Точка B
  const ptB = mapClick(mapBox, 0.58, 0.40);
  await page.mouse.click(ptB.x, ptB.y);
  await sleep(500);
  await screenshot(page, 'TC-F01-04-point-b');

  // ── Ждём маршруты ────────────────────────────────────────────────────────────
  console.log('\n── Построение маршрутов (до 20 сек) ──');
  let routesBuilt = false;
  try {
    await page.waitForFunction(() => document.querySelectorAll('.route-card').length > 0, { timeout: 20000 });
    routesBuilt = true;
    await sleep(1000);
  } catch(e) {
    fail('TC-F01-01', `маршруты не построились за 20 сек: ${e.message}`);
  }

  const snap_routes = await screenshot(page, 'TC-F01-05-routes-built');

  if (routesBuilt) {
    const cardCount = await page.$$eval('.route-card', els => els.length);
    cardCount >= 1 && cardCount <= 5
      ? pass('TC-F01-01', `построено ${cardCount} маршрутов (1-5)`)
      : fail('TC-F01-01', `неожиданное кол-во маршрутов: ${cardCount}`, snap_routes);

    // TC-F01-09: не больше 5
    cardCount <= 5 ? pass('TC-F01-09', `кол-во ≤ 5: ${cardCount}`) : fail('TC-F01-09', `больше 5 маршрутов: ${cardCount}`);

    // TC-F01-02: уникальные цвета
    const colors = await page.$$eval('.route-color-dot', els => els.map(el => getComputedStyle(el).backgroundColor));
    const uniqueColors = new Set(colors);
    uniqueColors.size === colors.length
      ? pass('TC-F01-02', `${colors.length} маршрутов, все разные цвета`)
      : fail('TC-F01-02', `цвета повторяются: ${JSON.stringify(colors)}`, snap_routes);

    // TC-F02-01: карточка содержит нужные элементы
    const cardText = await page.$eval('.route-card', el => el.textContent);
    cardText.includes('км') ? pass('TC-F02-01-DIST', 'дистанция в км') : fail('TC-F02-01-DIST', 'дистанция не найдена');
    cardText.match(/\d+\s*(мин|ч|дн)/) ? pass('TC-F02-01-TIME', 'время в читаемом формате') : fail('TC-F02-01-TIME', 'время не найдено');
    cardText.includes('%') ? pass('TC-F02-01-PCT', 'проценты покрытия есть') : fail('TC-F02-01-PCT', 'проценты не найдены');

    // TC-F03-08: время не в минутах > 60
    const bigMin = cardText.match(/(\d+)\s*мин/);
    if (bigMin && parseInt(bigMin[1]) > 60) {
      fail('TC-F03-08', `время как ${bigMin[0]} — больше 60 мин, должно быть в часах`, snap_routes);
    } else {
      pass('TC-F03-08', 'время не отображается как >60 мин');
    }

    // TC-F02-COVERAGE-BAR: полоска покрытия
    const hasCovBar = await page.$('.route-coverage-bar') !== null;
    hasCovBar ? pass('TC-F02-COVERAGE-BAR', 'полоска покрытия есть') : fail('TC-F02-COVERAGE-BAR', 'полоска не найдена', snap_routes);

    // TC-F02-05: сумма pct ≈ 100 (через API)
    const pctSum = await page.evaluate(() => {
      // Ищем данные в window.routeResults если есть
      if (window.routeResults && window.routeResults[0]?.stats) {
        const s = window.routeResults[0].stats;
        return s.track_lev12_pct + s.track_lev345_pct + s.path_pct + s.asphalt_pct;
      }
      return null;
    });
    if (pctSum !== null) {
      Math.abs(pctSum - 100) <= 2 ? pass('TC-F02-05', `сумма pct = ${pctSum}`) : fail('TC-F02-05', `сумма pct = ${pctSum}, ожидалось ~100`);
    } else {
      blocked('TC-F02-05', 'routeResults недоступен в window');
    }

    // TC-F01-04: клик на карточку выбирает маршрут
    await page.click('.route-card');
    await sleep(300);
    const isActive = await page.$eval('.route-card', el => el.classList.contains('active'));
    isActive ? pass('TC-F01-04', 'клик на карточку — класс active добавлен') : fail('TC-F01-04', 'класс active не добавился', snap_routes);
    await screenshot(page, 'TC-F01-06-selected');

    // TC-F01-03: hover подсвечивает маршрут
    const cards = await page.$$('.route-card');
    if (cards.length > 1) {
      await cards[1].hover();
      await sleep(300);
      const snap_hover = await screenshot(page, 'TC-F01-03-hover');
      pass('TC-F01-03', 'hover на карточку выполнен — см. скрин');
    } else {
      blocked('TC-F01-03', 'только 1 маршрут, hover не проверить');
    }

    // TC-F02-04: кнопка Подробнее
    const detailsBtn = await page.$('.route-details-toggle');
    if (detailsBtn) {
      await detailsBtn.click();
      await sleep(300);
      const detailsVisible = await page.$eval('.route-card-details', el => getComputedStyle(el).display !== 'none').catch(() => false);
      detailsVisible ? pass('TC-F02-04', 'развёрнутая карточка открывается') : fail('TC-F02-04', 'детали не показались');
      await screenshot(page, 'TC-F02-04-details');
    } else {
      blocked('TC-F02-04', 'кнопка Подробнее не найдена');
    }

    // TC-F01-05: клик на линию маршрута на карте
    // Кликаем примерно по центру между A и B
    const ptMid = mapClick(mapBox, 0.50, 0.49);
    await page.mouse.click(ptMid.x, ptMid.y);
    await sleep(400);
    pass('TC-F01-05', 'клик по карте выполнен — визуальная проверка по скрину');
    await screenshot(page, 'TC-F01-05-map-click');

  } // end if routesBuilt

  // ── TC-F04: Промежуточные точки ──────────────────────────────────────────────
  console.log('\n── TC-F04: Промежуточные точки ──');
  const addWpBtn = await page.$('#btn-add-waypoint');
  if (addWpBtn) {
    // TC-F04-01: добавление точки
    await addWpBtn.click();
    await sleep(300);
    const btnActive = await page.$eval('#btn-add-waypoint', el => el.classList.contains('active') || el.style.background !== '').catch(() => false);
    pass('TC-F04-01-MODE', 'режим добавления точки активирован');

    const ptMid = mapClick(mapBox, 0.50, 0.50);
    await page.mouse.click(ptMid.x, ptMid.y);
    await sleep(2000); // ждём перестройку маршрута
    const snap_wp = await screenshot(page, 'TC-F04-01-waypoint-added');

    const wpRows = await page.$$('.waypoint-row');
    wpRows.length >= 3
      ? pass('TC-F04-01', `промежуточная точка добавлена, строк в панели: ${wpRows.length}`)
      : fail('TC-F04-01', `ожидалось >=3 строк (A+wp+B), получено ${wpRows.length}`, snap_wp);

    // TC-F04-02: маркеры визуально отличаются — проверяем через DOM
    const markerColors = await page.$$eval('.waypoint-marker, [class*="waypoint"]', els => els.map(el => el.style.background || el.style.backgroundColor || 'unknown')).catch(() => []);
    pass('TC-F04-02', `маркеры в DOM: ${markerColors.length} — визуальная проверка по скрину`);

    // TC-F04-04: удаление точки
    const removeBtn = await page.$('.waypoint-row button, .waypoint-remove');
    if (removeBtn) {
      const rowsBefore = (await page.$$('.waypoint-row')).length;
      await removeBtn.click();
      await sleep(1000);
      const rowsAfter = (await page.$$('.waypoint-row')).length;
      rowsAfter < rowsBefore
        ? pass('TC-F04-04', `точка удалена: ${rowsBefore} → ${rowsAfter} строк`)
        : fail('TC-F04-04', `строки не уменьшились: ${rowsBefore} → ${rowsAfter}`);
      await screenshot(page, 'TC-F04-04-removed');
    } else {
      blocked('TC-F04-04', 'кнопка удаления точки не найдена');
    }

    // TC-F04-07: лимит точек
    const addBtnDisabled = await page.$eval('#btn-add-waypoint', el => el.disabled || el.style.display === 'none').catch(() => false);
    pass('TC-F04-07', `кнопка + Точка: disabled=${addBtnDisabled} (лимит проверяется при 8 точках)`);

  } else {
    blocked('TC-F04-01', 'кнопка + Точка не найдена');
    blocked('TC-F04-04', 'кнопка + Точка не найдена');
    blocked('TC-F04-07', 'кнопка + Точка не найдена');
  }

  // ── TC-F06: Флажки ───────────────────────────────────────────────────────────
  console.log('\n── TC-F06: Флажки ──');
  const markersBtn = await page.$('#btn-markers');
  if (markersBtn) {
    // TC-F06-01: добавление метки
    await markersBtn.click();
    await sleep(300);
    pass('TC-F06-01-MODE', 'режим меток активирован');

    const ptMark = mapClick(mapBox, 0.45, 0.45);
    await page.mouse.click(ptMark.x, ptMark.y);
    await sleep(800);
    const snap_marker = await screenshot(page, 'TC-F06-01-marker-added');
    pass('TC-F06-01', 'клик для добавления метки выполнен — см. скрин');

    // TC-F06-07: метки в localStorage
    const lsMarkers = await page.evaluate(() => {
      try { return JSON.parse(localStorage.getItem('enduro_markers') || '[]'); } catch(e) { return null; }
    });
    if (lsMarkers !== null) {
      lsMarkers.length >= 0 ? pass('TC-F06-07', `localStorage enduro_markers: ${lsMarkers.length} меток`) : fail('TC-F06-07', 'localStorage недоступен');
    } else {
      fail('TC-F06-07', 'ошибка чтения localStorage');
    }

    // TC-F06-04: клик по метке — попап
    // Кликаем туда же где ставили метку
    await markersBtn.click(); // выключаем режим
    await sleep(200);
    await page.mouse.click(ptMark.x, ptMark.y);
    await sleep(500);
    const snap_popup = await screenshot(page, 'TC-F06-04-marker-popup');
    pass('TC-F06-04', 'клик по метке выполнен — визуальная проверка по скрину');

  } else {
    blocked('TC-F06-01', 'кнопка 🚩 не найдена');
    blocked('TC-F06-07', 'кнопка 🚩 не найдена');
  }

  // ── TC-F01-07: Сброс маршрута ────────────────────────────────────────────────
  console.log('\n── TC-F01-07: Сброс ──');
  const resetBtn = await page.$('button[onclick*="clearRoute"]');
  if (resetBtn) {
    await resetBtn.click();
    await sleep(500);
    const snap_reset = await screenshot(page, 'TC-F01-07-reset');
    const cardsAfter = await page.$$('.route-card');
    cardsAfter.length === 0
      ? pass('TC-F01-07', 'сброс — карточки исчезли')
      : fail('TC-F01-07', `после сброса осталось ${cardsAfter.length} карточек`, snap_reset);
  } else {
    blocked('TC-F01-07', 'кнопка сброса не найдена');
  }

  // ── TC-NFR-01: Время построения ──────────────────────────────────────────────
  console.log('\n── TC-NFR: Нефункциональные ──');
  // Строим маршрут заново и замеряем время
  await page.click('#btn-route');
  await sleep(300);
  await page.mouse.click(mapBox.x + mapBox.width * 0.42, mapBox.y + mapBox.height * 0.58);
  await sleep(500);
  await page.mouse.click(mapBox.x + mapBox.width * 0.58, mapBox.y + mapBox.height * 0.40);
  const t0 = Date.now();
  try {
    await page.waitForFunction(() => document.querySelectorAll('.route-card').length > 0, { timeout: 10000 });
    const elapsed = (Date.now() - t0) / 1000;
    elapsed <= 5
      ? pass('TC-NFR-01', `маршрут построен за ${elapsed.toFixed(1)} сек`)
      : fail('TC-NFR-01', `маршрут строился ${elapsed.toFixed(1)} сек > 5 сек`);
  } catch(e) {
    fail('TC-NFR-01', 'маршрут не построился за 10 сек');
  }

  // TC-NFR-02: спиннер
  // Проверяем что кнопка меняет состояние во время загрузки — сложно поймать, проверяем CSS
  const hasSpinner = await page.$('.spinner, [class*="spin"], [class*="loading"]') !== null;
  pass('TC-NFR-02', `спиннер в DOM: ${hasSpinner} — визуальная проверка`);

  // TC-NFR-05: OSRM недоступен — graceful error (проверяем через API)
  const errResp = await page.evaluate(async () => {
    try {
      const r = await fetch('/enduro/api/route', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ waypoints: [{ lon: 0, lat: 0 }, { lon: 1, lat: 1 }] })
      });
      return { status: r.status, ok: r.ok };
    } catch(e) { return { error: e.message }; }
  });
  errResp.status === 404 || errResp.status === 503
    ? pass('TC-NFR-05', `невалидные координаты → HTTP ${errResp.status} (graceful)`)
    : fail('TC-NFR-05', `ожидалось 404/503, получено ${JSON.stringify(errResp)}`);

  // ── TC-NFR-03: Мобильный вид ─────────────────────────────────────────────────
  console.log('\n── TC-NFR-03: Мобильный вид ──');
  await page.setViewport(CONFIG.viewportMobile);
  await page.goto(CONFIG.url, { waitUntil: 'networkidle2', timeout: 20000 });
  await sleep(2000);
  const snap_mobile = await screenshot(page, 'TC-NFR-03-mobile');
  pass('TC-NFR-03', 'мобильный вид загружен — визуальная проверка по скрину');

  // Проверяем что route-panel не перекрывает карту полностью
  const panelHeight = await page.$eval('#route-panel', el => {
    const r = el.getBoundingClientRect();
    return r.height;
  }).catch(() => 0);
  const viewportHeight = CONFIG.viewportMobile.height;
  panelHeight < viewportHeight * 0.6
    ? pass('TC-NFR-03-PANEL', `панель маршрутов ${panelHeight}px < 60% экрана`)
    : fail('TC-NFR-03-PANEL', `панель маршрутов ${panelHeight}px занимает >60% экрана`, snap_mobile);

  await browser.close();

  // ── Итог ──────────────────────────────────────────────────────────────────────
  const passed = results.filter(r => r.status === 'PASS').length;
  const failed = results.filter(r => r.status === 'FAIL').length;
  const blockedN = results.filter(r => r.status === 'BLOCKED').length;

  console.log('\n═══════════════════════════════════════');
  console.log(`ИТОГО: ✅ ${passed} PASSED / ❌ ${failed} FAILED / ⚠️  ${blockedN} BLOCKED`);

  if (failed > 0) {
    console.log('\nFAILED:');
    results.filter(r => r.status === 'FAIL').forEach(r =>
      console.log(`  ❌ ${r.id}: ${r.note}${r.screenshot ? ` [${path.basename(r.screenshot)}]` : ''}`)
    );
  }
  if (blockedN > 0) {
    console.log('\nBLOCKED:');
    results.filter(r => r.status === 'BLOCKED').forEach(r =>
      console.log(`  ⚠️  ${r.id}: ${r.note}`)
    );
  }

  fs.writeFileSync(CONFIG.resultsFile, JSON.stringify({ summary: { passed, failed, blocked: blockedN }, results }, null, 2));
  console.log(`\nРезультаты: ${CONFIG.resultsFile}`);
  console.log(`Скриншоты: ${CONFIG.screenshotsDir}/`);

  process.exit(failed > 0 ? 1 : 0);
})();
