'use strict';

/**
 * ET-014 — поведенческие unit-тесты для closeTerrainPopup() и openSheet().
 *
 * Покрывают TC-U-01..TC-U-02 (часть) из docs/work-items/ET-014/04-test-plan.yaml,
 * а также проверяют логику ADR-019: при открытии любого bottom-sheet
 * `#terrain-popup` принудительно закрывается, а `#terrain-toggle` теряет
 * класс `.active`. Поведение базируется на JS-функциях из блока ET-014 в
 * src/web/app.js (между маркерами `// >>> ET-014 sheet-popup yield block`
 * и `// <<< ET-014 sheet-popup yield block <<<`).
 *
 * Запуск: `node --test tests/unit/sheet_popup.test.js`
 * (в CI оборачивается pytest-тестом tests/unit/test_sheet_popup.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');

/**
 * Извлекает ET-014-блок из app.js и собирает из него модуль, подставляя
 * переданные зависимости (window, document, closeTerrainOnOutside,
 * closeSheet). Стиль повторяет загрузчик ET-007 (base_layer.test.js).
 */
function loadEt014Module(deps) {
  const src = fs.readFileSync(APP_JS, 'utf8');
  const m = src.match(
    /\/\/ >>> ET-014 sheet-popup yield block[^\n]*\n([\s\S]*?)\/\/ <<< ET-014 sheet-popup yield block/
  );
  assert.ok(m, 'ET-014-блок не найден в app.js (маркеры отсутствуют)');
  const factory = new Function(
    'window', 'document', 'closeTerrainOnOutside', 'closeSheet',
    m[1] + '\nreturn { closeTerrainPopup, openSheet };'
  );
  return factory(
    deps.window,
    deps.document,
    deps.closeTerrainOnOutside || (() => {}),
    deps.closeSheet || (() => {}),
  );
}

/**
 * Готовит мок-DOM: #terrain-popup, #terrain-toggle, #sheet-backdrop,
 * а также произвольный набор bottom-sheets. Каждый bottom-sheet имеет
 * classList с методами add/remove/contains и querySelectorAll-совместимый
 * матчинг по селектору '.bottom-sheet.open' (через document.querySelectorAll).
 */
function makeEnv({
  popupVisible = false,
  toggleActive = false,
  sheets = [],            // [{ id, open }]
  backdropVisible = false,
} = {}) {
  const popup = {
    style: { display: popupVisible ? 'block' : 'none' },
  };
  const _toggleClasses = new Set(['map-btn']);
  if (toggleActive) _toggleClasses.add('active');
  const toggle = {
    classList: {
      _classes: _toggleClasses,
      add(c) { this._classes.add(c); },
      remove(c) { this._classes.delete(c); },
      contains(c) { return this._classes.has(c); },
    },
  };

  const _backdropClasses = new Set();
  if (backdropVisible) _backdropClasses.add('visible');
  const backdrop = {
    classList: {
      _classes: _backdropClasses,
      add(c) { this._classes.add(c); },
      remove(c) { this._classes.delete(c); },
      contains(c) { return this._classes.has(c); },
    },
  };

  // Bottom-sheets с classList API.
  const sheetEls = sheets.map(({ id, open }) => {
    const _classes = new Set(['bottom-sheet']);
    if (open) _classes.add('open');
    return {
      id,
      classList: {
        _classes,
        add(c) { this._classes.add(c); },
        remove(c) { this._classes.delete(c); },
        contains(c) { return this._classes.has(c); },
      },
    };
  });

  const docCalls = {
    removeEventListener: [],
  };

  const document = {
    getElementById(id) {
      if (id === 'terrain-popup') return popup;
      if (id === 'terrain-toggle') return toggle;
      if (id === 'sheet-backdrop') return backdrop;
      const s = sheetEls.find((e) => e.id === id);
      return s || null;
    },
    querySelectorAll(selector) {
      if (selector === '.bottom-sheet.open') {
        return sheetEls.filter((s) => s.classList.contains('open'));
      }
      return [];
    },
    removeEventListener(type, fn) {
      docCalls.removeEventListener.push([type, fn]);
    },
    addEventListener() { /* not used by closeTerrainPopup */ },
  };

  return { document, popup, toggle, backdrop, sheetEls, docCalls };
}

// ─── TC-U-02 (часть А): popup закрывается при открытии sheet ────────────
test('TC-U-02: openSheet() закрывает открытый terrain-popup и снимает .active', () => {
  const env = makeEnv({
    popupVisible: true,
    toggleActive: true,
    sheets: [{ id: 'sheet-gps-filters', open: false }],
  });
  const mod = loadEt014Module({ document: env.document });

  mod.openSheet('sheet-gps-filters');

  assert.equal(env.popup.style.display, 'none', 'popup должен быть скрыт');
  assert.ok(!env.toggle.classList.contains('active'),
    'кнопка #terrain-toggle должна потерять класс active');
});

// ─── REQ-F-04 / AC-06: повторное открытие стабильно ─────────────────────
test('REQ-F-04: повторный openSheet() — sheet остаётся open, без артефактов', () => {
  const env = makeEnv({
    popupVisible: false,
    sheets: [{ id: 'sheet-gps-filters', open: false }],
  });
  const mod = loadEt014Module({ document: env.document });

  mod.openSheet('sheet-gps-filters');
  const sheet = env.sheetEls.find((s) => s.id === 'sheet-gps-filters');
  assert.ok(sheet.classList.contains('open'), 'sheet должен иметь класс open');
  assert.ok(env.backdrop.classList.contains('visible'),
    'backdrop должен быть видим');

  // Повторный вызов — sheet остаётся открытым, никаких регрессий.
  mod.openSheet('sheet-gps-filters');
  assert.ok(sheet.classList.contains('open'), 'sheet всё ещё open');
  assert.ok(env.backdrop.classList.contains('visible'),
    'backdrop всё ещё visible');
});

// ─── REQ-F-06: другие sheets — popup-helper тоже срабатывает (но no-op) ─
test('REQ-F-06: openSheet() для других sheets тоже зовёт closeTerrainPopup', () => {
  // Popup закрыт изначально — closeTerrainPopup должна быть no-op.
  const env = makeEnv({
    popupVisible: false,
    sheets: [
      { id: 'sheet-route', open: false },
      { id: 'sheet-recon', open: false },
    ],
  });
  const mod = loadEt014Module({ document: env.document });

  mod.openSheet('sheet-route');
  const sheet = env.sheetEls.find((s) => s.id === 'sheet-route');
  assert.ok(sheet.classList.contains('open'));
  assert.equal(env.popup.style.display, 'none', 'popup остаётся скрытым');
  assert.ok(!env.toggle.classList.contains('active'),
    'active не появляется (popup и не был открыт)');
});

// ─── closeTerrainPopup — no-op если popup уже скрыт ─────────────────────
test('closeTerrainPopup: no-op если popup уже скрыт', () => {
  const env = makeEnv({ popupVisible: false });
  const mod = loadEt014Module({ document: env.document });

  mod.closeTerrainPopup();

  assert.equal(env.popup.style.display, 'none');
  // removeEventListener не должен вызываться (нечего отписывать).
  assert.equal(env.docCalls.removeEventListener.length, 0,
    'removeEventListener не должен вызываться при закрытом popup');
});

// ─── closeTerrainPopup: отписывает closeTerrainOnOutside ────────────────
test('closeTerrainPopup: при открытом popup отписывает click-listener', () => {
  const env = makeEnv({ popupVisible: true, toggleActive: true });
  const dummyHandler = function closeTerrainOnOutside() {};
  const mod = loadEt014Module({
    document: env.document,
    closeTerrainOnOutside: dummyHandler,
  });

  mod.closeTerrainPopup();

  assert.equal(env.popup.style.display, 'none');
  assert.ok(!env.toggle.classList.contains('active'));
  assert.equal(env.docCalls.removeEventListener.length, 1,
    'removeEventListener должен быть вызван 1 раз');
  assert.equal(env.docCalls.removeEventListener[0][0], 'click');
  assert.equal(env.docCalls.removeEventListener[0][1], dummyHandler);
});

// ─── closeTerrainPopup: безопасен при отсутствии #terrain-popup ─────────
test('closeTerrainPopup: безопасен если #terrain-popup отсутствует', () => {
  const env = makeEnv({ popupVisible: false });
  // Перекроем getElementById чтобы вернуть null для terrain-popup.
  const origGet = env.document.getElementById.bind(env.document);
  env.document.getElementById = (id) => (id === 'terrain-popup' ? null : origGet(id));
  const mod = loadEt014Module({ document: env.document });

  assert.doesNotThrow(() => mod.closeTerrainPopup());
});

// ─── openSheet: ранний выход если sheet не найден (без побочных эффектов) ─
test('openSheet: ранний выход если sheet не найден (popup не трогается)', () => {
  const env = makeEnv({ popupVisible: true, toggleActive: true });
  const mod = loadEt014Module({ document: env.document });

  mod.openSheet('does-not-exist');

  // popup остаётся открытым: helper вызывается ПОСЛЕ null-check на sheet.
  assert.equal(env.popup.style.display, 'block',
    'popup должен остаться открытым, если sheet не найден');
  assert.ok(env.toggle.classList.contains('active'));
});

// ─── REQ-F-01: закрытие конкурирующих sheets продолжает работать ────────
test('openSheet: закрывает другие открытые sheets (через closeSheet)', () => {
  const env = makeEnv({
    sheets: [
      { id: 'sheet-route', open: true },
      { id: 'sheet-gps-filters', open: false },
    ],
  });
  const closeSheetCalls = [];
  const mod = loadEt014Module({
    document: env.document,
    closeSheet: (id) => closeSheetCalls.push(id),
  });

  mod.openSheet('sheet-gps-filters');

  assert.deepEqual(closeSheetCalls, ['sheet-route'],
    'closeSheet должен быть вызван для sheet-route');
  const target = env.sheetEls.find((s) => s.id === 'sheet-gps-filters');
  assert.ok(target.classList.contains('open'));
});
