'use strict';

/**
 * ET-002 — поведенческие unit-тесты чекбокса видимости POI.
 *
 * Покрывают TP-01..TP-04 из docs/work-items/ET-002/04-test-plan.yaml.
 *
 * Тесты исполняют РЕАЛЬНЫЙ код из src/web/app.js: блок POI извлекается
 * по маркерам `>>> ET-002 POI visibility block` и оборачивается в
 * фабрику через `new Function`, которой передаются мок-зависимости
 * (window, document, localStorage, layerState, layerGroups). Так
 * монолитный browser-скрипт проверяется без полной загрузки в Node.
 *
 * Запуск: `node --test tests/unit/poi_toggle.test.js`
 * (в CI оборачивается pytest-тестом tests/unit/test_poi_toggle.py).
 */

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const APP_JS = path.join(__dirname, '..', '..', 'src', 'web', 'app.js');

/**
 * Извлекает блок POI-логики из app.js и собирает из него модуль,
 * подставляя переданные зависимости.
 */
function loadPoiModule(deps) {
  const src = fs.readFileSync(APP_JS, 'utf8');
  const m = src.match(
    /\/\/ >>> ET-002 POI visibility block[^\n]*\n([\s\S]*?)\/\/ <<< ET-002 POI visibility block/
  );
  assert.ok(m, 'POI-блок не найден в app.js (маркеры ET-002 отсутствуют)');
  const factory = new Function(
    'layerState', 'layerGroups', 'window', 'document', 'localStorage',
    m[1] + '\nreturn { applyPoiVisibility, onPoiCheckbox, restorePoiState };'
  );
  return factory(
    deps.layerState, deps.layerGroups, deps.window, deps.document, deps.localStorage
  );
}

/**
 * Готовит изолированное мок-окружение для одного теста.
 * @param {object} [opts]
 * @param {string} [opts.stored] - значение ключа poi-visible в localStorage
 *   (если не указан — ключ отсутствует).
 * @param {boolean} [opts.layerExists] - что возвращает map.getLayer().
 */
function makeEnv({ stored, layerExists = true } = {}) {
  const calls = { setLayoutProperty: [], setItem: [] };
  const store = {};
  if (stored !== undefined) store['poi-visible'] = stored;

  const localStorage = {
    getItem: (k) => (k in store ? store[k] : null),
    setItem: (k, v) => { store[k] = String(v); calls.setItem.push([k, String(v)]); },
  };
  const map = {
    getLayer: () => layerExists,
    setLayoutProperty: (id, prop, val) => calls.setLayoutProperty.push([id, prop, val]),
  };
  const checkbox = { checked: true };
  const document = {
    getElementById: (id) => (id === 'poi-visible-cb' ? checkbox : null),
  };
  const layerState = { tracks: true, paths: true, poi: true, basemap: true };
  const layerGroups = { poi: ['poi-circles', 'poi-labels'] };
  const win = { _map: map };

  const mod = loadPoiModule({
    layerState, layerGroups, window: win, document, localStorage,
  });
  return { mod, calls, checkbox, layerState, store };
}

// ── TP-01: onPoiCheckbox() скрывает слои при checked=false ───────────────
test('TP-01: снятый чекбокс скрывает слои POI и сохраняет 0', () => {
  const env = makeEnv();
  env.checkbox.checked = false;

  env.mod.onPoiCheckbox();

  assert.deepEqual(env.calls.setLayoutProperty, [
    ['poi-circles', 'visibility', 'none'],
    ['poi-labels', 'visibility', 'none'],
  ]);
  assert.deepEqual(env.calls.setItem, [['poi-visible', '0']]);
  assert.equal(env.layerState.poi, false);
});

// ── TP-02: onPoiCheckbox() показывает слои при checked=true ──────────────
test('TP-02: установленный чекбокс показывает слои POI и сохраняет 1', () => {
  const env = makeEnv();
  env.checkbox.checked = true;

  env.mod.onPoiCheckbox();

  assert.deepEqual(env.calls.setLayoutProperty, [
    ['poi-circles', 'visibility', 'visible'],
    ['poi-labels', 'visibility', 'visible'],
  ]);
  assert.deepEqual(env.calls.setItem, [['poi-visible', '1']]);
  assert.equal(env.layerState.poi, true);
});

// ── TP-03: восстановление состояния — POI скрыты ─────────────────────────
test('TP-03: restorePoiState() при poi-visible=0 скрывает POI', () => {
  const env = makeEnv({ stored: '0' });

  env.mod.restorePoiState();

  assert.equal(env.checkbox.checked, false);
  assert.equal(env.layerState.poi, false);
  assert.deepEqual(env.calls.setLayoutProperty, [
    ['poi-circles', 'visibility', 'none'],
    ['poi-labels', 'visibility', 'none'],
  ]);
  // restore не должен переписывать localStorage
  assert.deepEqual(env.calls.setItem, []);
});

// ── TP-04: восстановление состояния — POI видны (default) ────────────────
test('TP-04: restorePoiState() без ключа включает POI по умолчанию', () => {
  const env = makeEnv();

  env.mod.restorePoiState();

  assert.equal(env.checkbox.checked, true);
  assert.equal(env.layerState.poi, true);
  assert.deepEqual(env.calls.setLayoutProperty, [
    ['poi-circles', 'visibility', 'visible'],
    ['poi-labels', 'visibility', 'visible'],
  ]);
});

// ── Доп.: значение '1' восстанавливает видимость явно ────────────────────
test('restorePoiState() при poi-visible=1 показывает POI', () => {
  const env = makeEnv({ stored: '1' });

  env.mod.restorePoiState();

  assert.equal(env.checkbox.checked, true);
  assert.equal(env.layerState.poi, true);
});

// ── Доп.: POI-логика не трогает чужие слои (дух TP-08) ───────────────────
test('onPoiCheckbox() меняет только слои poi-circles и poi-labels', () => {
  const env = makeEnv();
  env.checkbox.checked = false;

  env.mod.onPoiCheckbox();

  const touched = env.calls.setLayoutProperty.map((c) => c[0]);
  assert.deepEqual([...touched].sort(), ['poi-circles', 'poi-labels']);
});

// ── Доп.: layerState синхронизируется даже без слоёв на карте ────────────
test('applyPoiVisibility() обновляет layerState даже если слой ещё не добавлен', () => {
  const env = makeEnv({ layerExists: false });

  env.mod.applyPoiVisibility(false);

  assert.equal(env.layerState.poi, false);
  assert.deepEqual(env.calls.setLayoutProperty, []);
});
